Compare commits

...
26 Commits
Author SHA1 Message Date
Ronan Berder b3c5d16e28 Update ReallySimpleDocs to 1.0.1 2026-06-24 20:17:41 +08:00
Ronan Berder a8c066306b Update RSD docs integration 2026-06-24 19:07:23 +08:00
Ronan Berder bbdfe1d272 Disable HTMX boost on docs home link 2026-06-23 18:22:45 +08:00
Ronan Berder a17eaa20d3 Prepare 1.0.0-beta.6 2026-06-23 18:14:19 +08:00
Ronan Berder fbf3f47b75 Use published reallysimpledocs beta 2026-06-22 18:33:30 +08:00
Ronan Berder cb72d80558 Prepare 1.0.0-beta.5 2026-06-22 18:19:38 +08:00
Ronan Berder 9806d6af63 Prepare 1.0.0-beta.4 2026-06-20 18:18:11 +08:00
Ronan Berder 971b2e171b Prepare 1.0.0-beta.3 2026-06-20 13:58:42 +08:00
Ronan Berder 0ae97ae627 Expand chart examples and derived surfaces 2026-06-19 16:42:15 +08:00
Ronan Berder 1d33827162 Prepare 1.0 docs and style migration 2026-06-17 11:19:31 +08:00
Ronan Berder 815e3ffffe Prepare 1.0.0-beta.2 release 2026-06-14 14:34:41 +08:00
Ronan Berder b971bce153 Build Basecoat before docs 2026-06-13 22:53:10 +08:00
Ronan Berder 47efe50c5a Migrate docs to ReallySimpleDocs 2026-06-13 12:53:17 +08:00
Ronan Berder 349dac9cf1 Prepare beta release docs and exports 2026-06-13 11:55:08 +08:00
Ronan Berder 4973389fbe Prepare 1.0.0 beta release 2026-06-08 21:42:31 +08:00
Ronan Berder 4ccacc98a5 Remove temporary sidebar submenu 2026-06-06 18:51:39 +08:00
Ronan Berder 2e8daaef03 Fix sidebar nested item styling 2026-06-06 16:41:18 +08:00
Ronan Berder 1509cdadef Rename Cloudflare worker to basecoat 2026-06-04 15:45:13 +08:00
Ronan Berder 89b3942965 chore: bump packages to 1.0.0 2026-06-04 12:36:12 +08:00
Ronan Berder 0829ebe0bf Rename Wrangler service to basecoat 2026-06-04 12:33:01 +08:00
Ronan Berder 04ad999ce4 Regenerate Sera and Rhea docs styles 2026-06-04 12:25:46 +08:00
Ronan Berder 1e5011344f docs: tighten repo and agent guidance 2026-06-04 12:18:40 +08:00
Ronan Berder 1268f5a146 rename style pack source files 2026-06-03 23:20:23 +08:00
Ronan Berder c1cf01da45 prepare basecoat 1.0 migration 2026-06-03 23:08:41 +08:00
Ronan Berder 7e7d509e90 Migrate empty and item docs to Basecoat APIs 2026-06-03 18:59:21 +08:00
Ronan Berder 317c490e0c Align button styles across all eight variants 2026-06-01 15:10:35 +08:00
275 changed files with 34575 additions and 21937 deletions
-47
View File
@@ -1,47 +0,0 @@
import eleventyLucideicons from "@grimlink/eleventy-plugin-lucide-icons";
import prettier from "prettier";
import Nunjucks from "nunjucks";
import fs from "fs";
import path from "path";
export default async function(eleventyConfig) {
eleventyConfig.addWatchTarget("src/nunjucks/");
eleventyConfig.setInputDirectory("./docs/src");
eleventyConfig.addPassthroughCopy({"docs/src/assets": "assets"});
eleventyConfig.addPassthroughCopy({ "src/js": "assets/js" });
eleventyConfig.addPassthroughCopy({ "docs/src/llms.txt": "llms.txt" });
eleventyConfig.addPlugin(eleventyLucideicons);
eleventyConfig.addShortcode("fetchCode", function(filePath) {
const absolutePath = path.resolve(process.cwd(), filePath);
try {
return fs.readFileSync(absolutePath, 'utf8');
} catch (error) {
console.error(`[Eleventy FetchCode Error] Failed to read: ${absolutePath}`, error);
return `<!-- Error fetching code for ${filePath} -->`;
}
});
eleventyConfig.addNunjucksAsyncFilter("prettyHtml", async (code, callback) => {
try {
const formattedCode = await prettier.format(code, {
parser: "html",
printWidth: 1000,
singleAttributePerLine: false,
bracketSameLine: true,
htmlWhitespaceSensitivity: "ignore"
});
callback(null, formattedCode);
} catch (err) {
console.error("Error formatting HTML with Prettier:", err);
callback(err, code);
}
});
let nunjucksEnvironment = new Nunjucks.Environment(
new Nunjucks.FileSystemLoader([
"src/nunjucks",
"docs/src/_includes"
]),
{ autoescape: true }
);
eleventyConfig.setLibrary("njk", nunjucksEnvironment);
}
+7 -3
View File
@@ -1,12 +1,16 @@
.*
!.gitignore
!.gitattributes
!.eleventy.js
_site/
node_modules/
packages/cli/dist/
packages/css/dist/
# Logs
npm-debug.log*
npm-debug.log*
dist/
docs/generated/
public/assets/
public/fragments/
+86 -101
View File
@@ -1,112 +1,97 @@
# Basecoat – Agent Guide
# Agent Rules
## What is Basecoat?
## Communication
- Keep answers concise, technical, and to the point.
- Do not use filler or glazing openers.
- Keep explanations MECE: separate facts, decisions, risks, and open questions.
- If scope is partial, state exactly what was not included.
- Challenge incorrect assumptions with evidence from code, docs, or upstream sources.
Basecoat is a vanilla JavaScript/HTML/CSS port of shadcn/ui components. It reuses shadcn/ui's design patterns and Tailwind CSS classes while maintaining semantic HTML, native browser elements, and full accessibility (ARIA attributes, proper hierarchies, keyboard navigation). The goal is to provide shadcn-quality components without React dependencies.
## Project Intent
- Basecoat is a Tailwind CSS, vanilla HTML, CSS, and JavaScript implementation of shadcn/ui's visual language for non-React stacks.
- Basecoat maps upstream shadcn/ui concepts onto simpler semantic markup; it is not a React, Radix, Base UI, `cn-*`, or `data-slot` DOM port.
- Prefer the browser platform: native elements, semantic HTML, CSS state selectors, and small vanilla JS only when needed.
## Quickstart
## Source Boundaries
- Do not hand-edit build outputs:
- `packages/*/dist/**`
- `dist/**`
- `docs/generated/**`
- `public/assets/**`
- Generated source entrypoints such as `src/css/basecoat-vega.css` are committed, but must be regenerated through `scripts/generate-css-entrypoints.js` or build scripts, not manually maintained.
- Install deps: `npm i`
- Dev docs server: `npm run docs:dev`
- Build CSS/JS: `npm run build`
- Build static docs: `npm run docs:build`
## Public API and Markup
- Keep public component APIs small: usually one root class, semantic child elements, documented ARIA/data attributes only when needed, and minimal JS for behavior.
- Avoid required child classes when meaningful markup can identify children: `input`, `textarea`, `select`, `button`, `a`, `label`, `fieldset`, `legend`, `header`, `section`, `footer`, `svg`, `kbd`, `hr`.
- Add new child classes, wrappers, `data-*` attributes, macro arguments, or public selectors only as intentional API.
- Do not add roles for styling; ARIA must describe behavior or accessibility state.
- Prefer intentional divergence over awkward imitation when upstream markup does not map cleanly to Basecoat.
## What to edit (and what not)
## Accessibility
- Start with the native element that matches the control.
- Use `<fieldset>` and `<legend>` for grouped form controls when appropriate.
- Use native `<dialog>` where possible.
- Keep composite-widget keyboard behavior predictable: skip disabled items, respect orientation, and handle Escape/Enter/Space according to role.
- Disabled custom items must be skipped by pointer and keyboard interactions.
- Use logical CSS utilities/properties for RTL; flip only icons whose meaning is directional.
- Edit sources only:
- `src/css/basecoat.css` – All component CSS classes (Tailwind + custom)
- `src/js/*.js` – Individual component JS files (kebab-case, ESM) + `basecoat.js` (component registry)
- `src/nunjucks/*.njk` and `src/jinja/*.html.jinja` – Component template macros
- `docs/src/components/*.njk` – Component documentation pages with examples
- `docs/src/_includes/` – Layout templates, partials, and navigation
- Do not edit build outputs:
- `packages/*/dist` – built CSS/JS bundles
- `_site/` – generated docs site
## CSS
- Component CSS in `src/css/components/*.css` owns structure, layout primitives, accessibility selectors, and behavior hooks.
- Style-pack CSS in `src/css/styles/*.css` owns visual choices: color, radius, shadow/ring, typography, spacing, variants, and state visuals.
- Each style bundle must stand alone. Do not load Vega/default and then undo it in another style.
- Avoid defensive resets that only compensate for another style pack.
- Prefer Tailwind logical utilities (`ps`, `pe`, `ms`, `me`, `start`, `end`, `rounded-s`, `rounded-e`) over physical left/right utilities.
- Prefer `border` for lines and separators.
- Avoid `!important`; if unavoidable, keep the selector narrow and document why.
## Project layout
## JavaScript
- JavaScript is behavior only; do not use JS to paper over CSS architecture issues.
- Register JS components with `window.basecoat.register()` and make initialization idempotent with `data-*-initialized` flags.
- Keep generated DOM minimal and aligned with documented HTML.
- Prefer native browser APIs and events.
- Keep scroll management scoped to component containers. Do not use `scrollIntoView()` when it can scroll the page unexpectedly.
- Emit custom events only for meaningful integration points.
- Workspaces: root npm workspace with publishable packages in `packages/*`.
- Packages:
- `packages/css` – built CSS and JS bundles under `dist/`.
- `packages/cli` – CLI; distributed assets live under `dist/`.
- Docs site: `docs/src` (Eleventy); output goes to `_site/`.
## Upstream Migration
- Always check the upstream shadcn/ui repo before changing a migrated component: `https://github.com/shadcn-ui/ui`.
- Use a local copy of the upstream repo for migration work.
- Review these upstream repo paths:
- components: `apps/v4/registry/bases/base/ui/`
- docs: `apps/v4/content/docs/components/`
- examples: `apps/v4/registry/bases/base/examples/`
- styles: `apps/v4/registry/styles/style-{vega,nova,maia,lyra,mira,luma,sera,rhea}.css`
- Review Basecoat files in the same pass:
- component CSS: `src/css/components/<component>.css`
- style packs: `src/css/styles/{vega,nova,maia,lyra,mira,luma,sera,rhea}.css`
- JS: `src/js/<component>.js` when behavior changes
- templates: `src/nunjucks/*.njk` and `src/jinja/*.html.jinja` when markup changes
- docs: `docs/src/components/<component>.md`
- Map upstream concepts, not implementation details. Do not port `cn-*` selectors directly.
- Classify differences as `drift fixed`, `intentional`, or `deferred`.
## Conventions
## Docs
- Docs are Basecoat API docs, not React docs.
- Update docs when supported markup, variants, states, JS behavior, templates, or macros change.
- Prefer concrete HTML examples over abstract composition trees.
- Do not include React-only APIs such as `asChild`.
- Do not add "Composition" sections unless they describe stable Basecoat markup.
- Do not add API Reference sections for native/CSS-only components unless there is a real Basecoat API.
- Structure component docs as: title and preview, `## Usage`, optional `### HTML structure` for stable markup requirements, optional `### JavaScript API` for components with Basecoat JS methods/events/options, then `## Examples`.
- Keep `Usage` focused on the minimal working snippet and important Basecoat/upstream differences; put variants, states, RTL, and richer patterns under `Examples`.
- Keep snippets minimal and aligned with current behavior.
- In HTML snippets, keep text-only elements on one line, such as `<p>Text</p>`; use multiline formatting when the element contains child elements or when line length would hurt readability.
- In HTML examples, keep icons as single-line `<svg ...>...</svg>` elements, keep simple text-only or inline-formatting elements on one line, and avoid blank separator lines between adjacent markup parts.
- Document intentional differences from upstream near the relevant example.
- ESM everywhere (`import`/`export`).
- Prettier defaults (2 spaces; single quotes per file preference).
- Filenames for assets: kebab-case (e.g., `dropdown-menu.js`, `basecoat.cdn.css`).
- JS naming: `camelCase` for vars/functions; `PascalCase` for DOM classes and exported component names when applicable.
## Validation
- Run `npm run build` after source/package changes.
- Run `npm run docs:build` after docs or docs asset changes.
- Manually verify changed components in docs across light/dark mode, style packs, variants, RTL when supported, and relevant interaction states.
## Design Principles
- **Minimal code**: Prefer native HTML elements and browser behavior over JavaScript abstractions
- **Accessibility-first**: Semantic HTML, proper ARIA attributes, keyboard navigation
- **CSS patterns**:
- Use `border` for lines/separators (not `background` on `<hr>`)
- Use `:is()` and `:has()` for compact selectors
- Use `isolation: isolate` to contain z-index stacking contexts
- Use `data-*` attributes for styling states, `aria-*` for accessibility semantics
- **Component patterns**:
- Dialog variants use wrapper classes (e.g., `.command-dialog` wraps `.command`)
- JavaScript components register with `window.basecoat.register()`
- Emit custom events for user actions (e.g., `command:select`)
- Filter disabled options (`aria-disabled="true"`) from all interactions
- **When NOT to create a component**:
- If it's just composition of existing elements (users can do it with standard HTML/Tailwind)
- If behavior is fully native (e.g., fieldset, legend)
## Adding new components
Before creating a component, ask:
1. Does shadcn/ui have it? → Port it
2. Is it pure composition? → Document pattern, no component
3. Does it need JS behavior? → Create component
4. Is it just CSS? → Add to basecoat.css, document usage
Examples:
- Field/Fieldset: CSS-only patterns (no JS component)
- Empty/Item/Input Group: Pure composition (docs only)
- Command/Select: JS behavior required (full component)
## Common tasks
- Update a component's CSS/JS: Edit `src/css/basecoat.css` and/or `src/js/*.js`.
- Update a component template: Edit `src/nunjucks/*.njk` and/or `src/jinja/*.html.jinja`.
- Add a new component: Create JS, CSS, templates, and docs page under `docs/src/components/*.njk`.
- Update navigation: Edit files in `docs/src/_includes/partials/`.
## Testing
No automated testing configured. User validates manually via the docs site (`npm run docs:dev`).
## Commits and PRs
- Commits: concise, imperative (≤72 chars), optional scope.
- Example: `select: prevent change event on init`.
- PRs: include description, linked issues (e.g., `Closes #123`), and before/after screenshots or code snippets for visual/behavioral changes.
- Keep PRs focused; update docs examples when components or APIs change.
## Environment and tooling
- Node 18+ recommended.
- Eleventy config: `.eleventy.js`.
- Tailwind via `@tailwindcss/cli` v4.
- Build uses Tailwind CLI and `terser`; both are provided via devDependencies.
## Git workflow
- `.gitattributes` configures merge strategies for generated files
- `docs/src/assets/styles.css` auto-resolves to incoming changes (regenerated by Tailwind)
- Always run `npm run build` before publishing packages
## Publishing packages
1. Bump version in root `package.json`
2. Run `npm run build` to generate dist files
3. Run `npm publish --workspaces` to publish both packages
## Package references
- `packages/css` usage and details: see `packages/css/README.md`.
- `packages/cli` usage and details: see `packages/cli/README.md`.
## Change Management
- Ask before removing intentional functionality or public API.
- Preserve backward compatibility when the repo already depends on it, especially package exports and default `basecoat.css` behavior.
- Keep generated outputs and docs consistent with source changes.
- Remove stale comments/docs during refactors.
- Keep commits focused and imperative.
- Keep release-visible changes in `CHANGELOG.md` under `## [Unreleased]`; put backlog or incomplete work in `TODO.md` or the migration tracker.
+128
View File
@@ -0,0 +1,128 @@
# Changelog
## Unreleased
## [1.0.0-beta.6] - 2026-06-23
### Changed
- Updated docs dependency to the published `reallysimpledocs@1.0.0-beta.5` package.
- Refined component documentation examples across the docs site, including Select, Table, Tabs, Switch, and Theme Switcher.
- Clarified CDN and npm installation guidance for default and named style bundles.
- Updated Table examples to avoid inline overlay menus inside scrollable table containers and documented the overflow limitation.
### Fixed
- Fixed dark-mode unchecked Switch thumb colors across all style packs to match upstream shadcn/ui behavior.
## [1.0.0-beta.5] - 2026-06-22
### Added
- Added a dedicated Accordion component with native `<details>` markup, single-item JavaScript behavior, `data-multiple`, disabled item handling, style-pack spacing, and docs.
- Added a dedicated Breadcrumb component with semantic `nav`/`ol` markup, style-pack visuals, collapsed and dropdown examples, and docs.
- Added documented Card action support with `.card-action`.
- Added Combobox clear button, popup trigger, and input-group support, with docs aligned to upstream examples.
### Fixed
- Fixed Combobox single selection reopening immediately after selection and filtering the reopened list to the selected value.
- Improved Combobox multiple chips and remove buttons to match style-pack button sizing more closely.
## [1.0.0-beta.4] - 2026-06-20
### Changed
- Renamed the compact scrollbar utility from `scrollbar-thin` to `scrollbar-sm`; `scrollbar-thin` remains available through the compatibility stylesheet.
## [1.0.0-beta.3] - 2026-06-20
### Breaking Changes
- Changed Button, Badge, Card, Avatar, and Alert visual APIs to use a canonical root class plus data attributes instead of composed visual classes. For example, use `class="btn" data-variant="outline"`, `class="badge" data-variant="secondary"`, `class="card" data-size="sm"`, `class="avatar" data-size="lg"`, and `class="alert" data-variant="destructive"`. Legacy aliases are available only through the optional compatibility stylesheet.
- Changed icon-only Button sizing from `data-icon="only"` plus optional `data-size` to upstream-aligned `data-size="icon|icon-xs|icon-sm|icon-lg"`.
### Added
- Added an opt-in `basecoat-css/compat` stylesheet for pre-1.0 default Basecoat class aliases.
- Added an optional Chart.js helper with `window.basecoat.chart()`, chart CSS, generated tooltips, generated legends, and docs.
- Added shared Alert action layout support for direct child `<footer>` action regions.
- Added dedicated Avatar and Avatar Group component styles and docs.
- Added dedicated Progress component styles and docs with label, controlled, and RTL examples.
## [1.0.0-beta.2] - 2026-06-14
### Breaking Changes
- Removed the `.form` convenience selector for Basecoat 1.0. Use explicit component classes (`label`, `input`, `textarea`, `select`) or compose fields with `.field` / `.fieldset`.
- Changed Combobox markup and behavior to an input-first structure. The visible input now filters options, the hidden input stores the submitted value, single select stores the selected value, and multiple select stores a JSON array.
- Removed Combobox `data-multiple`; use `aria-multiselectable="true"` on the Combobox listbox, matching Select.
- Changed Command markup to the migrated Basecoat structure: `.command-dialog` wraps `.command`, the search input lives in the command header, and items use role-based menu markup with `role="menuitem"`.
- Removed Combobox-specific search-header behavior from Select. Use the dedicated Combobox component for editable/filterable selection.
- Removed built-in document command events for Toast, Sidebar, and Theme. Use element methods instead: `toaster.toast(config)`, `sidebar.open()`, `sidebar.close()`, `sidebar.toggle()`, and `window.basecoat.theme.*`.
- Reworked style loading for style packs. Non-default styles are standalone bundles and should not be loaded on top of the default/Vega bundle.
### Added
- Added standalone style packs: Vega, Nova, Maia, Lyra, Mira, Luma, Sera, and Rhea.
- Added dedicated Empty component styles and docs.
- Added dedicated Item component styles and docs.
- Added dedicated Input Group component styles and docs.
- Added dedicated Spinner docs and examples using `animate-spin` and `size-4` patterns.
- Added style-specific package entrypoints such as `basecoat-css/nova` and styleless base entrypoints for custom themes.
- Added `window.basecoat.refresh(element)` as a generic dispatcher for components that expose `refresh()`.
- Added `refresh()` methods to Command, Select, Combobox, Dropdown Menu, and Tabs for dynamic child lists.
- Added method APIs for Sidebar, Toast, and Theme.
- Added `data-format="object"` to Select and Combobox for opt-in hidden input serialization as `{ value, label }` objects.
- Added `selected` details to Select and Combobox change events and JavaScript properties.
- Added `data-filter="manual"` to Command and Combobox for app-owned remote or local-search result filtering.
### Changed
- Changed `window.basecoat.init(name)` and `window.basecoat.initAll()` to initialize uninitialized components only, instead of forcing global reinitialization.
- Added internal destroy hooks for JavaScript components so removed component roots clean up event listeners and runtime state.
- Reworked component CSS so shared component files own structure and behavior hooks while style-pack files own visual treatment.
- Updated Button, Button Group, Input, Textarea, Select, Combobox, Command, Dialog, Dropdown Menu, Popover, Field, Tabs, Table, Card, Alert, Badge, Kbd, Label, Skeleton, Radio, Switch, Empty, Item, and Input Group toward current shadcn/ui styles.
- Split Native Select documentation from custom Select documentation.
- Updated docs to use Basecoat-specific HTML usage instead of React/shadcn composition APIs.
- Updated docs site styling, navigation, fonts, and style switcher for the 1.0 style system.
- Migrated the docs build to ReallySimpleDocs/Astro while keeping Basecoat's Nunjucks source examples as a pre-render step.
- Changed docs build scripts so local Basecoat package assets are generated before ReallySimpleDocs resolves `basecoat-css`.
### Removed
- Removed the old Form component page. Form layout should now be composed with Field, Fieldset, Input, Textarea, Select, Native Select, Checkbox, Radio, and Switch.
### Fixed
- Fixed destructive Alert descriptions/content using muted text instead of destructive text across all style packs.
### Migration Notes
To keep the previous `.form` wrapper behavior, define it in your own Tailwind CSS:
```css
.form label { @apply label; }
.form input { @apply input; }
.form textarea { @apply textarea; }
.form select { @apply select; }
```
To keep the previous document-event command APIs, add bridge listeners in your app:
```js
document.addEventListener('basecoat:toast', (event) => {
document.getElementById('toaster')?.toast(event.detail?.config || {});
});
document.addEventListener('basecoat:sidebar', (event) => {
const sidebar = document.getElementById(event.detail?.id || 'sidebar');
const action = event.detail?.action || 'toggle';
if (['open', 'close', 'toggle'].includes(action)) sidebar?.[action]();
});
document.addEventListener('basecoat:theme', (event) => {
const mode = event.detail?.mode;
mode ? window.basecoat.theme.set(mode) : window.basecoat.theme.toggle();
});
```
+17 -1
View File
@@ -12,4 +12,20 @@
- Test locally before submitting.
- Follow existing code style.
Thanks for helping!
## Cloudflare Workers deploys
Docs are built with 11ty and deployed as Workers Static Assets from `_site`.
- Local preview: `npm run workers:dev`
- Production deploy: `npm run workers:deploy`
- Preview version upload: `npm run workers:preview`
Workers Builds settings:
- Build command: `npm run docs:build`
- Deploy command: `npx wrangler deploy`
- Non-production branch deploy command: `npx wrangler versions upload`
- Production branch: `main`
- Enable builds for non-production branches for branch preview URLs.
Thanks for helping!
+121 -14
View File
@@ -1,29 +1,136 @@
# Basecoat
A collection of modern UI components built with Tailwind CSS that works with any web stack. Basecoat brings the magic of [shadcn/ui](https://ui.shadcn.com) to traditional web applications, no React required.
Basecoat is a Tailwind CSS, vanilla HTML/CSS/JavaScript implementation of the shadcn/ui design system. It provides shadcn-style components for any web stack without React, Radix, or framework runtime dependencies.
![screenshot](docs/src/assets/images/screenshot.png)
## Features
- **Lightweight**: no runtime JS, just CSS and a tiny bit of vanilla JavaScript for the more interactive components.
- **Easy to use**: add classes like `btn` or `input` and you're done.
- **Framework-agnostic**: works with any backend or frontend stack.
- **Accessible**: components follow accessibility best practices.
- **Dark mode ready**: respects your Tailwind config.
- **Extendable**: tweak styles with Tailwind or CSS variables.
- **Themable**: fully compatible with shadcn/ui themes.
- **Readable**: no class soup, just clean markup.
- **Free and open source**: MIT licensed.
- Semantic HTML-first components.
- Tailwind CSS v4 source files and generated CSS bundles.
- Small vanilla JavaScript for components that need behavior.
- Nunjucks and Jinja template macros.
- Standalone style packs: Vega, Nova, Maia, Lyra, Mira, Luma, Sera, and Rhea.
- Dark mode and CSS variable theming.
- CDN, npm, and CLI usage paths.
## Documentation
Visit [basecoatui.com](https://basecoatui.com).
- Website: [basecoatui.com](https://basecoatui.com)
- Installation: [basecoatui.com/installation](https://basecoatui.com/installation)
- Customization: [basecoatui.com/customization](https://basecoatui.com/customization)
## Installation
## Packages
Visit [basecoatui.com/installation](https://basecoatui.com/installation)
This repository publishes two workspace packages:
- `basecoat-css`: CSS, JavaScript, Nunjucks macros, and Jinja macros.
- `basecoat-cli`: CLI for adding Basecoat assets to a project.
Package details live in:
- `packages/css/README.md`
- `packages/cli/README.md`
## Install
```bash
npm install basecoat-css
```
Use the default bundle:
```css
@import "tailwindcss";
@import "basecoat-css";
```
Use a specific style bundle:
```css
@import "tailwindcss";
@import "basecoat-css/nova";
```
Use the styleless base plus a custom style file:
```css
@import "tailwindcss";
@import "basecoat-css/base";
@import "./style-acme.css";
```
## Repository Layout
```text
.
├── docs/
│ ├── generated/ Generated RSD/Astro docs input
│ └── src/ Documentation source pages, examples, and fragments
├── packages/
│ ├── cli/ Published CLI package
│ └── css/ Published CSS package
├── public/ Generated docs assets for Astro
├── scripts/ Build and generation scripts
└── src/
├── components/ Astro overrides for ReallySimpleDocs
├── css/
│ ├── base/ Shared tokens, base layer, and semantic utilities
│ ├── components/ Component structure and behavior hooks
│ └── styles/ Style-pack visual rules
├── docs.css Documentation-site CSS extension
├── jinja/ Jinja component macros
├── js/ Vanilla JS components and registry
└── nunjucks/ Nunjucks component macros
```
## CSS Architecture
Basecoat separates structure from style:
- `src/css/base/base.css`: shared tokens and semantic utilities.
- `src/css/components/*.css`: component layout, structure, accessibility selectors, and behavior hooks.
- `src/css/styles/*.css`: style-pack visuals such as color, radius, shadow, typography, spacing, variants, and state styles.
Generated source entrypoints are committed for transparency and package imports:
- `src/css/basecoat.css`: default backward-compatible Vega bundle.
- `src/css/basecoat-base.css`: base plus components, no style pack.
- `src/css/basecoat-components.css`: component imports only.
- `src/css/basecoat-<style>.css`: base plus one style pack.
- `src/css/basecoat-<style>.cdn.css`: CDN-compatible wrapper.
These entrypoints are generated by `scripts/generate-css-entrypoints.js` and by the build scripts.
## JavaScript Lifecycle
- `window.basecoat.init(name)` initializes uninitialized components for one registered component.
- `window.basecoat.initAll()` initializes all uninitialized registered components.
- `window.basecoat.refresh(element)` asks an initialized component to rescan dynamic children when it supports refresh.
- Components use internal destroy hooks to clean up listeners when initialized roots are removed from the DOM.
## Development
```bash
# Install dependencies.
npm i
# Generate docs input and run the ReallySimpleDocs/Astro site.
npm run docs:dev
# Build package assets.
npm run build
# Build the static docs site.
npm run docs:build
# Run the Workers docs site locally.
npm run workers:dev
# Deploy the Workers docs site.
npm run workers:deploy
```
## License
[MIT](/LICENSE.md)
[MIT](LICENSE.md)
+53
View File
@@ -0,0 +1,53 @@
import { defineConfig } from 'astro/config';
import reallySimpleDocs from 'reallysimpledocs/astro';
export default defineConfig({
site: process.env.SITE_URL || 'https://basecoatui.com',
vite: {
build: {
assetsInlineLimit: 0,
},
resolve: {
dedupe: ['basecoat-css'],
},
},
integrations: [
reallySimpleDocs({
docsDir: './docs/src',
routeBase: '/',
style: 'vega',
css: false,
bodyAttrs: {
'hx-boost': 'true',
'hx-target': '#content',
'hx-select': '#content',
'hx-swap': 'outerHTML',
'hx-push-url': 'true',
},
components: {
Head: './docs/src/site/StyleHead.astro',
SidebarHeader: './docs/src/site/SidebarHeader.astro',
ContentHeader: './docs/src/site/ContentHeader.astro',
},
site: {
title: 'Basecoat',
subtitle: 'v1.0.0-beta.4',
description: 'A components library built with Tailwind CSS that works with any web stack.',
url: process.env.SITE_URL || 'https://basecoatui.com',
keywords: ['components', 'component library', 'component system', 'UI', 'UI kit', 'shadcn', 'shadcn/ui', 'Tailwind CSS', 'Tailwind', 'CSS', 'HTML', 'Jinja', 'Nunjucks', 'JS', 'JavaScript', 'vanilla JS', 'vanilla JavaScript'],
author: {
name: 'Ronan Berder',
x: '@hunvreus',
},
assets: {
favicon: 'favicon.svg',
appleTouchIcon: 'apple-touch-icon.png',
socialImage: 'social.png',
},
logo: {
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" class="h-4 w-4"><rect width="256" height="256" fill="none"></rect><line x1="208" y1="128" x2="128" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></line><line x1="192" y1="40" x2="40" y2="192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></line></svg>',
},
},
}),
],
});
-92
View File
@@ -1,92 +0,0 @@
/* HTMX */
.htmx-indicator {
@apply invisible opacity-0 transition-discrete transition-all;
}
.htmx-indicator:not(.htmx-request) {
@apply hidden;
}
.htmx-request,
.htmx-request.htmx-indicator {
@apply visible opacity-100;
}
.content {
> h2 {
@apply mt-12 lg:mt-20 mb-6 scroll-m-22 text-2xl font-semibold tracking-tight first:mt-0;
}
> h3 {
@apply mt-8 mb-6 scroll-m-22 text-xl font-semibold tracking-tight;
}
> h4 {
@apply my-6 scroll-m-22 font-semibold tracking-tight;
}
.prose {
@apply my-6;
a {
@apply font-medium underline underline-offset-4;
}
code:not(pre > code) {
@apply relative rounded-md bg-muted/50 px-[0.3rem] py-[0.2rem] font-mono text-sm;
}
blockquote {
@apply border-l-4 border-border pl-4;
}
ol {
@apply list-decimal ml-6;
}
ul {
@apply list-disc ml-6;
}
ol,
ul {
li {
@apply mt-2;
}
}
dl {
@apply border-b pb-3 my-6;
dt {
@apply border-t pt-3 mt-3 first:mt-0;
}
dd {
@apply mt-2 space-y-2;
}
dl {
@apply mt-4 border-b-0 pb-0 pl-8 my-0;
}
}
b,
strong {
@apply font-semibold;
}
> p,
> blockquote,
> ul,
> ol,
> dl,
> table,
> pre,
> p {
@apply my-6;
}
> *:first-child {
@apply !mt-0;
}
> *:last-child {
@apply !mb-0;
}
}
.step {
counter-increment: step;
&:before {
@apply absolute w-8 h-8 md:w-9 md:h-9 bg-muted rounded-full font-mono font-semibold text-center text-sm inline-flex items-center justify-center -indent-px border-4 mr-2 border-background;
@apply md:ml-[-50px] md:mt-[-4px];
content: counter(step);
}
> h3 {
@apply max-md:ml-10 min-h-8 flex items-center;
}
}
}
-151
View File
@@ -1,151 +0,0 @@
/*
Custom Highlight.js theme with CSS variables
derived from github.min.css and github-dark.min.css
*/
:root {
/* Base */
--hljs-bg: #fff;
--hljs-color: #24292e;
/* Tokens */
--hljs-keyword: #d73a49;
--hljs-title: #6f42c1;
--hljs-literal: #005cc5;
--hljs-string: #032f62;
--hljs-symbol: #e36209;
--hljs-comment: #6a737d;
--hljs-tag: #22863a;
--hljs-section: #005cc5;
--hljs-bullet: #735c0f;
/* Diffs */
--hljs-addition-color: #22863a;
--hljs-addition-bg: #f0fff4;
--hljs-deletion-color: #b31d28;
--hljs-deletion-bg: #ffeef0;
}
html.dark {
/* Base */
--hljs-bg: #0d1117;
--hljs-color: #c9d1d9;
/* Tokens */
--hljs-keyword: #ff7b72;
--hljs-title: #d2a8ff;
--hljs-literal: #79c0ff;
--hljs-string: #a5d6ff;
--hljs-symbol: #ffa657;
--hljs-comment: #8b949e;
--hljs-tag: #7ee787;
--hljs-section: #1f6feb;
--hljs-bullet: #f2cc60;
/* Diffs */
--hljs-addition-color: #aff5b4;
--hljs-addition-bg: #033a16;
--hljs-deletion-color: #ffdcd7;
--hljs-deletion-bg: #67060c;
}
/* --- Base --- */
pre code.hljs{
display:block;
overflow-x:auto;
padding:1em
}
code.hljs{
padding:3px 5px
}
/* --- Mapped styles --- */
.hljs {
color: var(--hljs-color);
background: var(--hljs-bg);
}
.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
.hljs-template-tag,
.hljs-template-variable,
.hljs-type,
.hljs-variable.language_ {
color: var(--hljs-keyword);
}
.hljs-title,
.hljs-title.class_,
.hljs-title.class_.inherited__,
.hljs-title.function_ {
color: var(--hljs-title);
}
.hljs-attr,
.hljs-attribute,
.hljs-literal,
.hljs-meta,
.hljs-number,
.hljs-operator,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-id,
.hljs-variable {
color: var(--hljs-literal);
}
.hljs-meta .hljs-string,
.hljs-regexp,
.hljs-string {
color: var(--hljs-string);
}
.hljs-built_in,
.hljs-symbol {
color: var(--hljs-symbol);
}
.hljs-code,
.hljs-comment,
.hljs-formula {
color: var(--hljs-comment);
}
.hljs-name,
.hljs-quote,
.hljs-selector-pseudo,
.hljs-selector-tag {
color: var(--hljs-tag);
}
.hljs-subst {
color: var(--hljs-color);
}
.hljs-section {
color: var(--hljs-section);
font-weight: 700;
}
.hljs-bullet {
color: var(--hljs-bullet);
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
.hljs-addition {
color: var(--hljs-addition-color);
background-color: var(--hljs-addition-bg);
}
.hljs-deletion {
color: var(--hljs-deletion-color);
background-color: var(--hljs-deletion-bg);
}
-8
View File
@@ -1,8 +0,0 @@
@import "tailwindcss";
@import "../../src/css/basecoat.css";
@import "./custom.css";
@import "./highlight.css";
@import "./themes/claude.css";
@import "./themes/doom-64.css";
@import "./themes/supabase.css";
-95
View File
@@ -1,95 +0,0 @@
.theme-claude {
&:root {
--background: oklch(0.98 0.01 95.10);
--foreground: oklch(0.34 0.03 95.72);
--card: oklch(0.98 0.01 95.10);
--card-foreground: oklch(0.19 0.00 106.59);
--popover: oklch(1.00 0 0);
--popover-foreground: oklch(0.27 0.02 98.94);
--primary: oklch(0.62 0.14 39.04);
--primary-foreground: oklch(1.00 0 0);
--secondary: oklch(0.92 0.01 92.99);
--secondary-foreground: oklch(0.43 0.02 98.60);
--muted: oklch(0.93 0.02 90.24);
--muted-foreground: oklch(0.61 0.01 97.42);
--accent: oklch(0.92 0.01 92.99);
--accent-foreground: oklch(0.27 0.02 98.94);
--destructive: oklch(0.19 0.00 106.59);
--destructive-foreground: oklch(1.00 0 0);
--border: oklch(0.88 0.01 97.36);
--input: oklch(0.76 0.02 98.35);
--ring: oklch(0.59 0.17 253.06);
--chart-1: oklch(0.56 0.13 43.00);
--chart-2: oklch(0.69 0.16 290.41);
--chart-3: oklch(0.88 0.03 93.13);
--chart-4: oklch(0.88 0.04 298.18);
--chart-5: oklch(0.56 0.13 42.06);
--sidebar: oklch(0.97 0.01 98.88);
--sidebar-foreground: oklch(0.36 0.01 106.65);
--sidebar-primary: oklch(0.62 0.14 39.04);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.92 0.01 92.99);
--sidebar-accent-foreground: oklch(0.33 0 0);
--sidebar-border: oklch(0.94 0 0);
--sidebar-ring: oklch(0.77 0 0);
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--radius: 0.5rem;
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
}
&.dark {
--background: oklch(0.27 0.00 106.64);
--foreground: oklch(0.81 0.01 93.01);
--card: oklch(0.27 0.00 106.64);
--card-foreground: oklch(0.98 0.01 95.10);
--popover: oklch(0.31 0.00 106.60);
--popover-foreground: oklch(0.92 0.00 106.48);
--primary: oklch(0.67 0.13 38.76);
--primary-foreground: oklch(1.00 0 0);
--secondary: oklch(0.98 0.01 95.10);
--secondary-foreground: oklch(0.31 0.00 106.60);
--muted: oklch(0.22 0.00 106.71);
--muted-foreground: oklch(0.77 0.02 99.07);
--accent: oklch(0.21 0.01 95.42);
--accent-foreground: oklch(0.97 0.01 98.88);
--destructive: oklch(0.64 0.21 25.33);
--destructive-foreground: oklch(1.00 0 0);
--border: oklch(0.36 0.01 106.89);
--input: oklch(0.43 0.01 100.22);
--ring: oklch(0.59 0.17 253.06);
--chart-1: oklch(0.56 0.13 43.00);
--chart-2: oklch(0.69 0.16 290.41);
--chart-3: oklch(0.21 0.01 95.42);
--chart-4: oklch(0.31 0.05 289.32);
--chart-5: oklch(0.56 0.13 42.06);
--sidebar: oklch(0.24 0.00 67.71);
--sidebar-foreground: oklch(0.81 0.01 93.01);
--sidebar-primary: oklch(0.33 0 0);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.17 0.00 106.62);
--sidebar-accent-foreground: oklch(0.81 0.01 93.01);
--sidebar-border: oklch(0.94 0 0);
--sidebar-ring: oklch(0.77 0 0);
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--radius: 0.5rem;
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
}
}
-95
View File
@@ -1,95 +0,0 @@
.theme-doom-64 {
&:root {
--background: oklch(0.85 0 0);
--foreground: oklch(0.24 0 0);
--card: oklch(0.76 0 0);
--card-foreground: oklch(0.24 0 0);
--popover: oklch(0.76 0 0);
--popover-foreground: oklch(0.24 0 0);
--primary: oklch(0.50 0.19 27.48);
--primary-foreground: oklch(1.00 0 0);
--secondary: oklch(0.50 0.09 126.19);
--secondary-foreground: oklch(1.00 0 0);
--muted: oklch(0.78 0 0);
--muted-foreground: oklch(0.41 0 0);
--accent: oklch(0.59 0.10 245.74);
--accent-foreground: oklch(1.00 0 0);
--destructive: oklch(0.71 0.20 46.46);
--destructive-foreground: oklch(0 0 0);
--border: oklch(0.43 0 0);
--input: oklch(0.43 0 0);
--ring: oklch(0.50 0.19 27.48);
--chart-1: oklch(0.50 0.19 27.48);
--chart-2: oklch(0.50 0.09 126.19);
--chart-3: oklch(0.59 0.10 245.74);
--chart-4: oklch(0.71 0.20 46.46);
--chart-5: oklch(0.57 0.04 40.43);
--sidebar: oklch(0.76 0 0);
--sidebar-foreground: oklch(0.24 0 0);
--sidebar-primary: oklch(0.50 0.19 27.48);
--sidebar-primary-foreground: oklch(1.00 0 0);
--sidebar-accent: oklch(0.59 0.10 245.74);
--sidebar-accent-foreground: oklch(1.00 0 0);
--sidebar-border: oklch(0.43 0 0);
--sidebar-ring: oklch(0.50 0.19 27.48);
--font-sans: "Oxanium", sans-serif;
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: "Source Code Pro", monospace;
--radius: 0px;
--shadow-2xs: 0px 2px 4px 0px hsl(0 0% 0% / 0.20);
--shadow-xs: 0px 2px 4px 0px hsl(0 0% 0% / 0.20);
--shadow-sm: 0px 2px 4px 0px hsl(0 0% 0% / 0.40), 0px 1px 2px -1px hsl(0 0% 0% / 0.40);
--shadow: 0px 2px 4px 0px hsl(0 0% 0% / 0.40), 0px 1px 2px -1px hsl(0 0% 0% / 0.40);
--shadow-md: 0px 2px 4px 0px hsl(0 0% 0% / 0.40), 0px 2px 4px -1px hsl(0 0% 0% / 0.40);
--shadow-lg: 0px 2px 4px 0px hsl(0 0% 0% / 0.40), 0px 4px 6px -1px hsl(0 0% 0% / 0.40);
--shadow-xl: 0px 2px 4px 0px hsl(0 0% 0% / 0.40), 0px 8px 10px -1px hsl(0 0% 0% / 0.40);
--shadow-2xl: 0px 2px 4px 0px hsl(0 0% 0% / 1.00);
}
&.dark {
--background: oklch(0.22 0 0);
--foreground: oklch(0.91 0 0);
--card: oklch(0.29 0 0);
--card-foreground: oklch(0.91 0 0);
--popover: oklch(0.29 0 0);
--popover-foreground: oklch(0.91 0 0);
--primary: oklch(0.61 0.21 27.03);
--primary-foreground: oklch(1.00 0 0);
--secondary: oklch(0.64 0.15 133.01);
--secondary-foreground: oklch(0 0 0);
--muted: oklch(0.26 0 0);
--muted-foreground: oklch(0.71 0 0);
--accent: oklch(0.75 0.12 244.75);
--accent-foreground: oklch(0 0 0);
--destructive: oklch(0.78 0.17 68.09);
--destructive-foreground: oklch(0 0 0);
--border: oklch(0.41 0 0);
--input: oklch(0.41 0 0);
--ring: oklch(0.61 0.21 27.03);
--chart-1: oklch(0.61 0.21 27.03);
--chart-2: oklch(0.64 0.15 133.01);
--chart-3: oklch(0.75 0.12 244.75);
--chart-4: oklch(0.78 0.17 68.09);
--chart-5: oklch(0.65 0.03 40.80);
--sidebar: oklch(0.19 0 0);
--sidebar-foreground: oklch(0.91 0 0);
--sidebar-primary: oklch(0.61 0.21 27.03);
--sidebar-primary-foreground: oklch(1.00 0 0);
--sidebar-accent: oklch(0.75 0.12 244.75);
--sidebar-accent-foreground: oklch(0 0 0);
--sidebar-border: oklch(0.41 0 0);
--sidebar-ring: oklch(0.61 0.21 27.03);
--font-sans: "Oxanium", sans-serif;
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: "Source Code Pro", monospace;
--radius: 0px;
--shadow-2xs: 0px 2px 5px 0px hsl(0 0% 0% / 0.30);
--shadow-xs: 0px 2px 5px 0px hsl(0 0% 0% / 0.30);
--shadow-sm: 0px 2px 5px 0px hsl(0 0% 0% / 0.60), 0px 1px 2px -1px hsl(0 0% 0% / 0.60);
--shadow: 0px 2px 5px 0px hsl(0 0% 0% / 0.60), 0px 1px 2px -1px hsl(0 0% 0% / 0.60);
--shadow-md: 0px 2px 5px 0px hsl(0 0% 0% / 0.60), 0px 2px 4px -1px hsl(0 0% 0% / 0.60);
--shadow-lg: 0px 2px 5px 0px hsl(0 0% 0% / 0.60), 0px 4px 6px -1px hsl(0 0% 0% / 0.60);
--shadow-xl: 0px 2px 5px 0px hsl(0 0% 0% / 0.60), 0px 8px 10px -1px hsl(0 0% 0% / 0.60);
--shadow-2xl: 0px 2px 5px 0px hsl(0 0% 0% / 1.50);
}
}
-100
View File
@@ -1,100 +0,0 @@
.theme-supabase {
&:root {
--background: oklch(0.99 0 0);
--foreground: oklch(0.20 0 0);
--card: oklch(0.99 0 0);
--card-foreground: oklch(0.20 0 0);
--popover: oklch(0.99 0 0);
--popover-foreground: oklch(0.44 0 0);
--primary: oklch(0.83 0.13 160.91);
--primary-foreground: oklch(0.26 0.01 166.46);
--secondary: oklch(0.99 0 0);
--secondary-foreground: oklch(0.20 0 0);
--muted: oklch(0.95 0 0);
--muted-foreground: oklch(0.24 0 0);
--accent: oklch(0.95 0 0);
--accent-foreground: oklch(0.24 0 0);
--destructive: oklch(0.55 0.19 32.73);
--destructive-foreground: oklch(0.99 0.00 17.21);
--border: oklch(0.90 0 0);
--input: oklch(0.90 0 0);
--ring: oklch(0.83 0.13 160.91);
--chart-1: oklch(0.83 0.13 160.91);
--chart-2: oklch(0.62 0.19 259.81);
--chart-3: oklch(0.61 0.22 292.72);
--chart-4: oklch(0.77 0.16 70.08);
--chart-5: oklch(0.70 0.15 162.48);
--sidebar: oklch(0.99 0 0);
--sidebar-foreground: oklch(0.55 0 0);
--sidebar-primary: oklch(0.83 0.13 160.91);
--sidebar-primary-foreground: oklch(0.26 0.01 166.46);
--sidebar-accent: oklch(0.95 0 0);
--sidebar-accent-foreground: oklch(0.24 0 0);
--sidebar-border: oklch(0.90 0 0);
--sidebar-ring: oklch(0.83 0.13 160.91);
--font-sans: Outfit, sans-serif;
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: monospace;
--radius: 0.5rem;
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
--tracking-normal: 0.025em;
}
&.dark {
--background: oklch(0.18 0 0);
--foreground: oklch(0.93 0.01 255.51);
--card: oklch(0.20 0 0);
--card-foreground: oklch(0.93 0.01 255.51);
--popover: oklch(0.26 0 0);
--popover-foreground: oklch(0.73 0 0);
--primary: oklch(0.44 0.10 156.76);
--primary-foreground: oklch(0.92 0.01 167.16);
--secondary: oklch(0.26 0 0);
--secondary-foreground: oklch(0.99 0 0);
--muted: oklch(0.24 0 0);
--muted-foreground: oklch(0.71 0 0);
--accent: oklch(0.31 0 0);
--accent-foreground: oklch(0.99 0 0);
--destructive: oklch(0.31 0.09 29.79);
--destructive-foreground: oklch(0.94 0.00 34.31);
--border: oklch(0.28 0 0);
--input: oklch(0.28 0 0);
--ring: oklch(0.80 0.18 151.71);
--chart-1: oklch(0.80 0.18 151.71);
--chart-2: oklch(0.71 0.14 254.62);
--chart-3: oklch(0.71 0.16 293.54);
--chart-4: oklch(0.84 0.16 84.43);
--chart-5: oklch(0.78 0.13 181.91);
--sidebar: oklch(0.18 0 0);
--sidebar-foreground: oklch(0.63 0 0);
--sidebar-primary: oklch(0.44 0.10 156.76);
--sidebar-primary-foreground: oklch(0.92 0.01 167.16);
--sidebar-accent: oklch(0.31 0 0);
--sidebar-accent-foreground: oklch(0.99 0 0);
--sidebar-border: oklch(0.28 0 0);
--sidebar-ring: oklch(0.80 0.18 151.71);
--font-sans: Outfit, sans-serif;
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: monospace;
--radius: 0.5rem;
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
}
body {
letter-spacing: var(--tracking-normal);
}
}
-10
View File
@@ -1,10 +0,0 @@
export default {
title: "Basecoat",
description: "A components library built with Tailwind CSS that works with any web stack.",
keywords: ["components", "component library", "component system", "UI", "UI kit", "shadcn", "shadcn/ui", "Tailwind CSS", "Tailwind", "CSS", "HTML", "Jinja", "Nunjucks", "JS", "JavaScript", "vanilla JS", "vanilla JavaScript"],
author: {
name: "Ronan Berder",
x: "@hunvreus"
},
url: process.env.ELEVENTY_ENV === 'development' ? "http://localhost:8080" : "https://basecoatui.com"
};
-112
View File
@@ -1,112 +0,0 @@
<!DOCTYPE html>
<html>
<head>
{# Dark mode #}
<script>
(() => {
try {
const stored = localStorage.getItem('themeMode');
if (stored ? stored === 'dark'
: matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
}
} catch (_) {}
const apply = dark => {
document.documentElement.classList.toggle('dark', dark);
try { localStorage.setItem('themeMode', dark ? 'dark' : 'light'); } catch (_) {}
};
document.addEventListener('basecoat:theme', (event) => {
const mode = event.detail?.mode;
apply(mode === 'dark' ? true
: mode === 'light' ? false
: !document.documentElement.classList.contains('dark'));
});
})();
</script>
{# Theme variant #}
<script>
(function() {
try {
const storedTheme = localStorage.getItem('themeVariant');
if (storedTheme) document.documentElement.classList.add(`theme-${storedTheme}`);
} catch (event) {
console.error('Could not apply theme variant from localStorage', event);
}
})();
</script>
{# Meta #}
<meta charset="utf-8">
<meta http-equiv="Content-Language" content="en">
<meta name="viewport" content="width=device-width, initial-scale=1">
{# Title & Description #}
{% set meta_title = title ~ ' | ' ~ site.title if title else site.title %}
{% set meta_description = description or site.description %}
<title>{{ meta_title }}</title>
<meta name="description" content="{{ meta_description }}"/>
<meta name="title" content="{{ meta_title }}">
{% if site.keywords %}<meta name="keywords" content="{{ site.keywords | join(',') }}" />{% endif %}
{# Favicon #}
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png">
{# Open Graph / Facebook #}
<meta property="og:type" content="website">
<meta property="og:url" content="{{ site.url }}{{ page.url }}">
<meta property="og:title" content="{{ meta_title }}">
<meta property="og:description" content="{{ meta_description }}">
<meta property="og:image" content="{{ site.url }}/assets/social-screenshot.png">
<meta property="og:site_name" content="{{ site.title }}">
<meta property="og:locale" content="en_US">
<meta property="og:author" content="{{ site.author.name }}">
{# X/Twitter #}
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:url" content="{{ site.url }}{{ page.url }}">
<meta name="twitter:title" content="{{ title if title else site.title }}">
<meta name="twitter:description" content="{{ meta_description }}">
<meta name="twitter:image" content="{{ site.url }}/assets/social-screenshot.png">
<meta name="twitter:creator" content="{{ site.author.x }}">
{# CSS #}
<link rel="stylesheet" href="/assets/styles.css">
{# JS #}
<script src="/assets/js/basecoat.js?v={{ pkg.version }}" defer></script>
<script src="/assets/js/command.js?v={{ pkg.version }}" defer></script>
<script src="/assets/js/dropdown-menu.js?v={{ pkg.version }}" defer></script>
<script src="/assets/js/popover.js?v={{ pkg.version }}" defer></script>
<script src="/assets/js/select.js?v={{ pkg.version }}" defer></script>
<script src="/assets/js/sidebar.js?v={{ pkg.version }}" defer></script>
<script src="/assets/js/tabs.js?v={{ pkg.version }}" defer></script>
<script src="/assets/js/toast.js?v={{ pkg.version }}" defer></script>
{# HTMX #}
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
{# Highlight.js #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/django.min.js"></script>
<script>
const highlight = () => {
if (!window.hljs) return;
document
.querySelectorAll('pre code:not([data-highlighted]), code.highlight:not([data-highlighted])')
.forEach(el => hljs.highlightElement(el));
};
if (!window._hljsInit) {
window._hljsInit = true;
document.addEventListener('DOMContentLoaded', highlight);
document.addEventListener('htmx:afterSwap', highlight);
}
</script>
<script>
document.addEventListener('htmx:historyRestore', () => {
if (window.basecoat) window.basecoat.initAll();
});
</script>
{% if head %}{{ head | safe }}{% endif %}
</head>
<body>
{{ content | safe }}
{% from "toast.njk" import toaster %}
{{ toaster() }}
</body>
</html>
-15
View File
@@ -1,15 +0,0 @@
---
layout: layouts/base.njk
---
{% include "partials/sidebar.njk" %}
<main id="content">
<header class="bg-background sticky inset-x-0 top-0 isolate flex shrink-0 items-center gap-2 border-b z-10">
{% include "partials/header.njk" %}
</header>
<div class="p-4 md:p-6 xl:p-12">
{{ content | safe }}
</div>
</main>
-21
View File
@@ -1,21 +0,0 @@
---
layout: layouts/layout.njk
---
<main class="mx-auto relative flex w-full max-w-screen-lg gap-10">
<div class="mx-auto w-full flex-1 max-w-screen-md">
<header class="space-y-2">
<h1 class="text-2xl font-semibold tracking-tight sm:text-3xl xl:text-4xl">{{ title }}</h1>
<p class="text-muted-foreground text-[1.05rem] sm:text-base">{{ description }}</p>
</header>
<article class="pb-12 mt-8 content">
{{ content | safe }}
</article>
</div>
{% if toc %}
<div class="hidden text-sm xl:block w-full max-w-[300px]">
{% from "macros/toc.njk" import toc_nav %}
{{ toc_nav(toc) }}
</div>
{% endif %}
</main>
-23
View File
@@ -1,23 +0,0 @@
{% macro code_block(code, language="html", class="my-4") %}
<div class="relative {{ class }}">
<pre class="grid text-sm max-h-[650px] overflow-y-auto rounded-xl scrollbar"><code class="language-{{ language }} !bg-muted/40 !p-3.5">{{- code | escape -}}</code></pre>
<button
onclick="
const button = this;
const code = this.parentElement.querySelector('pre code');
if (!code) return;
navigator.clipboard.writeText(code.textContent || '').then(() => {
button.classList.add('copied');
setTimeout(() => { button.classList.remove('copied') }, 2000);
}).catch(err => {
console.error('Failed to copy text: ', err);
});
"
class="btn-icon-ghost size-8 absolute right-2.5 top-2 text-muted-foreground hover:text-foreground group"
>
{% lucide "copy", {"class": "group-[.copied]:hidden" } %}
{% lucide "check", {"class": "hidden group-[.copied]:block" } %}
</button>
</div>
{% endmacro %}
@@ -1,26 +0,0 @@
{% macro code_preview(id, code, class=None, default_code=false) %}
{% from "./code_block.njk" import code_block %}
{% set code_panel = code_block(code, class="") %}
{% set preview_panel %}
<div class="ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 relative rounded-md border">
<div class="preview flex min-h-[350px] w-full justify-center p-10 items-center">
<div {% if class %}class="{{ class }}"{% endif %}>
{{ code | safe }}
</div>
</div>
</div>
{% endset %}
<div class="relative my-6">
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "Preview", panel: preview_panel },
{ tab: "Code", panel: code_panel }
] %}
{{ tabs(
tabsets=tabsets,
default_tab_index=(2 if default_code else 1)
) }}
</div>
{% endmacro %}
-20
View File
@@ -1,20 +0,0 @@
{% macro toc_nav(items=[]) %}
<nav class="sticky top-22 space-y-2 [&_ul]:m-0 [&_ul]:list-none [&_ul_ul]:pl-4 [&_li]:mt-0 [&_li]:pt-2 [&_a]:inline-block [&_a]:no-underline [&_a]:transition-colors [&_a]:hover:text-foreground [&_a]:text-muted-foreground">
<h4 class="font-medium">On This Page</h4>
{% macro toc_list(items=[]) %}
{% if items and items.length %}
<ul>
{% for item in items %}
<li>
<a href="#{{ item.id }}">{{ item.label }}</a>
{% if item.children and item.children.length %}
{{ toc_list(item.children) }}
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
{% endmacro %}
{{ toc_list(items) }}
</nav>
{% endmacro %}
-64
View File
@@ -1,64 +0,0 @@
<div class="flex h-14 w-full items-center gap-2 px-4">
<button
type="button"
onclick="document.dispatchEvent(new CustomEvent('basecoat:sidebar'))"
aria-label="Toggle sidebar"
data-tooltip="Toggle sidebar"
data-side="bottom"
data-align="start"
class="btn-sm-icon-ghost mr-auto size-7 -ml-1.5"
>
{% lucide "panel-left" %}
</button>
<select
class="select h-8 leading-none"
id="theme-select"
>
<option value="">Default</option>
<option value="claude">Claude</option>
<option value="doom-64">Doom 64</option>
<option value="supabase">Supabase</option>
</select>
<script>
(() => {
const themeSelect = document.getElementById('theme-select');
const storedTheme = localStorage.getItem('themeVariant');
if (themeSelect && storedTheme) themeSelect.value = storedTheme;
themeSelect.addEventListener('change', () => {
const newTheme = themeSelect.value;
document.documentElement.classList.forEach(c => {
if (c.startsWith('theme-')) document.documentElement.classList.remove(c);
});
if (newTheme) {
document.documentElement.classList.add(`theme-${newTheme}`);
localStorage.setItem('themeVariant', newTheme);
} else {
localStorage.removeItem('themeVariant');
}
});
})();
</script>
<button
type="button"
aria-label="Toggle dark mode"
data-tooltip="Toggle dark mode"
data-side="bottom"
onclick="document.dispatchEvent(new CustomEvent('basecoat:theme'))"
class="btn-icon-outline size-8"
>
<span class="hidden dark:block">{% lucide "sun" %}</span>
<span class="block dark:hidden">{% lucide "moon" %}</span>
</button>
<a
href="https://github.com/hunvreus/basecoat"
class="btn-icon size-8"
target="_blank"
rel="noopener noreferrer"
data-tooltip="GitHub repository"
data-side="bottom"
data-align="end"
>
{% lucide "github" %}
</a>
</div>
@@ -1,89 +0,0 @@
<section id="accordion" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Accordion</h2>
<a href="/components/accordion" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="group grid w-full max-w-xl gap-4">
<section class="accordion">
<details class="group border-b last:border-b-0">
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
Is it accessible?
{% lucide "chevron-down", { "class": "text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200 group-open:rotate-180" } %}
</h2>
</summary>
<section class="pb-4">
<p class="text-sm">Yes. It adheres to the WAI-ARIA design pattern.</p>
</section>
</details>
<details class="group border-b last:border-b-0">
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
Is it styled?
{% lucide "chevron-down", { "class": "text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200 group-open:rotate-180" } %}
</h2>
</summary>
<section class="pb-4">
<p class="text-sm">Yes. It comes with default styles that matches the other components' aesthetic.</p>
</section>
</details>
<details class="group border-b last:border-b-0">
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
Is it animated?
{% lucide "chevron-down", { "class": "text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200 group-open:rotate-180" } %}
</h2>
</summary>
<section>
<p class="text-sm">Yes. It's animated by default, but you can disable it if you prefer.</p>
</section>
</details>
</section>
<section class="accordion">
<details class="group border-b last:border-b-0">
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
What are the key considerations when implementing a comprehensive enterprise-level authentication system?
{% lucide "chevron-down", { "class": "text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200 group-open:rotate-180" } %}
</h2>
</summary>
<section class="pb-4">
<p class="text-sm">Implementing a robust enterprise authentication system requires careful consideration of multiple factors. This includes secure password hashing and storage, multi-factor authentication (MFA) implementation, session management, OAuth2 and SSO integration, regular security audits, rate limiting to prevent brute force attacks, and maintaining detailed audit logs. Additionally, you'll need to consider scalability, performance impact, and compliance with relevant data protection regulations such as GDPR or HIPAA.</p>
</section>
</details>
<details class="group">
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
How does modern distributed system architecture handle eventual consistency and data synchronization across multiple regions?
{% lucide "chevron-down", { "class": "text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200 group-open:rotate-180" } %}
</h2>
</summary>
<section>
<p class="text-sm">Modern distributed systems employ various strategies to maintain data consistency across regions. This often involves using techniques like CRDT (Conflict-Free Replicated Data Types), vector clocks, and gossip protocols. Systems might implement event sourcing patterns, utilize message queues for asynchronous updates, and employ sophisticated conflict resolution strategies. Popular solutions like Amazon's DynamoDB and Google's Spanner demonstrate different approaches to solving these challenges, balancing between consistency, availability, and partition tolerance as described in the CAP theorem.</p>
</section>
</details>
</section>
<script>
(() => {
const accordions = document.querySelectorAll('.accordion');
accordions.forEach(accordion => {
accordion.addEventListener('click', (event) => {
const summary = event.target.closest('summary');
if (!summary) return;
const details = summary.closest('details');
if (!details) return;
accordion.querySelectorAll('details').forEach(detailsEl => {
if (detailsEl !== details) {
detailsEl.removeAttribute('open');
}
});
});
});
})();
</script>
</div>
</div>
</section>
@@ -1,26 +0,0 @@
{% from "dialog.njk" import dialog %}
<section id="alert-dialog" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Alert Dialog</h2>
<a href="/components/alert-dialog" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
{% set footer %}
<button class="btn-outline" onclick="document.getElementById('alert-dialog-demo').close()">Cancel</button>
<button class="btn-primary" onclick="document.getElementById('alert-dialog-demo').close()">Continue</button>
{% endset %}
{{ dialog(
id="alert-dialog-demo",
title="Are you absolutely sure?",
description="This action cannot be undone. This will permanently delete your account and remove your data from our servers.",
trigger="Open dialog",
trigger_attrs={"class": "btn-outline"},
footer=footer,
close_button=false,
close_on_overlay_click=false
) }}
</div>
</section>
@@ -1,72 +0,0 @@
<section id="alert" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Alert</h2>
<a href="/components/alert" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="grid max-w-xl items-start gap-4">
<div class="alert">
{% lucide "circle-check" %}
<h2>Success! Your changes have been saved</h2>
<section>This is an alert with icon, title and description.</section>
</div>
<div class="alert">
{% lucide "bookmark-check" %}
<section>This is an alert with icon, description and no title.</section>
</div>
<div class="alert">
<section>This one has a description only. No title. No icon.</section>
</div>
<div class="alert">
{% lucide "popcorn" %}
<h2>Let's try one with icon and title.</h2>
</div>
<div class="alert">
{% lucide "shield-alert" %}
<h2>This is a very long alert title that demonstrates how the component handles extended text content and potentially wraps across multiple lines</h2>
</div>
<div class="alert">
{% lucide "gift" %}
<section>This is a very long alert description that demonstrates how the component handles extended text content and potentially wraps across multiple lines</section>
</div>
<div class="alert">
{% lucide "circle-alert" %}
<h2>This is an extremely long alert title that spans multiple lines to demonstrate how the component handles very lengthy headings while maintaining readability and proper text wrapping behavior</h2>
<section>This is an equally long description that contains detailed information about the alert. It shows how the component can accommodate extensive content while preserving proper spacing, alignment, and readability across different screen sizes and viewport widths. This helps ensure the user experience remains consistent regardless of the content length.</section>
</div>
<div class="alert-destructive">
{% lucide "circle-alert" %}
<h2>Something went wrong!</h2>
<section>Your session has expired. Please log in again.</section>
</div>
<div class="alert-destructive">
{% lucide "circle-alert" %}
<h2>Something went wrong!</h2>
<section>
<p>Please verify your billing information and try again.</p>
<ul>
<li>Check your card details</li>
<li>Ensure sufficient funds</li>
<li>Verify billing address</li>
</ul>
</section>
</div>
<div class="alert border-amber-50 bg-amber-50 text-amber-900 dark:border-amber-950 dark:bg-amber-950 dark:text-amber-100">
{% lucide "circle-check" %}
<h2>Plot Twist: This Alert is Actually Amber!</h2>
<section>This one has custom colors for light and dark mode.</section>
</div>
</div>
</div>
</section>
@@ -1,37 +0,0 @@
<section id="avatar" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Avatar</h2>
<a href="/components/avatar" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-row flex-wrap items-center gap-4">
<img class="size-8 shrink-0 object-cover rounded-full" alt="@hunvreus" src="https://github.com/hunvreus.png">
<span class="size-8 shrink-0 bg-muted flex items-center justify-center rounded-full">CN</span>
<img class="size-12 shrink-0 object-cover rounded-full" alt="@hunvreus" src="https://github.com/hunvreus.png">
<img class="size-8 shrink-0 object-cover rounded-lg" alt="@hunvreus" src="https://github.com/hunvreus.png">
<div class="flex -space-x-2 [&_img]:ring-background [&_img]:ring-2 [&_img]:grayscale [&_img]:size-8 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
<div class="flex -space-x-2 [&_img]:ring-background [&_img]:ring-2 [&_img]:grayscale [&_img]:size-12 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
<div class="flex -space-x-2 hover:space-x-1 [&_img]:ring-background [&_img]:size-12 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full [&_img]:ring-2 [&_img]:grayscale [&_img]:transition-all [&_img]:ease-in-out [&_img]:duration-300">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
</div>
</div>
</section>
@@ -1,47 +0,0 @@
<section id="badge" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Badge</h2>
<a href="/components/badge" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col gap-2">
<div class="flex flex-wrap items-center gap-2 md:flex-row">
<span class="badge">Primary</span>
<span class="badge-secondary">Secondary</span>
<span class="badge-outline">Outline</span>
<span class="badge-destructive">Destructive</span>
<span class="badge-outline">
{% lucide "check" %}
Badge
</span>
<span class="badge-destructive">
{% lucide "circle-alert" %}
Alert
</span>
<span class="badge rounded-full min-w-5 px-1">8</span>
<span class="badge-destructive rounded-full min-w-5 px-1">99</span>
<span class="badge-outline rounded-full min-w-5 px-1 font-mono tabular-nums">20+</span>
</div>
<div class="flex flex-wrap items-center gap-2 md:flex-row">
<a href="#" class="badge">
Link
{% lucide "arrow-right" %}
</a>
<a href="#" class="badge-secondary">
Link
{% lucide "arrow-right" %}
</a>
<a href="#" class="badge-destructive">
Link
{% lucide "arrow-right" %}
</a>
<a href="#" class="badge-outline">
Link
{% lucide "arrow-right" %}
</a>
</div>
</div>
</div>
</section>
@@ -1,42 +0,0 @@
{% from "dropdown-menu.njk" import dropdown_menu %}
<section id="breadcrumb" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Breadcrumb</h2>
<a href="/components/breadcrumb" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<ol class="text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5">
<li class="inline-flex items-center gap-1.5">
<a href="#" class="hover:text-foreground transition-colors">Home</a>
</li>
<li>{% lucide "chevron-right", { "class": "size-3.5" } %}</li>
<li class="inline-flex items-center gap-1.5">
{% set trigger %}
{% lucide "ellipsis" %}
{% endset %}
{{ dropdown_menu(
id="demo-breadcrumb-menu",
trigger=trigger,
trigger_attrs={"class": "flex size-9 items-center justify-center h-4 w-4 hover:text-foreground cursor-pointer"},
popover_attrs={"class": "p-1"},
items=[
{ label: "Documentation" },
{ label: "Themes" },
{ label: "GitHub" }
]
) }}
</li>
<li>{% lucide "chevron-right", { "class": "size-3.5" } %}</li>
<li class="inline-flex items-center gap-1.5">
<a href="#" class="hover:text-foreground transition-colors">Components</a>
</li>
<li>{% lucide "chevron-right", { "class": "size-3.5" } %}</li>
<li class="inline-flex items-center gap-1.5">
<span class="text-foreground font-normal">Breadcrumb</span>
</li>
</ol>
</div>
</section>
@@ -1,101 +0,0 @@
<section id="button" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Button</h2>
<a href="/components/button" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col gap-6">
<div class="flex flex-wrap items-center gap-2 md:flex-row">
<button class="btn-primary">Primary</button>
<button class="btn-outline">Outline</button>
<button class="btn-ghost">Ghost</button>
<button class="btn-destructive">
{% lucide "send" %}
Danger
</button>
<button class="btn-secondary">Secondary</button>
<button class="btn-link">Link</button>
<button class="btn-outline">
{% lucide "send" %}
Send
</button>
<button class="btn-outline">
Learn more
{% lucide "arrow-right" %}
</button>
<button class="btn-outline" disabled>
{% lucide "loader", { "class": "animate-spin" } %}
Loading
</button>
</div>
<div class="flex flex-wrap items-center gap-2 md:flex-row">
<button class="btn-sm-primary">Primary</button>
<button class="btn-sm-outline">Outline</button>
<button class="btn-sm-ghost">Ghost</button>
<button class="btn-sm-destructive">Danger</button>
<button class="btn-sm-secondary">Secondary</button>
<button class="btn-sm-link">Link</button>
<button class="btn-sm-outline">
{% lucide "send" %}
Send
</button>
<button class="btn-sm-outline">
Learn more
{% lucide "arrow-right" %}
</button>
<button class="btn-sm-outline" disabled>
{% lucide "loader", { "class": "animate-spin" } %}
Loading
</button>
</div>
<div class="flex flex-wrap items-center gap-2 md:flex-row">
<button class="btn-lg-primary">Primary</button>
<button class="btn-lg-outline">Outline</button>
<button class="btn-lg-ghost">Ghost</button>
<button class="btn-lg-destructive">
{% lucide "send" %}
Danger
</button>
<button class="btn-lg-secondary">Secondary</button>
<button class="btn-lg-link">Link</button>
<button class="btn-lg-outline">
{% lucide "send" %}
Send
</button>
<button class="btn-lg-outline">
Learn more
{% lucide "arrow-right" %}
</button>
<button class="btn-lg-outline" disabled>
{% lucide "loader", { "class": "animate-spin" } %}
Loading
</button>
</div>
<div class="flex flex-wrap items-center gap-2 md:flex-row">
<button class="btn-icon-primary">
{% lucide "download" %}
</button>
<button class="btn-icon-secondary">
{% lucide "upload" %}
</button>
<button class="btn-icon-outline">
{% lucide "arrow-right" %}
</button>
<button class="btn-icon-ghost">
{% lucide "ellipsis" %}
</button>
<button class="btn-icon-destructive">
{% lucide "trash-2" %}
</button>
<button class="btn-icon-outline" disabled>
{% lucide "loader", { "class": "animate-spin" } %}
</button>
</div>
</div>
</div>
</section>
@@ -1,147 +0,0 @@
<section id="card" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Card</h2>
<a href="/components/card" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col items-start gap-4">
<div class="card w-full max-w-sm">
<header>
<h2>Login to your account</h2>
<p>Enter your details below to login to your account</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-2">
<label for="demo-card-form-email">Email</label>
<input type="email" id="demo-card-form-email">
</div>
<div class="grid gap-2">
<div class="flex items-center gap-2">
<label for="demo-card-form-password">Password</label>
<a href="#" class="ml-auto inline-block text-sm underline-offset-4 hover:underline">Forgot your password?</a>
</div>
<input type="password" id="demo-card-form-password">
</div>
</form>
</section>
<footer class="flex flex-col items-center gap-2">
<button type="button" class="btn w-full">Login</button>
<button type="button" class="btn-outline w-full">Login with Google</button>
<p class="mt-4 text-center text-sm">Don't have an account? <a href="#" class="underline-offset-4 hover:underline">Sign up</a></p>
</footer>
</div>
<div class="card">
<header>
<h2>Meeting Notes</h2>
<p>Transcript from the meeting with the client.</p>
</header>
<section class="text-sm">
<p>Client requested dashboard redesign with focus on mobile responsiveness.</p>
<ol class="mt-4 flex list-decimal flex-col gap-2 pl-6">
<li>New analytics widgets for daily/weekly metrics</li>
<li>Simplified navigation menu</li>
<li>Dark mode support</li>
<li>Timeline: 6 weeks</li>
<li>Follow-up meeting scheduled for next Tuesday</li>
</ol>
</section>
<footer class="flex items-center">
<div class="flex -space-x-2 [&_img]:ring-background [&_img]:ring-2 [&_img]:grayscale [&_img]:size-8 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
</footer>
</div>
<div class="card">
<header>
<h2>Is this an image?</h2>
<p>This is a card with an image.</p>
</header>
<section class="px-0">
<img
alt="Photo by Drew Beamer"
loading="lazy"
width="500"
height="500"
class="aspect-video object-cover" style="color:transparent"
srcset="
https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80&w=640&q=75 1x,
https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80&w=1080&q=75 2x
"
src="https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80&w=1080&q=75"
/>
</section>
<footer class="flex items-center gap-2">
<span class="badge-outline">
{% lucide "bed" %}
1
</span>
<span class="badge-outline">
{% lucide "bath" %}
2
</span>
<span class="badge-outline">
{% lucide "land-plot" %}
350m²
</span>
<span class="ml-auto font-medium tabular-nums">
$135,000
</span>
</footer>
</div>
<div class="flex w-full flex-wrap items-start gap-8 md:*:[.card]:basis-1/4">
<div class="card">
<section>Content Only</section>
</div>
<div class="card">
<header>
<h2>Header Only</h2>
<p>This is a card with a header and a description.</p>
</header>
</div>
<div class="card">
<header>
<h2>Header and Content</h2>
<p>This is a card with a header and a content.</p>
</header>
<section>Content only.</section>
</div>
<div class="card">
<footer>Footer Only</footer>
</div>
<div class="card">
<header>
<h2>Header + Footer</h2>
<p>This is a card with a header and a footer.</p>
</header>
<footer>Footer</footer>
</div>
<div class="card">
<section>Content</section>
<footer>Footer</footer>
</div>
<div class="card">
<header>
<h2>Header + Content + Footer</h2>
<p>This is a card with a header, content and footer.</p>
</header>
<section>Content</section>
<footer>Footer</footer>
</div>
</div>
</div>
</div>
</section>
@@ -1,34 +0,0 @@
<section id="checkbox" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Checkbox</h2>
<a href="/components/checkbox" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col gap-6 max-w-lg">
<label class="label gap-3">
<input type="checkbox" class="input">
Accept terms and conditions
</label>
<div class="flex items-start gap-3">
<input type="checkbox" id="demo-checkbox-label-and-description" class="input">
<div class="grid gap-2">
<label for="demo-checkbox-label-and-description" class="label">Accept terms and conditions</label>
<p class="text-muted-foreground text-sm">By clicking this checkbox, you agree to the terms and conditions.</p>
</div>
</div>
<label class="label gap-3">
<input type="checkbox" class="input" disabled>
Enable notifications
</label>
<label class="flex items-start gap-3 border p-3 hover:bg-accent/50 rounded-lg has-[input[type='checkbox']:checked]:border-blue-600 has-[input[type='checkbox']:checked]:bg-blue-50 dark:has-[input[type='checkbox']:checked]:border-blue-900 dark:has-[input[type='checkbox']:checked]:bg-blue-950">
<input type="checkbox" class="input checked:bg-blue-600 checked:border-blue-600 dark:checked:bg-blue-700 dark:checked:border-blue-700 checked:after:bg-white" checked>
<div class="grid gap-2">
<h2 class="text-sm leading-none font-medium">Enable notifications</h2>
<p class="text-muted-foreground text-sm">You can enable or disable notifications at any time.</p>
</div>
</label>
</div>
</div>
</section>
@@ -1,83 +0,0 @@
{% from "select.njk" import select %}
<section id="combobox" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Combobox</h2>
<a href="/components/combobox" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-wrap items-start gap-4">
{% set options_frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"] %}
{% call select(
popover_attrs={"class": "w-48"},
listbox_attrs={"data-empty": "No framework found."},
is_combobox=true
) %}
{% for framework in options_frameworks %}
<div role="option" data-value="{{ framework }}">{{ framework }}</div>
{% endfor %}
{% endcall %}
{% set options_timezones = [
{
label: "Americas",
timezones: [
{ value: "America/New_York", label: "(GMT-5) New York" },
{ value: "America/Los_Angeles", label: "(GMT-8) Los Angeles" },
{ value: "America/Chicago", label: "(GMT-6) Chicago" },
{ value: "America/Toronto", label: "(GMT-5) Toronto" },
{ value: "America/Vancouver", label: "(GMT-8) Vancouver" },
{ value: "America/Sao_Paulo", label: "(GMT-3) São Paulo" }
]
},
{
label: "Europe",
timezones: [
{ value: "Europe/London", label: "(GMT+0) London" },
{ value: "Europe/Paris", label: "(GMT+1) Paris" },
{ value: "Europe/Berlin", label: "(GMT+1) Berlin" },
{ value: "Europe/Rome", label: "(GMT+1) Rome" },
{ value: "Europe/Madrid", label: "(GMT+1) Madrid" },
{ value: "Europe/Amsterdam", label: "(GMT+1) Amsterdam" }
]
},
{
label: "Asia/Pacific",
timezones: [
{ value: "Asia/Tokyo", label: "(GMT+9) Tokyo" },
{ value: "Asia/Shanghai", label: "(GMT+8) Shanghai" },
{ value: "Asia/Singapore", label: "(GMT+8) Singapore" },
{ value: "Asia/Dubai", label: "(GMT+4) Dubai" },
{ value: "Australia/Sydney", label: "(GMT+11) Sydney" },
{ value: "Asia/Seoul", label: "(GMT+9) Seoul" }
]
}
] %}
{% call select(
popover_attrs={"class": "w-72"},
listbox_attrs={"data-empty": "No timezone found."},
is_combobox=true
) %}
<div class="max-h-64 overflow-y-auto scrollbar">
{% for group in options_timezones %}
<div role="group" aria-labelledby="demo-combobox-timezones-group-{{ loop.index0 }}">
<span id="demo-combobox-timezones-group-{{ loop.index0 }}" role="heading">{{ group.label }}</span>
{% for timezone in group.timezones %}
<div role="option" data-value="{{ timezone.value}}">
{{ timezone.label }}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<hr role="separator">
<div role="option">
{% lucide "circle-plus" %}
Create timezone
</div>
{% endcall %}
</div>
</div>
</section>
@@ -1,65 +0,0 @@
{% from "dialog.njk" import dialog %}
<section id="dialog" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Dialog</h2>
<a href="/components/dialog" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-wrap items-center gap-4">
{% set footer %}
<button class="btn-outline" onclick="document.getElementById('demo-dialog-edit-profile').close()">Cancel</button>
<button class="btn" onclick="document.getElementById('demo-dialog-edit-profile').close()">Save changes</button>
{% endset %}
{% call dialog(
id="demo-dialog-edit-profile",
title="Edit profile",
description="Make changes to your profile here. Click save when you're done.",
trigger="Edit Profile",
trigger_attrs={"class": "btn-outline"},
dialog_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
footer=footer
) %}
<form class="form grid gap-4">
<div class="grid gap-3">
<label for="demo-dialog-edit-profile-name">Name</label>
<input type="text" value="Pedro Duarte" id="demo-dialog-edit-profile-name" />
</div>
<div class="grid gap-3">
<label for="demo-dialog-edit-profile-username">Username</label>
<input type="text" value="@peduarte" id="demo-dialog-edit-profile-username" />
</div>
</form>
{% endcall %}
{% set footer %}
<button class="btn-outline" onclick="this.closest('dialog').close()">Close</button>
{% endset %}
{% call dialog(
id="dialog-example",
title="Scrollable Content",
description="This is a dialog with scrollable content.",
trigger="Scrollable Content",
trigger_attrs={"class": "btn-outline"},
dialog_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
body_attrs={"class": "overflow-y-auto scrollbar"},
footer=footer
) %}
<div class="space-y-4 text-sm">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>
{% endcall %}
</div>
</div>
</section>
@@ -1,294 +0,0 @@
{% from "dropdown-menu.njk" import dropdown_menu %}
<section id="dropdown-menu" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Dropdown Menu</h2>
<a href="/components/dropdown-menu" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-wrap items-start gap-4">
{% call dropdown_menu(
id="dropdown-menu-default",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="account-options">
<div role="heading" id="account-options">My Account</div>
<div role="menuitem">
Profile
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
<div role="menuitem">
Billing
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</div>
<div role="menuitem">
Settings
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</div>
<div role="menuitem">
Keyboard shortcuts
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘K</span>
</div>
</div>
<hr role="separator">
<div role="menuitem">GitHub</div>
<div role="menuitem">Support</div>
<div role="menuitem" aria-disabled="true">API</div>
<hr role="separator">
<div role="menuitem">
Logout
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
{% endcall %}
{% call dropdown_menu(
id="dropdown-menu-checkboxes",
trigger="Checkboxes",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="account-options">
<div role="heading" id="account-options">Account Options</div>
<div role="menuitem">
{% lucide "user" %}
Profile
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
<div role="menuitem">
{% lucide "credit-card" %}
Billing
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</div>
<div role="menuitem">
{% lucide "settings" %}
Settings
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</div>
</div>
<hr role="separator">
<div role="group" aria-labelledby="appearance-options">
<div role="heading" id="appearance-options">Appearance</div>
<div
role="menuitemcheckbox"
aria-checked="true"
class="group"
>
{% lucide "check", {
"class": "invisible group-aria-checked:visible",
"aria-hidden": "true",
"focusable": "false"
} %}
Status Bar
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
<div
role="menuitemcheckbox"
aria-checked="false"
class="group"
aria-disabled="true"
>
{% lucide "check", {
"class": "invisible group-aria-checked:visible",
"aria-hidden": "true",
"focusable": "false"
} %}
Activity Bar
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</div>
<div
role="menuitemcheckbox"
aria-checked="false"
class="group"
>
{% lucide "check", {
"class": "invisible group-aria-checked:visible",
"aria-hidden": "true",
"focusable": "false"
} %}
Panel
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</div>
</div>
<hr role="separator">
<div role="menuitem">
{% lucide "log-out" %}
Logout
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
{% endcall %}
<script>
(() => {
const dropdownMenu = document.querySelector('#dropdown-menu-checkboxes');
const checkboxes = dropdownMenu.querySelectorAll('div[role="menuitemcheckbox"]');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('click', () => {
const isChecked = checkbox.getAttribute('aria-checked') === 'true';
checkbox.setAttribute('aria-checked', !isChecked);
});
});
})();
</script>
{% set profile_menu %}
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<img alt="@hunvreus" src="https://github.com/hunvreus.png" class="size-8 shrink-0 rounded-full" alt="Ronan Berder"/>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">hunvreus</span>
<span class="text-muted-foreground truncate text-xs">hunvreus@example.com</span>
</div>
</div>
<hr role="separator">
<div role="menuitem">
{% lucide "star" %}
Upgrade to Pro
</div>
<hr role="separator">
<div role="menuitem">
{% lucide "user" %}
Account
</div>
<div role="menuitem">
{% lucide "credit-card" %}
Billing
</div>
<div role="menuitem">
{% lucide "bell" %}
Notifications
</div>
<hr role="separator">
<div role="menuitem">
{% lucide "log-out" %}
Signout
</div>
{% endset %}
{% call dropdown_menu(
id="dropdown-menu-radio-group",
trigger="Radio Group",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="position-options">
<span id="position-options" role="heading">Panel Position</span>
<hr role="separator">
<div
role="menuitemradio"
aria-checked="false"
class="group"
>
<div class="size-4 flex items-center justify-center">
<div
class="size-2 rounded-full bg-foreground invisible group-aria-checked:visible"
aria-hidden="true",
focusable="false"
></div>
</div>
Status Bar
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
<div
role="menuitemradio"
aria-checked="true"
class="group"
>
<div class="size-4 flex items-center justify-center">
<div
class="size-2 rounded-full bg-foreground invisible group-aria-checked:visible"
aria-hidden="true",
focusable="false"
></div>
</div>
Activity Bar
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</div>
<div
role="menuitemradio"
aria-checked="false"
class="group"
>
<div class="size-4 flex items-center justify-center">
<div
class="size-2 rounded-full bg-foreground invisible group-aria-checked:visible"
aria-hidden="true",
focusable="false"
></div>
</div>
Panel
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</div>
</div>
{% endcall %}
<script>
(() => {
const dropdownMenu = document.querySelector('#dropdown-menu-radio-group');
const radioButtons = dropdownMenu.querySelectorAll('div[role="menuitemradio"]');
radioButtons.forEach(radioButton => {
radioButton.addEventListener('click', () => {
radioButtons.forEach(radioButton => {
radioButton.setAttribute('aria-checked', 'false');
});
radioButton.setAttribute('aria-checked', 'true');
});
});
})();
</script>
{% set trigger %}
<img alt="@hunvreus" src="https://github.com/hunvreus.png" class="size-8 shrink-0 rounded-full" alt="Ronan Berder"/>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">hunvreus</span>
<span class="text-muted-foreground truncate text-xs">hunvreus@example.com</span>
</div>
{% lucide "chevrons-up-down", { "class": "text-muted-foreground ml-auto" } %}
{% endset %}
{% call dropdown_menu(
id="dropdown-checkboxes",
trigger=trigger,
trigger_attrs={"class": "btn-outline h-12 justify-start px-2 md:max-w-[200px]"}
) %}
{{ profile_menu | safe }}
{% endcall %}
{% set trigger %}
<img alt="@hunvreus" src="https://github.com/hunvreus.png" class="size-8 shrink-0 rounded-full" alt="Ronan Berder"/>
{% endset %}
{% call dropdown_menu(
id="dropdown-checkboxes",
trigger=trigger,
trigger_attrs={"class": "btn-icon-ghost rounded-full size-8"},
popover_attrs={"data-align": "end"}
) %}
{{ profile_menu | safe }}
{% endcall %}
{% set trigger %}
{% lucide "ellipsis" %}
{% endset %}
{% call dropdown_menu(
id="dropdown-checkboxes",
trigger=trigger,
trigger_attrs={"class": "btn-icon-ghost"},
popover_attrs={"class": "min-w-32"}
) %}
<div role="menuitem">
{% lucide "pencil" %}
Edit
</div>
<div role="menuitem">
{% lucide "share" %}
Share
</div>
<hr role="separator">
<div role="menuitem" class="text-destructive hover:bg-destructive/10 dark:hover:bg-destructive/20 focus:bg-destructive/10 dark:focus:bg-destructive/20 focus:text-destructive [&_svg]:!text-destructive">
{% lucide "trash-2" %}
Delete
</div>
{% endcall %}
</div>
</div>
</section>
@@ -1,106 +0,0 @@
<section id="form" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
<h2 class="text-sm font-medium">Form</h2>
</header>
<div class="p-4">
<form class="form grid w-full max-w-sm gap-6">
<div class="grid gap-2">
<label for="demo-form-text">Username</label>
<input type="text" id="demo-form-text" placeholder="hunvreus">
<p class="text-muted-foreground text-sm">This is your public display name.</p>
</div>
<div class="grid gap-2">
<label for="demo-form-select">Email</label>
<select id="demo-form-select">
<option value="bob@example.com">m@example.com</option>
<option value="alice@example.com">m@google.com</option>
<option value="john@example.com">m@support.com</option>
</select>
<p class="text-muted-foreground text-sm">You can manage email addresses in your email settings.</p>
</div>
<div class="grid gap-2">
<label for="demo-form-text">Bio</label>
<textarea id="demo-form-textarea" placeholder="I like to..." rows="3"></textarea>
<p class="text-muted-foreground text-sm">You can @mention other users and organizations.</p>
</div>
<div class="flex flex-col gap-3">
<label for="demo-form-radio">Notify me about...</label>
<fieldset id="demo-form-radio" class="grid gap-3">
<label class="font-normal"><input type="radio" name="demo-form-radio" value="1">All new messages</label>
<label class="font-normal"><input type="radio" name="demo-form-radio" value="2">Direct messages and mentions</label>
<label class="font-normal"><input type="radio" name="demo-form-radio" value="3" >Nothing</label>
</fieldset>
</div>
<div class="flex flex-row items-start gap-3 rounded-md border p-4 shadow-xs">
<input type="checkbox" id="demo-form-checkbox">
<div class="flex flex-col gap-1">
<label for="demo-form-checkbox" class="leading-snug">Use different settings for my mobile devices</label>
<p class="text-muted-foreground text-sm leading-snug">You can manage your mobile notifications in the mobile settings page.</p>
</div>
</div>
<div class="flex flex-col gap-4">
<header>
<label for="demo-form-checkboxes" class="text-base leading-normal">Sidebar</label>
<p class="text-muted-foreground text-sm">Select the items you want to display in the sidebar.</p>
</header>
<fieldset id="demo-form-checkboxes" class="flex flex-col gap-2">
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="1" checked>
Recents
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="2" checked>
Home
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="3">
Applications
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="4">
Desktop
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="5">
Download
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="6">
Documents
</label>
</fieldset>
</div>
<div class="grid gap-2">
<label for="demo-form-date">Date of birth</label>
<input type="date" id="demo-form-date">
<p class="text-muted-foreground text-sm">Your date of birth is used to calculate your age.</p>
</div>
<section class="grid gap-4">
<h3 class="text-lg font-medium">Email Notifications</h3>
<div class="gap-2 flex flex-row items-start justify-between rounded-lg border p-4 shadow-xs">
<div class="flex flex-col gap-0.5">
<label for="demo-form-switch" class="leading-normal">Marketing emails</label>
<p class="text-muted-foreground text-sm">Receive emails about new products, features, and more.</p>
</div>
<input type="checkbox" id="demo-form-switch" role="switch">
</div>
<div class="gap-2 flex flex-row items-start justify-between rounded-lg border p-4 shadow-xs">
<div class="flex flex-col gap-0.5 opacity-60">
<label for="demo-form-switch-disabled" class="leading-normal">Marketing emails</label>
<p class="text-muted-foreground text-sm">Receive emails about new products, features, and more.</p>
</div>
<input type="checkbox" id="demo-form-switch-disabled" role="switch" disabled>
</div>
</section>
<button type="submit" class="btn">Submit</button>
</form>
</div>
</section>
@@ -1,27 +0,0 @@
<section id="input" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Input</h2>
<a href="/components/input" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col gap-y-4">
<input type="text" class="input" placeholder="Text">
<input type="text" class="input" disabled placeholder="Disabled">
<input type="text" class="input" aria-invalid="true" placeholder="Error">
<input type="email" class="input" placeholder="Email">
<input type="password" class="input" placeholder="Password">
<input type="number" class="input" placeholder="Number">
<input type="file" class="input">
<input type="tel" class="input" placeholder="Tel">
<input type="url" class="input" placeholder="URL">
<input type="search" class="input" placeholder="Search">
<input type="date" class="input">
<input type="datetime-local" class="input">
<input type="month" class="input">
<input type="week" class="input">
<input type="time" class="input">
</div>
</div>
</section>
@@ -1,30 +0,0 @@
<section id="label" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Label</h2>
<a href="/components/label" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col gap-y-4">
<div class="grid w-full max-w-sm gap-6">
<div class="flex items-center gap-3">
<input type="checkbox" id="label-demo-terms" class="input">
<label for="label-demo-terms" class="label">Accept terms and conditions</label>
</div>
<div class="grid gap-3">
<label for="label-demo-text" class="label">Username</label>
<input type="text" id="label-demo-text" class="input" placeholder="Username">
</div>
<div class="grid gap-3">
<label for="label-demo-disabled" class="label">Disabled</label>
<input type="text" id="label-demo-disabled" class="peer input" placeholder="Disabled" disabled>
</div>
<div class="grid gap-3">
<label for="label-demo-textarea" class="label">Message</label>
<textarea id="label-demo-textarea" class="textarea" placeholder="Message"></textarea>
</div>
</div>
</div>
</div>
</section>
@@ -1,34 +0,0 @@
<section id="pagination" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Pagination</h2>
<a href="/components/pagination" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="inline-flex">
<nav role="navigation" aria-label="pagination" class="mx-auto flex w-full justify-center">
<ul class="flex flex-row items-center gap-1">
<li>
<a href="#" class="btn-ghost">{% lucide "chevron-left" %} Previous</a>
</li>
<li>
<a href="#" class="btn-ghost size-9">1</a>
</li>
<li>
<a href="#" class="btn-outline size-9">2</a>
</li>
<li>
<a href="#" class="btn-ghost size-9">3</a>
</li>
<li>
<a href="#" class="btn-icon-ghost">{% lucide "ellipsis" %}</a>
</li>
<li>
<a href="#" class="btn-ghost">Next {% lucide "chevron-right" %}</a>
</li>
</ul>
</nav>
</div>
</div>
</section>
@@ -1,43 +0,0 @@
{% from "popover.njk" import popover %}
<section id="popover" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Popover</h2>
<a href="/components/popover" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
{% call popover(
id="demo-popover",
trigger="Open popover",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "w-80"}
) %}
<div class="grid gap-4">
<header class="grid gap-1.5">
<h4 class="leading-none font-medium">Dimensions</h4>
<p class="text-muted-foreground text-sm">Set the dimensions for the layer.</p>
</header>
<form class="form grid gap-2">
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-width">Width</label>
<input type="text" id="demo-popover-width" value="100%" class="col-span-2 h-8"/>
</div>
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-max-width">Max. width</label>
<input type="text" id="demo-popover-max-width" value="300px" class="col-span-2 h-8"/>
</div>
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-height">Height</label>
<input type="text" id="demo-popover-height" value="25px" class="col-span-2 h-8"/>
</div>
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-max-height">Max. height</label>
<input type="text" id="demo-popover-max-height" value="none" class="col-span-2 h-8"/>
</div>
</form>
</div>
{% endcall %}
</div>
</section>
@@ -1,33 +0,0 @@
<section id="radio-group" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Radio Group</h2>
<a href="/components/radio-group" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col gap-y-6">
<fieldset class="grid gap-3">
<label class="label"><input type="radio" name="demo-radio-group" value="default" class="input">Default</label>
<label class="label"><input type="radio" name="demo-radio-group" value="comfortable" class="input">Comfortable</label>
<label class="label"><input type="radio" name="demo-radio-group" value="compact" class="input">Compact</label>
</fieldset>
<fieldset class="grid gap-3 max-w-sm">
<label class="label gap-3 items-start hover:bg-accent/50 rounded-lg border p-4 has-[input[type='radio']:checked]:border-green-600 has-[input[type='radio']:checked]:bg-green-50 dark:has-[input[type='radio']:checked]:border-green-900 dark:has-[input[type='radio']:checked]:bg-green-950">
<input type="radio" name="demo-radio-group-styled" value="no" class="input checked:bg-green-600 checked:border-green-600 dark:checked:bg-input/30 checked:before:bg-background dark:checked:before:bg-primary">
<div class="grid gap-1 font-normal">
<h2 class="font-medium">Starter Plan</h2>
<p class="text-muted-foreground leading-snug">Perfect for small businesses getting started with our platform</p>
</div>
</label>
<label class="label gap-3 items-start hover:bg-accent/50 rounded-lg border p-4 has-[input[type='radio']:checked]:border-green-600 has-[input[type='radio']:checked]:bg-green-50 dark:has-[input[type='radio']:checked]:border-green-900 dark:has-[input[type='radio']:checked]:bg-green-950">
<input type="radio" name="demo-radio-group-styled" value="no" class="input checked:bg-green-600 checked:border-green-600 dark:checked:bg-input/30 checked:before:bg-background dark:checked:before:bg-primary">
<div class="grid gap-1 font-normal">
<h2 class="font-medium">Pro Plan</h2>
<p class="text-muted-foreground leading-snug">Advanced features for growing businesses with higher demands</p>
</div>
</label>
</fieldset>
</div>
</div>
</section>
@@ -1,99 +0,0 @@
{% from "select.njk" import select %}
<section id="select" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Select</h2>
<a href="/components/select" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4 gap">
<div class="flex flex-col gap-4">
<div class="flex flex-wrap items-center gap-2 md:flex-row">
<select class="select w-[180px]">
<optgroup label="Fruits">
<option>Apple</option>
<option>Banana</option>
<option>Blueberry</option>
</optgroup>
<optgroup label="Grapes">
<option>Pineapple</option>
</optgroup>
</select>
<select class="select w-[180px]" disabled>
<option>Disabled</option>
</select>
</div>
<div class="flex flex-wrap items-center gap-4">
<div class="flex flex-wrap items-center gap-2 md:flex-row">
{{ select(
id="select-default",
trigger_attrs={"class": "w-[180px]"},
selected="blueberry",
items=[
{
type: "group",
label: "Fruits",
items: [
{ type: "item", label: "Apple", value: "apple" },
{ type: "item", label: "Banana", value: "banana" },
{ type: "item", label: "Blueberry", value: "blueberry" }
]
},
{
type: "group",
label: "Grapes",
items: [
{ type: "item", label: "Pineapple", value: "pineapple" }
]
}
]
) }}
{% call select(
id="select-scrollbar",
trigger_attrs={"class": "w-[180px]"},
listbox_attrs={"class": "scrollbar overflow-y-auto max-h-64"}
) %}
{% for i in range(0, 99) %}
<div role="option" data-value="item-{{ i }}">Item {{ i }}</div>
{% endfor %}
{% endcall %}
{% call select(
id="select-disabled",
trigger_attrs={"disabled": "disabled", "class": "w-[180px]"}
) %}
<div role="option" data-value="disabled">Disabled</div>
{% endcall %}
{% call select(
id="select-with-icon",
name="chart-type",
trigger_attrs={'class': 'w-[180px]'}
) %}
<div type="button" role="option" data-value="bar">
<span class="flex items-center gap-2">
{% lucide "chart-bar", {"class": "text-muted-foreground"} %}
Bar
</span>
</div>
<div type="button" role="option" data-value="line">
<span class="flex items-center gap-2">
{% lucide "chart-line", {"class": "text-muted-foreground"} %}
Line
</span>
</div>
<div type="button" role="option" data-value="pie">
<span class="flex items-center gap-2">
{% lucide "chart-pie", {"class": "text-muted-foreground"} %}
Pie
</span>
</div>
{% endcall %}
</div>
</div>
</div>
</div>
</section>
@@ -1,40 +0,0 @@
<section id="skeleton" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Skeleton</h2>
<a href="/components/skeleton" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col gap-4">
<div class="flex items-center gap-4">
<div class="bg-accent animate-pulse size-10 shrink-0 rounded-full"></div>
<div class="grid gap-2">
<div class="bg-accent animate-pulse rounded-md h-4 w-[150px]"></div>
<div class="bg-accent animate-pulse rounded-md h-4 w-[100px]"></div>
</div>
</div>
<div class="flex max-sm:flex-col gap-4 w-full">
<div class="card w-full @md:w-auto @md:min-w-sm">
<header>
<div class="bg-accent animate-pulse rounded-md h-4 w-2/3"></div>
<div class="bg-accent animate-pulse rounded-md h-4 w-1/2"></div>
</header>
<section>
<div class="bg-accent animate-pulse rounded-md aspect-square w-full"></div>
</section>
</div>
<div class="card w-full @md:w-auto @md:min-w-sm">
<header>
<div class="bg-accent animate-pulse rounded-md h-4 w-2/3"></div>
<div class="bg-accent animate-pulse rounded-md h-4 w-1/2"></div>
</header>
<section>
<div class="bg-accent animate-pulse rounded-md aspect-square w-full"></div>
</section>
</div>
</div>
</div>
</section>
@@ -1,38 +0,0 @@
<section id="slider" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Slider</h2>
<a href="/components/slider" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="max-w-sm">
<input
type="range"
class="input w-full"
min="0"
max="27"
value="12"
>
</div>
</div>
</section>
<script>
(() => {
const sliders = document.querySelectorAll('input[type="range"].input');
if (!sliders) return;
const updateSlider = (el) => {
const min = parseFloat(el.min || 0);
const max = parseFloat(el.max || 100);
const value = parseFloat(el.value);
const percent = (max === min) ? 0 : ((value - min) / (max - min)) * 100;
el.style.setProperty('--slider-value', `${percent}%`);
};
sliders.forEach(slider => {
updateSlider(slider);
slider.addEventListener('input', (event) => updateSlider(event.target));
});
})();
</script>
@@ -1,27 +0,0 @@
<section id="switch" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Switch</h2>
<a href="/components/switch" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="inline-flex flex-col gap-y-6">
<label class="label">
<input type="checkbox" name="switch" role="switch" class="input">
Airplane Mode
</label>
<label class="label">
<input type="checkbox" name="switch" role="switch" class="input checked:bg-blue-500 dark:checked:bg-blue-600" checked>
Bluetooth
</label>
<label class="label gap-6 leading-none border rounded-lg p-4 has-[input[type='checkbox']:checked]:border-blue-600">
<div class="grid gap-1">
<h2 class="font-medium text-sm">Share across devices</h2>
<p class="text-muted-foreground text-sm">Focus is shared across devices, and turns off when you leave the app.</p>
</div>
<input type="checkbox" name="switch" role="switch" class="input checked:bg-blue-500 dark:checked:bg-blue-600">
</label>
</div>
</div>
</section>
@@ -1,74 +0,0 @@
<section id="table" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Table</h2>
<a href="/components/table" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="relative w-full overflow-x-auto">
<table class="table">
<caption>A list of your recent invoices.</caption>
<thead>
<tr>
<th>Invoice</th>
<th>Status</th>
<th>Method</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td class="font-medium">INV001</td>
<td>Paid</td>
<td>Credit Card</td>
<td class="text-right">$250.00</td>
</tr>
<tr>
<td class="font-medium">INV002</td>
<td>Pending</td>
<td>PayPal</td>
<td class="text-right">$150.00</td>
</tr>
<tr>
<td class="font-medium">INV003</td>
<td>Unpaid</td>
<td>Bank Transfer</td>
<td class="text-right">$350.00</td>
</tr>
<tr>
<td class="font-medium">INV004</td>
<td>Paid</td>
<td>Paypal</td>
<td class="text-right">$450.00</td>
</tr>
<tr>
<td class="font-medium">INV005</td>
<td>Paid</td>
<td>Credit Card</td>
<td class="text-right">$550.00</td>
</tr>
<tr>
<td class="font-medium">INV006</td>
<td>Pending</td>
<td>Bank Transfer</td>
<td class="text-right">$200.00</td>
</tr>
<tr>
<td class="font-medium">INV007</td>
<td>Unpaid</td>
<td>Credit Card</td>
<td class="text-right">$300.00</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3">Total</td>
<td class="text-right">$2,500.00</td>
</tr>
</tfoot>
</table>
</div>
</div>
</section>
@@ -1,105 +0,0 @@
{% from "tabs.njk" import tabs %}
<section id="tabs" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Tabs</h2>
<a href="/components/tabs" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col gap-6">
{% set account_panel %}
<div class="card">
<header>
<h2>Account</h2>
<p>Make changes to your account here. Click save when you're done.</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-3">
<label for="demo-tabs-account-name">Name</label>
<input type="text" id="demo-tabs-account-name" value="Pedro Duarte" />
</div>
<div class="grid gap-3">
<label for="demo-tabs-account-username">Username</label>
<input type="text" id="demo-tabs-account-username" value="@peduarte" />
</div>
</form>
</section>
<footer>
<button type="button" class="btn">Save changes</button>
</footer>
</div>
{% endset %}
{% set password_panel %}
<div class="card">
<header>
<h2>Password</h2>
<p>Change your password here. After saving, you'll be logged out.</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-3">
<label for="demo-tabs-password-current">Current password</label>
<input type="password" id="demo-tabs-password-current" />
</div>
<div class="grid gap-3">
<label for="demo-tabs-password-new">New password</label>
<input type="password" id="demo-tabs-password-new"/>
</div>
</form>
</section>
<footer>
<button type="button" class="btn">Save Password</button>
</footer>
</div>
{% endset %}
{% set tabsets = [
{ tab: "Account", panel: account_panel },
{ tab: "Password", panel: password_panel }
] %}
{{ tabs(
id='demo-tabs-with-panels',
tabsets=tabsets,
main_attrs={ "class": "max-w-[300px]" },
tablist_attrs={ "class": "w-full" }
) }}
{% set tabsets = [
{ tab: "Home" },
{ tab: "Settings" }
] %}
{{ tabs(
id='demo-tabs-without-panels',
tabsets=tabsets
) }}
{% set tabsets = [
{ tab: "Home" },
{ tab: "Disabled", tab_attrs: { "disabled": "disabled" } }
] %}
{{ tabs(
id='demo-tabs-disabled',
tabsets=tabsets
) }}
{% set label_preview %}
{% lucide 'app-window' %}
Preview
{% endset %}
{% set label_code %}
{% lucide 'code' %}
Code
{% endset %}
{% set tabsets = [
{ tab: label_preview },
{ tab: label_code }
] %}
{{ tabs(
id='demo-tabs-with-icons',
tabsets=tabsets
) }}
</div>
</div>
</section>
@@ -1,27 +0,0 @@
<section id="textarea" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Textarea</h2>
<a href="/components/textarea" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col gap-y-10">
<textarea class="textarea" placeholder="Type your message here"></textarea>
<textarea class="textarea" placeholder="Type your message here" aria-invalid="true"></textarea>
<div class="grid gap-3">
<label for="textarea-demo-label" class="label">Label</label>
<textarea id="textarea-demo-label" class="textarea" placeholder="Type your message here"></textarea>
</div>
<div class="grid gap-3">
<label for="textarea-demo-label-and-description" class="label">With label and description</label>
<textarea id="textarea-demo-label-and-description" class="textarea" placeholder="Type your message here"></textarea>
<p class="text-muted-foreground text-sm">Type your message and press enter to send.</p>
</div>
<div class="grid gap-3">
<label for="textarea-demo-disabled" class="label">Disabled</label>
<textarea id="textarea-demo-disabled" class="textarea" placeholder="Type your message here" disabled></textarea>
</div>
</div>
</div>
</section>
@@ -1,25 +0,0 @@
<section id="toast" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Toast</h2>
<a href="/components/toast" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-wrap items-center gap-2">
<div class="flex flex-wrap items-center gap-2">
<button class="btn-outline" hx-trigger="click" hx-get="/fragments/toast/success" hx-target="#toaster" hx-swap="beforeend">
Success
</button>
<button class="btn-outline" hx-trigger="click" hx-get="/fragments/toast/error" hx-target="#toaster" hx-swap="beforeend">
Error
</button>
<button class="btn-outline" hx-trigger="click" hx-get="/fragments/toast/info" hx-target="#toaster" hx-swap="beforeend">
Info
</button>
<button class="btn-outline" hx-trigger="click" hx-get="/fragments/toast/warning" hx-target="#toaster" hx-swap="beforeend">
Warning
</button>
</div>
</div>
</section>
@@ -1,24 +0,0 @@
<section id="tooltip" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Tooltip</h2>
<a href="/components/tooltip" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-wrap items-center gap-4">
<button type="button" class="btn-outline" data-tooltip="Top tooltip">
Top
</button>
<button type="button" class="btn-outline" data-tooltip="Right tooltip" data-side="right">
Right
</button>
<button type="button" class="btn-outline" data-tooltip="Bottom tooltip" data-side="bottom">
Bottom
</button>
<button type="button" class="btn-outline" data-tooltip="Left tooltip" data-side="left">
Left
</button>
</div>
</div>
</section>
-176
View File
@@ -1,176 +0,0 @@
{% from "sidebar.njk" import sidebar %}
{% from "popover.njk" import popover %}
<script>
(() => {
document.addEventListener('DOMContentLoaded', () => {
const sidebar = document.getElementById('sidebar');
if (!sidebar) return;
const updateCurrentLinks = () => {
const currentPath = window.location.pathname.replace(/\/$/, '');
const currentSearch = window.location.search;
const currentKey = `${currentPath}${currentSearch}`;
sidebar.querySelectorAll('a, button').forEach((item) => {
const href = item.getAttribute('href');
if (!href) {
item.removeAttribute('aria-current');
return;
}
const url = new URL(href, window.location.origin);
const itemPath = url.pathname.replace(/\/$/, '');
const itemKey = `${itemPath}${url.search}`;
if (itemKey === currentKey) {
item.setAttribute('aria-current', 'page');
} else {
item.removeAttribute('aria-current');
}
});
};
document.addEventListener('htmx:afterSettle', updateCurrentLinks);
window.addEventListener('popstate', updateCurrentLinks);
updateCurrentLinks();
});
})();
</script>
{% set footer %}
{% set trigger %}
<img src="https://github.com/hunvreus.png" class="rounded-lg shrink-0 size-8">
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">Ronan Berder</span>
<span class="truncate text-xs">@hunvreus</span>
</div>
{% lucide "chevrons-up-down" %}
{% endset %}
{% call popover(
trigger=trigger,
trigger_attrs={
"class": "btn-ghost p-2 h-12 w-full flex items-center justify-start",
"data-keep-mobile-sidebar-open": ""
},
popover_attrs={
"data-side": "top",
"class": "w-[271px] md:w-[239px]"
}
) %}
<div class="grid gap-4">
<header class="grid gap-1.5">
<h2 class="font-semibold">I hope you like Basecoat...</h2>
<p class="text-muted-foreground text-sm">My name is <a href="https://ronanberder.com" target="_blank">Ronan</a> and I made this (and <a class="underline underline-offset-4" href="https://pagescms.org" target="_target">other things</a>). If you find it useful, please consider sponsoring me on GitHub or following me on X.</p>
</header>
<footer class="grid gap-2">
<a href="https://github.com/sponsors/hunvreus" class="btn-sm" target="_blank">Sponsor me on GitHub</a>
<a href="https://x.com/hunvreus" class="btn-sm-outline" target="_blank">Follow me on X</a>
</footer>
</div>
{% endcall %}
{% endset %}
{% set header %}
<a href="/" class="btn-ghost p-2 h-12 w-full justify-start">
<div class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" class="h-4 w-4"><rect width="256" height="256" fill="none"></rect><line x1="208" y1="128" x2="128" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></line><line x1="192" y1="40" x2="40" y2="192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></line></svg>
</div>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">Basecoat</span>
<span class="truncate text-xs">v{{ pkg.version }}</span>
</div>
</a>
{# <div class="flex flex-col gap-2 px-2 text-sm">
<form class="form relative">
<label for="search" class="sr-only">Search</label>
<input type="text" placeholder="Search the docs..." class="h-8 w-full shadow-none pl-8">
{% lucide "search", { "class": "pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none" } %}
</form>
</div> #}
{% endset %}
{% set link_attrs = {
"hx-boost": "true",
"hx-select": "#content",
"hx-target": "#content",
"hx-swap": "outerHTML"
} %}
{% set new_link_attrs = {
"hx-boost": "true",
"hx-select": "#content",
"hx-target": "#content",
"hx-swap": "outerHTML",
"data-new-link": "true"
} %}
{% set icon_home %}{% lucide "house" %}{% endset %}
{% set icon_info %}{% lucide "info" %}{% endset %}
{% set icon_square_terminal %}{% lucide "square-terminal" %}{% endset %}
{% set icon_layout_dashboard %}{% lucide "layout-dashboard" %}{% endset %}
{% set icon_github %}<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>{% endset %}
{% set icon_discord %}<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Discord</title><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg>{% endset %}
{% set icon_github %}<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>{% endset %}
{% set icon_discord %}<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Discord</title><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg>{% endset %}
{% set menu = [
{ type: "group", label: "Getting started", items: [
{ label: "Introduction", url: "/introduction", icon: icon_info, attrs: link_attrs },
{ label: "Installation", url: "/installation", icon: icon_square_terminal, attrs: link_attrs },
{ label: "Kitchen sink", url: "/kitchen-sink", icon: icon_layout_dashboard, attrs: link_attrs },
{ label: "GitHub", url: "https://github.com/hunvreus/basecoat", icon: icon_github, attrs: { "target": "_blank" } },
{ label: "Discord", url: "https://basecoatui.com/chat", icon: icon_discord, attrs: { "target": "_blank" } }
]},
{ type: 'group', label: 'Components', items: [
{ label: "Accordion", url: "/components/accordion", attrs: link_attrs },
{ label: "Alert", url: "/components/alert", attrs: link_attrs },
{ label: "Alert Dialog", url: "/components/alert-dialog", attrs: link_attrs },
{ label: "Avatar", url: "/components/avatar", attrs: link_attrs },
{ label: "Badge", url: "/components/badge", attrs: link_attrs },
{ label: "Breadcrumb", url: "/components/breadcrumb", attrs: link_attrs },
{ label: "Button", url: "/components/button", attrs: link_attrs },
{ label: "Button Group", url: "/components/button-group", attrs: new_link_attrs },
{ label: "Card", url: "/components/card", attrs: link_attrs },
{ label: "Checkbox", url: "/components/checkbox", attrs: link_attrs },
{ label: "Command", url: "/components/command", attrs: new_link_attrs },
{ label: "Combobox", url: "/components/combobox", attrs: link_attrs },
{ label: "Dialog", url: "/components/dialog", attrs: link_attrs },
{ label: "Dropdown Menu", url: "/components/dropdown-menu", attrs: link_attrs },
{ label: "Empty", url: "/components/empty", attrs: new_link_attrs },
{ label: "Field", url: "/components/field", attrs: new_link_attrs },
{ label: "Form", url: "/components/form", attrs: link_attrs },
{ label: "Input", url: "/components/input", attrs: link_attrs },
{ label: "Input Group", url: "/components/input-group", attrs: new_link_attrs },
{ label: "Item", url: "/components/item", attrs: new_link_attrs },
{ label: "Kbd", url: "/components/kbd", attrs: new_link_attrs },
{ label: "Label", url: "/components/label", attrs: link_attrs },
{ label: "Pagination", url: "/components/pagination", attrs: link_attrs },
{ label: "Popover", url: "/components/popover", attrs: link_attrs },
{ label: "Progress", url: "/components/progress", attrs: new_link_attrs },
{ label: "Radio Group", url: "/components/radio-group", attrs: link_attrs },
{ label: "Select", url: "/components/select", attrs: link_attrs },
{ label: "Sidebar", url: "/components/sidebar", attrs: link_attrs },
{ label: "Skeleton", url: "/components/skeleton", attrs: link_attrs },
{ label: "Slider", url: "/components/slider", attrs: link_attrs },
{ label: "Spinner", url: "/components/spinner", attrs: new_link_attrs },
{ label: "Switch", url: "/components/switch", attrs: link_attrs },
{ label: "Table", url: "/components/table", attrs: link_attrs },
{ label: "Tabs", url: "/components/tabs", attrs: link_attrs },
{ label: "Textarea", url: "/components/textarea", attrs: link_attrs },
{ label: "Theme Switcher", url: "/components/theme-switcher", attrs: link_attrs },
{ label: "Toast", url: "/components/toast", attrs: link_attrs },
{ label: "Tooltip", url: "/components/tooltip", attrs: link_attrs }
]}
] %}
{{ sidebar(
id="sidebar",
label="Sidebar navigation",
header=header,
footer=footer,
content_attrs={ "class": "scrollbar [&_[data-new-link]::after]:content-['New'] [&_[data-new-link]::after]:ml-auto [&_[data-new-link]::after]:text-xs [&_[data-new-link]::after]:font-medium [&_[data-new-link]::after]:bg-sidebar-primary [&_[data-new-link]::after]:text-sidebar-primary-foreground [&_[data-new-link]::after]:px-2 [&_[data-new-link]::after]:py-0.5 [&_[data-new-link]::after]:rounded-md" },
menu=menu
) }}
+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Discord</title><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
@import "tailwindcss" source(none);
@source "../../../../docs/src/**/*.{astro,html,js,md,mdx}";
@source "../../../../src/pages/**/*.{astro,html,js}";
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-luma.css";
@import "reallysimpledocs/css";
@import "reallysimpledocs/css/styles/luma";
+8
View File
@@ -0,0 +1,8 @@
@import "tailwindcss" source(none);
@source "../../../../docs/src/**/*.{astro,html,js,md,mdx}";
@source "../../../../src/pages/**/*.{astro,html,js}";
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-lyra.css";
@import "reallysimpledocs/css";
@import "reallysimpledocs/css/styles/lyra";
+8
View File
@@ -0,0 +1,8 @@
@import "tailwindcss" source(none);
@source "../../../../docs/src/**/*.{astro,html,js,md,mdx}";
@source "../../../../src/pages/**/*.{astro,html,js}";
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-maia.css";
@import "reallysimpledocs/css";
@import "reallysimpledocs/css/styles/maia";
+8
View File
@@ -0,0 +1,8 @@
@import "tailwindcss" source(none);
@source "../../../../docs/src/**/*.{astro,html,js,md,mdx}";
@source "../../../../src/pages/**/*.{astro,html,js}";
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-mira.css";
@import "reallysimpledocs/css";
@import "reallysimpledocs/css/styles/mira";
+8
View File
@@ -0,0 +1,8 @@
@import "tailwindcss" source(none);
@source "../../../../docs/src/**/*.{astro,html,js,md,mdx}";
@source "../../../../src/pages/**/*.{astro,html,js}";
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-nova.css";
@import "reallysimpledocs/css";
@import "reallysimpledocs/css/styles/nova";
+8
View File
@@ -0,0 +1,8 @@
@import "tailwindcss" source(none);
@source "../../../../docs/src/**/*.{astro,html,js,md,mdx}";
@source "../../../../src/pages/**/*.{astro,html,js}";
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-rhea.css";
@import "reallysimpledocs/css";
@import "reallysimpledocs/css/styles/rhea";
+8
View File
@@ -0,0 +1,8 @@
@import "tailwindcss" source(none);
@source "../../../../docs/src/**/*.{astro,html,js,md,mdx}";
@source "../../../../src/pages/**/*.{astro,html,js}";
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-sera.css";
@import "reallysimpledocs/css";
@import "reallysimpledocs/css/styles/sera";
+8
View File
@@ -0,0 +1,8 @@
@import "tailwindcss" source(none);
@source "../../../../docs/src/**/*.{astro,html,js,md,mdx}";
@source "../../../../src/pages/**/*.{astro,html,js}";
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-vega.css";
@import "reallysimpledocs/css";
@import "reallysimpledocs/css/styles/vega";
+221
View File
@@ -0,0 +1,221 @@
# CLI
## Install Components
Use the CLI to copy Basecoat component assets into your project. It can install CSS, JavaScript, and the matching Nunjucks or Jinja macros for components that ship template helpers.
<CodeGroup>
```bash title="npm"
npx basecoat-cli add dialog
```
```bash title="pnpm"
pnpm dlx basecoat-cli add dialog
```
```bash title="bun"
bunx basecoat-cli add dialog
```
```bash title="yarn"
yarn dlx basecoat-cli add dialog
```
</CodeGroup>
The CLI asks which template engine you use and where it should copy JavaScript and macro files.
## Macros
Nunjucks and Jinja macros are optional helpers for components with larger HTML structures. They generate the same semantic Basecoat markup shown in the component docs.
<CodeGroup>
```jinja title="Nunjucks"
{% from "select.njk" import select %}
{{ select(
name="fruit",
items=[
{ value: "apple", label: "Apple" },
{ value: "banana", label: "Banana" },
{ value: "blueberry", label: "Blueberry" }
]
) }}
```
```jinja title="Jinja"
{% from "select.html.jinja" import select %}
{{ select(
name="fruit",
items=[
{ "value": "apple", "label": "Apple" },
{ "value": "banana", "label": "Banana" },
{ "value": "blueberry", "label": "Blueberry" }
]
) }}
```
</CodeGroup>
### Public Macros
| Component | Macro | Nunjucks | Jinja |
| --- | --- | --- | --- |
| [Combobox](/components/combobox) | `combobox()` | [combobox.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/combobox.njk#L18) | [combobox.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/combobox.html.jinja#L4) |
| [Command](/components/command) | `command()` | [command.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/command.njk#L12) | [command.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/command.html.jinja#L12) |
| [Command](/components/command) | `command_dialog()` | [command.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/command.njk#L79) | [command.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/command.html.jinja#L79) |
| [Dialog](/components/dialog) | `dialog()` | [dialog.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/dialog.njk#L19) | [dialog.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/dialog.html.jinja#L19) |
| [Dropdown Menu](/components/dropdown-menu) | `dropdown_menu()` | [dropdown-menu.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/dropdown-menu.njk#L11) | [dropdown-menu.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/dropdown-menu.html.jinja#L11) |
| [Popover](/components/popover) | `popover()` | [popover.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/popover.njk#L10) | [popover.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/popover.html.jinja#L10) |
| [Select](/components/select) | `select()` | [select.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/select.njk#L17) | [select.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/select.html.jinja#L16) |
| [Sidebar](/components/sidebar) | `sidebar()` | [sidebar.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/sidebar.njk#L16) | [sidebar.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/sidebar.html.jinja#L16) |
| [Tabs](/components/tabs) | `tabs()` | [tabs.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/tabs.njk#L16) | [tabs.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/tabs.html.jinja#L17) |
| [Toast](/components/toast) | `toaster()` | [toast.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/toast.njk#L11) | [toast.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/toast.html.jinja#L11) |
| [Toast](/components/toast) | `toast()` | [toast.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/toast.njk#L53) | [toast.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/toast.html.jinja#L53) |
Some macro files include internal recursive helpers such as `render_select_items()`. Those helpers are implementation details and are not part of the public API.
## Macro Reference
### `combobox()`
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | generated | Unique combobox id. |
| `selected` | `string \| array \| object` | `None` / `none` | Initial selected value or values. Objects can include `value` and `label`. |
| `name` | `string` | `None` / `none` | Hidden input name. |
| `multiple` | `boolean` | `false` | Enables chip-based multiple selection. |
| `placeholder` | `string` | `None` / `none` | Visible input placeholder. |
| `close_on_select` | `boolean` | `false` | Closes the popover after each option selection in multiple mode. |
| `auto_highlight` | `boolean` | `false` | Highlights the first matching option while filtering. |
| `format` | `"value" \| "object"` | `"value"` | Hidden input serialization format. |
| `main_attrs` | `object` | `{}` | Extra attributes for the `.combobox` root. |
| `input_attrs` | `object` | `{}` | Extra attributes for the visible input or chip input wrapper. |
| `popover_attrs` | `object` | `{}` | Extra attributes for the popover element. |
| `listbox_attrs` | `object` | `{}` | Extra attributes for the listbox. |
| `hidden_input_attrs` | `object` | `{}` | Extra attributes for the hidden input. |
### `command()`
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | generated | Unique command id. |
| `items` | `array` | `None` / `none` | Optional structured item data. Use caller content when omitted. |
| `placeholder` | `string` | `"Type a command or search..."` | Search input placeholder. |
| `empty_text` | `string` | `"No results found."` | Empty-state text. |
| `main_attrs` | `object` | `{}` | Extra attributes for the `.command` root. |
| `input_attrs` | `object` | `{}` | Extra attributes for the search input. |
| `menu_attrs` | `object` | `{}` | Extra attributes for the command menu. |
### `command_dialog()`
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | generated | Unique command dialog id. |
| `items` | `array` | `None` / `none` | Optional structured item data. Use caller content when omitted. |
| `placeholder` | `string` | `"Type a command or search..."` | Search input placeholder. |
| `empty_text` | `string` | `"No results found."` | Empty-state text. |
| `dialog_attrs` | `object` | `{}` | Extra attributes for the native dialog. |
| `input_attrs` | `object` | `{}` | Extra attributes for the search input. |
| `menu_attrs` | `object` | `{}` | Extra attributes for the command menu. |
| `open` | `boolean` | `false` | Renders the dialog open by default. |
### `dialog()`
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | generated | Unique dialog id. |
| `trigger` | `string` | `None` / `none` | Text or HTML for a generated trigger button. |
| `title` | `string` | `None` / `none` | Dialog title. |
| `description` | `string` | `None` / `none` | Dialog description. |
| `footer` | `string` | `None` / `none` | HTML for the dialog footer. |
| `dialog_attrs` | `object` | `{}` | Extra attributes for the native dialog. |
| `trigger_attrs` | `object` | `{}` | Extra attributes for the generated trigger button. |
| `header_attrs` | `object` | `{}` | Extra attributes for the dialog header. |
| `body_attrs` | `object` | `{}` | Extra attributes for the dialog body section. |
| `footer_attrs` | `object` | `{}` | Extra attributes for the dialog footer. |
| `open` | `boolean` | `false` | Renders the dialog open by default. |
| `close_button` | `boolean` | `true` | Includes the generated close button. |
| `close_on_overlay_click` | `boolean` | `true` | Adds backdrop-click close behavior. |
### `dropdown_menu()`
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `trigger` | `string` | required | HTML for the trigger button. |
| `id` | `string` | generated | Unique dropdown menu id. |
| `items` | `array` | `None` / `none` | Optional structured menu item data. Use caller content when omitted. |
| `main_attrs` | `object` | `{}` | Extra attributes for the `.dropdown-menu` root. |
| `trigger_attrs` | `object` | `{}` | Extra attributes for the trigger button. |
| `popover_attrs` | `object` | `{}` | Extra attributes for the popover element. |
| `menu_attrs` | `object` | `{}` | Extra attributes for the menu element. |
### `popover()`
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `trigger` | `string` | required | HTML for the trigger button. |
| `id` | `string` | generated | Unique popover id. |
| `main_attrs` | `object` | `{}` | Extra attributes for the `.popover` root. |
| `trigger_attrs` | `object` | `{}` | Extra attributes for the trigger button. |
| `popover_attrs` | `object` | `{}` | Extra attributes for the popover element. |
### `select()`
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | generated | Unique select id. |
| `selected` | `string \| array` | `None` / `none` | Initial selected value or values. |
| `name` | `string` | generated from `id` | Hidden input name. |
| `items` | `array` | `None` / `none` | Optional structured item data. Use caller content when omitted. |
| `multiple` | `boolean` | `false` | Enables multiple selection. |
| `placeholder` | `string` | `None` / `none` | Placeholder text for multiple mode with no selection. |
| `close_on_select` | `boolean` | `false` | Closes the popover after each option selection in multiple mode. |
| `format` | `"value" \| "object"` | `"value"` | Hidden input serialization format. |
| `main_attrs` | `object` | `{}` | Extra attributes for the `.select` root. |
| `trigger_attrs` | `object` | `{}` | Extra attributes for the trigger button. |
| `popover_attrs` | `object` | `{}` | Extra attributes for the popover element. |
| `listbox_attrs` | `object` | `{}` | Extra attributes for the listbox. |
| `input_attrs` | `object` | `{}` | Extra attributes for the hidden input. |
### `sidebar()`
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | `None` / `none` | Optional sidebar id. Use one when multiple sidebars exist. |
| `label` | `string` | `"Sidebar navigation"` | Navigation landmark label. |
| `open` | `boolean` | `true` | Initial open state. |
| `side` | `"left" \| "right"` | `"left"` | Side of the viewport. |
| `header` | `string` | `None` / `none` | HTML for the sidebar header. |
| `footer` | `string` | `None` / `none` | HTML for the sidebar footer. |
| `menu` | `array` | `None` / `none` | Optional structured menu data. Use caller content when omitted. |
| `main_attrs` | `object` | `{}` | Extra attributes for the `.sidebar` root. |
| `header_attrs` | `object` | `{}` | Extra attributes for the header. |
| `content_attrs` | `object` | `{}` | Extra attributes for the scrollable content section. |
| `footer_attrs` | `object` | `{}` | Extra attributes for the footer. |
### `tabs()`
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | generated | Unique tabs id. |
| `tabsets` | `array` | `[]` | Tab definitions. Each item can include `tab`, `panel`, `tab_attrs`, and `panel_attrs`. |
| `main_attrs` | `object` | `{}` | Extra attributes for the `.tabs` root. |
| `tablist_attrs` | `object` | `{}` | Extra attributes for the tablist. Use `aria-orientation="vertical"` or `data-variant="line"` here. |
| `default_tab_index` | `number` | `1` | One-based index of the initially selected tab. |
### `toaster()`
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | `"toaster"` | Toaster container id. |
| `toasts` | `array` | `[]` | Initial toast objects rendered inside the toaster. |
| `attrs` | `object` | `{}` | Extra attributes for the `.toaster` container. |
### `toast()`
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `category` | `"success" \| "error" \| "info" \| "warning"` | `"success"` | Toast category. Controls icon and ARIA role. |
| `title` | `string` | `""` | Toast title. |
| `description` | `string` | `""` | Toast description. |
| `action` | `object` | `None` / `none` | Action button/link config. Supports `label`, `onclick`, and `href`. |
| `cancel` | `object` | `None` / `none` | Cancel button/link config. Supports `label`, `onclick`, and `href`. |
| `attrs` | `object` | `{}` | Extra attributes for the `.toast` element. |
+309
View File
@@ -0,0 +1,309 @@
# Accordion
A vertically stacked set of interactive headings that each reveal a section of content.
<Preview class="w-full max-w-sm">
<section class="accordion">
<details open>
<summary>
What are your shipping options?
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>We offer standard (5-7 days), express (2-3 days), and overnight shipping. Free shipping on international orders.</section>
</details>
<details>
<summary>
What is your return policy?
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>You can return items within 30 days of delivery. Items must be unused and in their original packaging.</section>
</details>
<details>
<summary>
How can I contact customer support?
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>Email support@example.com or use live chat during business hours.</section>
</details>
</section>
</Preview>
## Usage
<Steps>
<Step title="Include CSS">
Import Tailwind and one full Basecoat style bundle.
```css
@import "tailwindcss";
@import "basecoat-css/vega.css";
```
Or import only the base CSS, Accordion component CSS, and one style pack.
```css
@import "tailwindcss";
@import "basecoat-css/base.css";
@import "basecoat-css/components/accordion.css";
@import "basecoat-css/styles/vega.css";
```
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Include JavaScript">
Copy or serve the full Basecoat JavaScript bundle.
```html
<script src="/assets/js/all.min.js" defer></script>
```
Or copy or serve the Basecoat runtime and Accordion script.
```html
<script src="/assets/js/basecoat.min.js" defer></script>
<script src="/assets/js/accordion.min.js" defer></script>
```
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Add your accordion HTML">
```html
<section class="accordion">
<details open>
<summary>
Is it accessible?
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>Yes. It uses native disclosure semantics.</section>
</details>
</section>
```
</Step>
</Steps>
Basecoat uses native `<details>` and `<summary>` elements for disclosure semantics. The accordion JavaScript only enforces single-item behavior by default and prevents disabled items from toggling. Add `data-multiple` to the root when more than one item can be open at a time.
### HTML structure
<dl>
<dt><code>&lt;section class="accordion"&gt;</code></dt>
<dd>
Root accordion container. Add <code>data-multiple</code> to allow multiple open items.
<dl>
<dt><code>&lt;details open&gt;</code></dt>
<dd>
Accordion item. Use the native <code>open</code> attribute for the default expanded item. Add <code>aria-disabled="true"</code> when an item should not toggle.
<dl>
<dt><code>&lt;summary&gt;</code></dt>
<dd>Accordion trigger. Keep the trigger text visible and add an optional trailing icon.</dd>
<dt><code>&lt;section&gt;</code></dt>
<dd>Accordion content. Use any semantic element that fits your content.</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
### JavaScript API
Accordion does not expose custom methods or events. Programmatically open or close an item with the native `details.open` boolean.
```js
document.querySelector("#billing").open = true
```
## Examples
### Basic
A basic accordion that shows one item at a time. The first item is open by default.
<Preview class="w-full max-w-sm">
<section class="accordion">
<details open>
<summary>
How do I reset my password?
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>Click on 'Forgot Password' on the login page, enter your email address, and we'll send you a link to reset your password. The link will expire in 24 hours.</section>
</details>
<details>
<summary>
Can I change my subscription plan?
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>Yes. You can upgrade or downgrade your plan from the billing settings page.</section>
</details>
<details>
<summary>
What payment methods do you accept?
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>We accept all major credit cards, PayPal, and bank transfers for annual plans.</section>
</details>
</section>
</Preview>
### Multiple
Use `data-multiple` to allow multiple items to be open at the same time.
<Preview class="w-full max-w-md">
<section class="accordion" data-multiple>
<details open>
<summary>
Notification Settings
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>Manage how you receive notifications. You can enable email alerts for updates or push notifications for mobile devices.</section>
</details>
<details>
<summary>
Privacy & Security
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>Control account security, session history, and privacy preferences.</section>
</details>
<details>
<summary>
Billing & Subscription
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>Review invoices, change plans, and update payment methods.</section>
</details>
</section>
</Preview>
### Disabled
Use `aria-disabled="true"` on an item to disable it.
<Preview class="w-full max-w-md">
<section class="accordion">
<details open>
<summary>
Can I access my account history?
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>Yes. Your account history is available from the activity page.</section>
</details>
<details aria-disabled="true">
<summary>
Premium feature information
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>This item is disabled.</section>
</details>
<details>
<summary>
How do I update my email address?
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>Go to account settings, update your email, then confirm the change from your inbox.</section>
</details>
</section>
</Preview>
### Borders
Add horizontal padding to the rows when using a bordered root.
<Preview class="w-full max-w-md">
<section class="accordion rounded-md border [&>details>summary]:px-4 [&>details>:not(summary)]:px-4">
<details id="billing" open>
<summary>
How does billing work?
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>We offer monthly and annual subscription plans. Billing is charged at the beginning of each cycle, and you can cancel anytime. All plans include automatic backups, 24/7 support, and unlimited team members.</section>
</details>
<details>
<summary>
Is my data secure?
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>Data is encrypted in transit and at rest.</section>
</details>
<details>
<summary>
What integrations do you support?
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>We support Slack, GitHub, Google Workspace, and webhooks.</section>
</details>
</section>
</Preview>
### Card
Wrap the accordion in a card for grouped support or billing content.
<Preview class="w-full max-w-md">
<div class="card">
<header>
<h2>Subscription & Billing</h2>
<p>Common questions about your account, plans, payments and cancellations.</p>
</header>
<section>
<section class="accordion">
<details open>
<summary>
What subscription plans do you offer?
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>We offer three subscription tiers: Starter ($9/month), Professional ($29/month), and Enterprise ($99/month). Each plan includes increasing storage limits, API access, priority support, and team collaboration features.</section>
</details>
<details>
<summary>
How does billing work?
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>Billing is charged at the beginning of each cycle.</section>
</details>
<details>
<summary>
How do I cancel my subscription?
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>Cancel from billing settings. Your plan remains active until the end of the current cycle.</section>
</details>
</section>
</section>
</div>
</Preview>
### RTL
Accordion spacing uses logical properties. Set `dir="rtl"` on the accordion or a parent element.
<Preview class="w-full max-w-md">
<section class="accordion" dir="rtl">
<details open>
<summary>
كيف يمكنني إعادة تعيين كلمة المرور؟
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>انقر على 'نسيت كلمة المرور' في صفحة تسجيل الدخول، أدخل عنوان بريدك الإلكتروني، وسنرسل لك رابطا لإعادة تعيين كلمة المرور. سينتهي صلاحية الرابط خلال 24 ساعة.</section>
</details>
<details>
<summary>
هل يمكنني تغيير خطة الاشتراك الخاصة بي؟
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>نعم، يمكنك تغيير الخطة من إعدادات الفوترة.</section>
</details>
<details>
<summary>
ما هي طرق الدفع التي تقبلونها؟
<svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</summary>
<section>نقبل بطاقات الائتمان الرئيسية والتحويلات البنكية.</section>
</details>
</section>
</Preview>
-75
View File
@@ -1,75 +0,0 @@
---
layout: layouts/page.njk
title: Accordion
description: A vertically stacked set of interactive headings that each reveal a section of content.
toc:
- label: Usage
id: usage
---
{% from "macros/code_preview.njk" import code_preview %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated Accordion component in Basecoat.</h2>
</div>
{% set code %}<section class="accordion">
<details class="group border-b last:border-b-0 ">
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
Is it accessible?
{% lucide "chevron-down", { "class": "text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200 group-open:rotate-180" } %}
</h2>
</summary>
<section class="pb-4">
<p class="text-sm">Yes. It adheres to the WAI-ARIA design pattern.</p>
</section>
</details>
<details class="group border-b last:border-b-0">
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
Is it styled?
{% lucide "chevron-down", { "class": "text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200 group-open:rotate-180" } %}
</h2>
</summary>
<section class="pb-4">
<p class="text-sm">Yes. It comes with default styles that matches the other components' aesthetic.</p>
</section>
</details>
<details class="group border-b last:border-b-0">
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
Is it animated?
{% lucide "chevron-down", { "class": "text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200 group-open:rotate-180" } %}
</h2>
</summary>
<section>
<p class="text-sm whitespace-pre-wrap">Yes. It's animated by default, but you can disable it if you prefer.</p>
</section>
</details>
</section>
<script>
(() => {
const accordions = document.querySelectorAll('.accordion');
accordions.forEach(accordion => {
accordion.addEventListener('click', (event) => {
const summary = event.target.closest('summary');
if (!summary) return;
const details = summary.closest('details');
if (!details) return;
accordion.querySelectorAll('details').forEach(detailsEl => {
if (detailsEl !== details) {
detailsEl.removeAttribute('open');
}
});
});
});
})();
</script>
{% endset %}
{{ code_preview("accordion", code | prettyHtml, "w-full max-w-md") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<p class="prose">Basecoat already animates <code>&lt;details&gt;</code> elements by default. The example add some Tailwind CSS utility classes for style and a bit of vanilla JavaScript to handle the open/close state.</p>
+187
View File
@@ -0,0 +1,187 @@
# Alert Dialog
A modal dialog that interrupts the user with important content and expects a response.
<Preview>
<button type="button" onclick="document.getElementById('demo-alert-dialog').showModal()" class="btn" data-variant="outline">Show Dialog</button>
<dialog id="demo-alert-dialog" class="alert-dialog" aria-labelledby="demo-alert-dialog-title" aria-describedby="demo-alert-dialog-description">
<div>
<header>
<h2 id="demo-alert-dialog-title">Are you absolutely sure?</h2>
<p id="demo-alert-dialog-description">This action cannot be undone. This will permanently delete your account and remove your data from our servers.</p>
</header>
<footer>
<button class="btn" data-variant="outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Continue</button>
</footer>
</div>
</dialog>
</Preview>
## Usage
<Steps>
<Step title="Include CSS">
Import Tailwind and one full Basecoat style bundle.
```css
@import "tailwindcss";
@import "basecoat-css/vega.css";
```
Or import only the base CSS, Alert Dialog component CSS, and one style pack.
```css
@import "tailwindcss";
@import "basecoat-css/base.css";
@import "basecoat-css/components/dialog.css";
@import "basecoat-css/styles/vega.css";
```
Alert Dialog uses the Dialog component CSS.
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Add your alert dialog HTML">
Alert Dialog uses a native `<dialog class="alert-dialog">` element. Omit the close button and backdrop-click handler when the user must choose an explicit action.
```html
<button type="button" onclick="document.getElementById('demo-alert-dialog').showModal()" class="btn" data-variant="outline">Show Dialog</button>
<dialog id="demo-alert-dialog" class="alert-dialog" aria-labelledby="demo-alert-dialog-title" aria-describedby="demo-alert-dialog-description">
<div>
<header>
<h2 id="demo-alert-dialog-title">Are you absolutely sure?</h2>
<p id="demo-alert-dialog-description">This action cannot be undone. This will permanently delete your account and remove your data from our servers.</p>
</header>
<footer>
<button class="btn" data-variant="outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Continue</button>
</footer>
</div>
</dialog>
```
</Step>
</Steps>
### HTML structure
<dl>
<dt><code>&lt;button type="button" onclick="dialog.showModal()"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Trigger button. Basecoat uses the native <code>HTMLDialogElement.showModal()</code> method.</dd>
<dt><code>&lt;dialog class="alert-dialog" id="&#123; DIALOG_ID &#125;"&gt;</code></dt>
<dd>
Native modal dialog. Add <code>aria-labelledby</code> and <code>aria-describedby</code> when title and description are present.
<dl>
<dt><code>&lt;div&gt;</code></dt>
<dd>
Dialog content surface.
<dl>
<dt><code>&lt;header&gt;</code></dt>
<dd>
Alert dialog header.
<dl>
<dt><code>&lt;figure&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Media area for an icon or image.</dd>
<dt><code>&lt;h2 id="&#123; TITLE_ID &#125;"&gt;</code></dt>
<dd>Alert dialog title. Reference it from <code>aria-labelledby</code>.</dd>
<dt><code>&lt;p id="&#123; DESCRIPTION_ID &#125;"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Alert dialog description. Reference it from <code>aria-describedby</code>.</dd>
</dl>
</dd>
<dt><code>&lt;footer&gt;</code></dt>
<dd>Action area. Include a cancel action and the primary action.</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
## Examples
### Basic
A basic alert dialog with a title, description, and cancel and continue buttons.
<Preview>
<button type="button" onclick="document.getElementById('alert-dialog-basic').showModal()" class="btn" data-variant="outline">Show Dialog</button>
<dialog id="alert-dialog-basic" class="alert-dialog" aria-labelledby="alert-dialog-basic-title" aria-describedby="alert-dialog-basic-description">
<div>
<header>
<h2 id="alert-dialog-basic-title">Are you absolutely sure?</h2>
<p id="alert-dialog-basic-description">This action cannot be undone. This will permanently delete your account and remove your data from our servers.</p>
</header>
<footer>
<button class="btn" data-variant="outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Continue</button>
</footer>
</div>
</dialog>
</Preview>
### Media
Use a `<figure>` in the header to add a media element such as an icon or image to the alert dialog.
<Preview>
<button type="button" onclick="document.getElementById('alert-dialog-media').showModal()" class="btn" data-variant="outline">Share Project</button>
<dialog id="alert-dialog-media" class="alert-dialog" aria-labelledby="alert-dialog-media-title" aria-describedby="alert-dialog-media-description">
<div>
<header>
<figure><svg class="lucide lucide-circle-fading-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a10 10 0 0 1 7.38 16.75" /><path d="M12 8v8" /><path d="M16 12H8" /><path d="M2.5 8.88a10 10 0 0 0 0 6.24" /><path d="M4.64 4.64a10 10 0 0 0 0 14.72" /></svg></figure>
<h2 id="alert-dialog-media-title">Share this project?</h2>
<p id="alert-dialog-media-description">Anyone with the link will be able to view and edit this project.</p>
</header>
<footer>
<button class="btn" data-variant="outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Share</button>
</footer>
</div>
</dialog>
</Preview>
### Destructive
Use a destructive action button when the alert dialog confirms a destructive operation.
<Preview>
<button type="button" onclick="document.getElementById('alert-dialog-destructive').showModal()" class="btn" data-variant="destructive">Delete Chat</button>
<dialog id="alert-dialog-destructive" class="alert-dialog" aria-labelledby="alert-dialog-destructive-title" aria-describedby="alert-dialog-destructive-description">
<div>
<header>
<figure><svg class="lucide lucide-trash-2" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /><path d="M14 11v6" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /><path d="M3 6h18" /><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></svg></figure>
<h2 id="alert-dialog-destructive-title">Delete this chat?</h2>
<p id="alert-dialog-destructive-description">This action cannot be undone. This will permanently delete the chat and remove it from your history.</p>
</header>
<footer>
<button class="btn" data-variant="outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" data-variant="destructive" onclick="this.closest('dialog').close()">Delete</button>
</footer>
</div>
</dialog>
</Preview>
### RTL
Alert Dialog supports document direction. Set `dir="rtl"` on the dialog or a parent element.
<Preview>
<div dir="rtl">
<button type="button" onclick="document.getElementById('alert-dialog-rtl').showModal()" class="btn" data-variant="outline">إظهار الحوار</button>
<dialog id="alert-dialog-rtl" class="alert-dialog" aria-labelledby="alert-dialog-rtl-title" aria-describedby="alert-dialog-rtl-description">
<div>
<header>
<h2 id="alert-dialog-rtl-title">هل أنت متأكد تماماً؟</h2>
<p id="alert-dialog-rtl-description">لا يمكن التراجع عن هذا الإجراء. سيتم حذف حسابك نهائياً وإزالة بياناتك من خوادمنا.</p>
</header>
<footer>
<button class="btn" data-variant="outline" onclick="this.closest('dialog').close()">إلغاء</button>
<button class="btn" onclick="this.closest('dialog').close()">متابعة</button>
</footer>
</div>
</dialog>
</div>
</Preview>
-119
View File
@@ -1,119 +0,0 @@
---
layout: layouts/page.njk
title: Alert Dialog
description: A modal dialog that interrupts the user with important content and expects a response.
toc:
- label: Usage
id: usage
children:
- label: HTML
id: usage-html
- label: Jinja and Nunjucks
id: usage-macro
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "dialog.njk" import dialog %}
{% set code_html %}
{% set footer %}
<button class="btn-outline" onclick="document.getElementById('alert-dialog').close()">Cancel</button>
<button class="btn-primary" onclick="document.getElementById('alert-dialog').close()">Continue</button>
{% endset %}
{{ dialog(
id="alert-dialog",
title="Are you absolutely sure?",
description="This action cannot be undone. This will permanently delete your account and remove your data from our servers.",
trigger="Open alert dialog",
trigger_attrs={"class": "btn-outline"},
footer=footer,
close_button=false,
close_on_overlay_click=false
) }}
{% endset %}
{{ code_preview("alert-dialog", code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-default"><a href="#usage-html">HTML</a></h3>
<section class="prose">
<p>The Alert Dialog component is identical to <a href="/components/dialog" class="font-medium underline underline-offset-4">Dialog</a> except for two small differences: we do not display a close button and we do not let the user close the dialog when clicking on the backdrop (no <code>onclick</code> attribute on the <code>&lt;dialog&gt;</code> or the <code>&lt;article&gt;</code> elements).</p>
</section>
{{ code_block(code_html | prettyHtml) }}
<section class="prose">
<p>The component has the following HTML structure:</p>
<dl>
<dt><code class="highlight language-html">&lt;button type="button"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The trigger button to open the alert dialog (using <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal" target="_blank">the native <code>showModal()</code> method</a>).</dd>
<dt><code class="highlight language-html">&lt;dialog class="dialog" id="{ DIALOG_ID }"&gt;</code></dt>
<dd>Wraps the whole component and acts as the backdrop. It can also have the following attributes:
<ul>
<li><code>aria-labelledby="{ TITLE_ID }"</code>: linked to by the <code>aria-labelledby</code> attribute of the dialog.</li>
<li><code>aria-describedby="{ DESCRIPTION_ID }"</code>: linked to by the <code>aria-describedby</code> attribute of the <code>&lt;dialog&gt;</code>.</li>
</ul>
<dl>
<dt><code class="highlight language-html">&lt;article&gt;</code></dt>
<dd>Wraps the content of the alert dialog.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dd>Contains the header for the alert dialog:
<dl>
<dt><code class="highlight language-html">&lt;h2&gt;</code></dt>
<dd>The title of the alert dialog, must have an <code>id</code> if you use the <code>aria-labelledby</code> attribute on the <code>&lt;dialog&gt;</code>.</dd>
<dt><code class="highlight language-html">&lt;p&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The description of the alert dialog, must have an <code>id</code> if you use the <code>aria-describedby</code> attribute on the <code>&lt;dialog&gt;</code>.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>For the content.</dd>
<dt><code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Usually contains actions.</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
</section>
<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>
<div class="prose">
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">dialog()</code> Nunjucks or Jinja macro for this component. Make sure to set <code>close_on_overlay_click</code> and <code>close_button</code> to <code>False</code> (or <code>false</code> for Nunjucks).</p>
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/dialog.html.jinja" target="_blank">
Jinja macro
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/dialog.njk" target="_blank">
Nunjucks macro
{% lucide "arrow-right" %}
</a>
</div>
{% set raw_code %}{% raw %}{% set footer %}
<button class="btn-outline" onclick="document.getElementById('demo-alert-dialog').close()">Cancel</button>
<button class="btn-primary" onclick="document.getElementById('demo-alert-dialog').close()">Continue</button>
{% endset %}
{{ dialog(
id="demo-alert-dialog",
title="Are you absolutely sure?",
description="This action cannot be undone. This will permanently delete your account and remove your data from our servers.",
trigger="Open Alert Dialog",
trigger_attrs={"class": "btn-outline"},
footer=footer,
close_button=False,
close_on_overlay_click=False
) }}{% endraw %}
{% endset%}
{{ code_block(raw_code, "jinja") }}
+140
View File
@@ -0,0 +1,140 @@
# Alert
<Preview class="w-full max-w-md">
<div class="grid items-start gap-4">
<div class="alert"><svg class="lucide lucide-circle-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="m9 12 2 2 4-4" /></svg>
<h2>Payment successful</h2>
<section>Your payment of $29.99 has been processed. A receipt has been sent to your email address.</section>
</div>
<div class="alert"><svg class="lucide lucide-info" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M12 16v-4" /><path d="M12 8h.01" /></svg>
<h2>New feature available</h2>
<section>We've added dark mode support. You can enable it in your account settings.</section>
</div>
</div>
</Preview>
## Usage
<Steps>
<Step title="Include CSS">
Import Tailwind and one full Basecoat style bundle.
```css
@import "tailwindcss";
@import "basecoat-css/vega.css";
```
Or import only the base CSS, Alert component CSS, and one style pack.
```css
@import "tailwindcss";
@import "basecoat-css/base.css";
@import "basecoat-css/components/alert.css";
@import "basecoat-css/styles/vega.css";
```
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Add your alert HTML">
Use `alert` for default styling. Add `data-variant="destructive"` for error states. Add a direct child `<footer>` for an inline-end action region.
```html
<div class="alert"><svg class="lucide lucide-info" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M12 16v-4" /><path d="M12 8h.01" /></svg>
<h2>Heads up!</h2>
<section>You can add components and dependencies to your app using the cli.</section>
<footer>
<button type="button" class="btn" data-variant="outline">Enable</button>
</footer>
</div>
```
</Step>
</Steps>
### HTML structure
<dl>
<dt><code>&lt;div class="alert"&gt;</code></dt>
<dd>
Main container. Use <code>data-variant="destructive"</code> for error states.
<dl>
<dt><code>&lt;svg&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>The icon.</dd>
<dt><code>&lt;h2&gt;</code></dt>
<dd>The title.</dd>
<dt><code>&lt;section&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>The description.</dd>
<dt><code>&lt;footer&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>The action region, aligned to the inline end of the alert. Put the action element inside it, such as a button or link.</dd>
</dl>
</dd>
</dl>
## Examples
### Basic
A basic alert with an icon, title and description.
<Preview class="w-full max-w-md">
<div class="alert"><svg class="lucide lucide-circle-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="m9 12 2 2 4-4" /></svg>
<h2>Account updated successfully</h2>
<section>Your profile information has been saved. Changes will be reflected immediately.</section>
</div>
</Preview>
### Destructive
Use `data-variant="destructive"` to create a destructive alert.
<Preview class="w-full max-w-md">
<div class="alert" data-variant="destructive"><svg class="lucide lucide-circle-alert" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><line x1="12" x2="12" y1="8" y2="12" /><line x1="12" x2="12.01" y1="16" y2="16" /></svg>
<h2>Payment failed</h2>
<section>Your payment could not be processed. Please check your payment method and try again.</section>
</div>
</Preview>
### Action
Use `<footer>` to add a button or other action element to the alert.
<Preview class="w-full max-w-md">
<div class="alert">
<h2>Dark mode is now available</h2>
<section>Enable it under your profile settings to get started.</section>
<footer>
<button type="button" class="btn" data-size="xs">Enable</button>
</footer>
</div>
</Preview>
### Custom colors
You can customize the alert colors by adding custom classes such as `bg-amber-50 dark:bg-amber-950` to the alert.
<Preview class="w-full max-w-md">
<div class="alert border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-50"><svg class="lucide lucide-triangle-alert" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /><path d="M12 9v4" /><path d="M12 17h.01" /></svg>
<h2>Your subscription will expire in 3 days.</h2>
<section>Renew now to avoid service interruption or upgrade to a paid plan to continue using the service.</section>
</div>
</Preview>
### RTL
Add `dir="rtl"` to the alert or an ancestor.
<Preview class="w-full max-w-md">
<div class="grid items-start gap-4" dir="rtl">
<div class="alert"><svg class="lucide lucide-circle-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="m9 12 2 2 4-4" /></svg>
<h2>تم الدفع بنجاح</h2>
<section>تمت معالجة دفعتك البالغة 29.99 دولارًا. تم إرسال إيصال إلى عنوان بريدك الإلكتروني.</section>
</div>
<div class="alert"><svg class="lucide lucide-info" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M12 16v-4" /><path d="M12 8h.01" /></svg>
<h2>ميزة جديدة متاحة</h2>
<section>لقد أضفنا دعم الوضع الداكن. يمكنك تفعيله في إعدادات حسابك.</section>
</div>
</div>
</Preview>
-108
View File
@@ -1,108 +0,0 @@
---
layout: layouts/page.njk
title: Alert
description: Displays a callout for user attention.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Destructive
id: example-destructive
- label: No description
id: example-no-description
- label: No icon
id: example-no-icon
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}
<div class="grid w-full max-w-xl items-start gap-4">
<div class="alert">
{% lucide "circle-check" %}
<h2>Success! Your changes have been saved</h2>
<section>This is an alert with icon, title and description.</section>
</div>
<div class="alert">
{% lucide "popcorn" %}
<h2>This Alert has a title and an icon. No description.</h2>
<section>This is an alert with icon, title and description.</section>
</div>
<div class="alert-destructive">
{% lucide "circle-check" %}
<h2>Unable to process your payment.</h2>
<section>
<p>Please verify your billing information and try again.</p>
<ul class="list-inside list-disc text-sm">
<li>Check your card details</li>
<li>Ensure sufficient funds</li>
<li>Verify billing address</li>
</ul>
</section>
</div>
</div>
{% endset %}
{{ code_preview("alert", code | prettyHtml) }}
<h2 id="usage">Usage</h2>
<section class="prose">
<p>Use the <code>alert</code> or <code>alert-destructive</code>.</p>
</section>
{% set code_simple %}
<div class="alert">
{% lucide "circle-check" %}
<h2>Success! Your changes have been saved</h2>
<section>This is an alert with icon, title and description.</section>
</div>
{% endset %}
{{ code_block(code_simple | prettyHtml) }}
<section class="prose">
<p>The component has the following HTML structure:</p>
<dl>
<dt><code class="highlight language-html">&lt;div class="alert"&gt;</code></dt>
<dd>Main container. Use <code>alert</code> for default styling or <code>alert-destructive</code> for error states.
<dl>
<dt><code class="highlight language-html">&lt;svg&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The icon.</dd>
<dt><code class="highlight language-html">&lt;h2&gt;</code></dt>
<dd>The title.</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The description.</dd>
</dl>
</dd>
</dl>
</section>
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-destructive"><a href="#example-destructive">Destructive</a></h3>
{% set code %}<div class="alert-destructive">
{% lucide "circle-alert" %}
<h2>Something went wrong!</h2>
<section>Your session has expired. Please log in again.</section>
</div>{% endset %}
{{ code_preview("alert-destructive", code) }}
<h3 id="example-no-description"><a href="#example-no-description">No description</a></h3>
{% set code %}
<div class="alert">
{% lucide "shield-alert" %}
<h2>This is a very long alert title that demonstrates how the component handles extended text content and potentially wraps across multiple lines</h2>
</div>{% endset %}
{{ code_preview("alert-no-description", code) }}
<h3 id="example-no-icon"><a href="#example-no-icon">No icon</a></h3>
{% set code %}<div class="alert">
<h2>Success! Your changes have been saved</h2>
<section>This is an alert with icon, title and description.</section>
</div>{% endset %}
{{ code_preview("alert-no-icon", code) }}
+222
View File
@@ -0,0 +1,222 @@
# Avatar
<Preview>
<div class="flex flex-row flex-wrap items-center gap-12">
<span class="avatar">
<img src="https://github.com/shadcn.png" alt="@shadcn" />
<span>CN</span>
</span>
<span class="avatar rounded-lg">
<img src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
<span>ER</span>
</span>
<div class="avatar-group [&_.avatar]:grayscale">
<span class="avatar">
<img src="https://github.com/shadcn.png" alt="@shadcn" />
<span>CN</span>
</span>
<span class="avatar">
<img src="https://github.com/maxleiter.png" alt="@maxleiter" />
<span>LR</span>
</span>
<span class="avatar">
<img src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
<span>ER</span>
</span>
</div>
</div>
</Preview>
## Usage
<Steps>
<Step title="Include CSS">
Import Tailwind and one full Basecoat style bundle.
```css
@import "tailwindcss";
@import "basecoat-css/vega.css";
```
Or import only the base CSS, Avatar component CSS, and one style pack.
```css
@import "tailwindcss";
@import "basecoat-css/base.css";
@import "basecoat-css/components/avatar.css";
@import "basecoat-css/styles/vega.css";
```
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Add your avatar HTML">
Add `class="avatar"` to a small inline container. Add `data-size="sm"` or `data-size="lg"` when needed. Put the image first and a fallback `<span>` after it. Add `.avatar-badge` for a status badge or badge icon.
```html
<span class="avatar">
<img src="https://github.com/shadcn.png" alt="@shadcn" />
<span>CN</span>
</span>
```
</Step>
</Steps>
### HTML structure
<dl>
<dt><code>&lt;div class="avatar-group"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>
Optional parent that groups avatars with overlap.
<dl>
<dt><code>&lt;span class="avatar"&gt;</code></dt>
<dd>
Avatar root. Use it standalone or as a direct child of <code>.avatar-group</code>. Add <code>data-size="sm"</code> or <code>data-size="lg"</code> for alternate sizes.
<dl>
<dt><code>&lt;img&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>The avatar image. Provide meaningful <code>alt</code> text.</dd>
<dt><code>&lt;span&gt;</code></dt>
<dd>Fallback initials or icon.</dd>
<dt><code>&lt;span class="avatar-badge"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Status badge positioned at the block-end inline-end corner. It can be empty or contain an icon.</dd>
</dl>
</dd>
<dt><code>&lt;span data-count&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Count or icon indicator displayed at the end of the group.</dd>
</dl>
</dd>
</dl>
## Examples
### Basic
<Preview>
<span class="avatar">
<img src="https://github.com/shadcn.png" alt="@shadcn" />
<span>CN</span>
</span>
</Preview>
### Badge
<Preview>
<span class="avatar">
<img src="https://github.com/shadcn.png" alt="@shadcn" />
<span>CN</span>
<span class="avatar-badge bg-green-600 dark:bg-green-800"></span>
</span>
</Preview>
### Badge with icon
<Preview>
<span class="avatar">
<img src="https://github.com/pranathip.png" alt="@pranathip" />
<span>PP</span>
<span class="avatar-badge"><svg class="lucide lucide-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /><path d="M12 5v14" /></svg></span>
</span>
</Preview>
### Avatar group
<Preview>
<div class="avatar-group">
<span class="avatar"><img src="https://github.com/shadcn.png" alt="@shadcn" /><span>CN</span></span>
<span class="avatar"><img src="https://github.com/maxleiter.png" alt="@maxleiter" /><span>LR</span></span>
<span class="avatar"><img src="https://github.com/evilrabbit.png" alt="@evilrabbit" /><span>ER</span></span>
</div>
</Preview>
### Avatar group count
<Preview>
<div class="avatar-group">
<span class="avatar"><img src="https://github.com/shadcn.png" alt="@shadcn" /><span>CN</span></span>
<span class="avatar"><img src="https://github.com/maxleiter.png" alt="@maxleiter" /><span>LR</span></span>
<span class="avatar"><img src="https://github.com/evilrabbit.png" alt="@evilrabbit" /><span>ER</span></span>
<span data-count>+3</span>
</div>
</Preview>
### Avatar group with icon
<Preview>
<div class="avatar-group">
<span class="avatar"><img src="https://github.com/shadcn.png" alt="@shadcn" /><span>CN</span></span>
<span class="avatar"><img src="https://github.com/maxleiter.png" alt="@maxleiter" /><span>LR</span></span>
<span class="avatar"><img src="https://github.com/evilrabbit.png" alt="@evilrabbit" /><span>ER</span></span>
<span data-count aria-label="Additional avatars"><svg class="lucide lucide-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /><path d="M12 5v14" /></svg></span>
</div>
</Preview>
### Sizes
<Preview>
<div class="grid gap-2">
<div class="flex flex-wrap items-center gap-2">
<span class="avatar" data-size="sm">
<img src="https://github.com/shadcn.png" alt="@shadcn" />
<span>CN</span>
</span>
<span class="avatar">
<img src="https://github.com/shadcn.png" alt="@shadcn" />
<span>CN</span>
</span>
<span class="avatar" data-size="lg">
<img src="https://github.com/shadcn.png" alt="@shadcn" />
<span>CN</span>
</span>
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="avatar" data-size="sm"><span>CN</span></span>
<span class="avatar"><span>CN</span></span>
<span class="avatar" data-size="lg"><span>CN</span></span>
</div>
</div>
</Preview>
### Dropdown
<Preview>
<div class="dropdown-menu">
<button type="button" id="avatar-dropdown-trigger" aria-label="Open user menu" aria-haspopup="menu" aria-controls="avatar-dropdown-menu" aria-expanded="false" class="btn rounded-full" data-variant="ghost" data-size="icon">
<span class="avatar">
<img src="https://github.com/shadcn.png" alt="@shadcn" />
<span>CN</span>
</span>
</button>
<div data-popover aria-hidden="true" class="w-32">
<div role="menu" id="avatar-dropdown-menu" aria-labelledby="avatar-dropdown-trigger">
<div role="menuitem">Profile</div>
<div role="menuitem">Billing</div>
<div role="menuitem">Settings</div>
<hr role="separator" />
<div role="menuitem" data-variant="destructive">Log out</div>
</div>
</div>
</div>
</Preview>
### RTL
<Preview>
<div class="flex flex-row flex-wrap items-center gap-12" dir="rtl">
<span class="avatar">
<img src="https://github.com/shadcn.png" alt="@shadcn" />
<span>CN</span>
</span>
<span class="avatar rounded-lg">
<img src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
<span>ER</span>
</span>
<div class="avatar-group [&_.avatar]:grayscale">
<span class="avatar"><img src="https://github.com/shadcn.png" alt="@shadcn" /><span>CN</span></span>
<span class="avatar"><img src="https://github.com/maxleiter.png" alt="@maxleiter" /><span>LR</span></span>
<span class="avatar"><img src="https://github.com/evilrabbit.png" alt="@evilrabbit" /><span>ER</span></span>
</div>
</div>
</Preview>
-40
View File
@@ -1,40 +0,0 @@
---
layout: layouts/page.njk
title: Avatar
description: An image element with a fallback for representing the user.
toc:
- label: Usage
id: usage
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated Avatar component in Basecoat.</h2>
</div>
{% set code %}
<div class="flex flex-row flex-wrap items-center gap-12">
<img class="size-8 shrink-0 object-cover rounded-full" alt="@hunvreus" src="https://github.com/hunvreus.png">
<img class="size-8 shrink-0 object-cover rounded-lg" alt="@shadcn" src="https://github.com/shadcn.png">
<div class="flex -space-x-2 [&_img]:ring-background [&_img]:ring-2 [&_img]:grayscale [&_img]:size-8 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
</div>
{% endset %}
{{ code_preview("avatar", code | prettyHtml, "w-full max-w-md") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Avatars are just <code>&lt;img&gt;</code> elements styled with Tailwind utility classes.</p>
</section>
{% set code_simple %}
<img class="size-8 shrink-0 object-cover rounded-full" alt="@hunvreus" src="https://github.com/hunvreus.png">
{% endset %}
{{ code_block(code_simple | prettyHtml, "html") }}
+140
View File
@@ -0,0 +1,140 @@
# Badge
<Preview>
<div class="flex flex-wrap gap-2">
<span class="badge">Badge</span>
<span class="badge" data-variant="secondary">Secondary</span>
<span class="badge" data-variant="destructive">Destructive</span>
<span class="badge" data-variant="outline">Outline</span>
</div>
</Preview>
## Usage
<Steps>
<Step title="Include CSS">
Import Tailwind and one full Basecoat style bundle.
```css
@import "tailwindcss";
@import "basecoat-css/vega.css";
```
Or import only the base CSS, Badge component CSS, and one style pack.
```css
@import "tailwindcss";
@import "basecoat-css/base.css";
@import "basecoat-css/components/badge.css";
@import "basecoat-css/styles/vega.css";
```
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Add your badge HTML">
Add `class="badge"` to an inline element. Add `data-variant` when the badge is not the default primary variant.
Inline SVG icons are spaced automatically by the badge gap.
```html
<span class="badge">Default</span>
<span class="badge" data-variant="secondary">Secondary</span>
<span class="badge" data-variant="destructive">Destructive</span>
<span class="badge" data-variant="outline">Outline</span>
<span class="badge" data-variant="ghost">Ghost</span>
```
</Step>
</Steps>
Badges support right-to-left layouts through native text flow. Flip directional icons explicitly.
## Examples
### Variants
Use `data-variant` to change the badge variant.
<Preview>
<div class="flex flex-wrap gap-2">
<span class="badge">Default</span>
<span class="badge" data-variant="secondary">Secondary</span>
<span class="badge" data-variant="destructive">Destructive</span>
<span class="badge" data-variant="outline">Outline</span>
<span class="badge" data-variant="ghost">Ghost</span>
</div>
</Preview>
### With icon
Use `data-icon="inline-start"` or `data-icon="inline-end"` on the icon for icon-aware spacing.
<Preview>
<div class="flex flex-wrap gap-2">
<span class="badge"><svg data-icon="inline-start" class="lucide lucide-badge-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z" /><path d="m9 12 2 2 4-4" /></svg>
Verified
</span>
<span class="badge" data-variant="secondary">
Bookmark
<svg data-icon="inline-end" class="lucide lucide-bookmark" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" /></svg>
</span>
</div>
</Preview>
### With spinner
Use an inline loading icon with `data-icon="inline-start"` or `data-icon="inline-end"`.
<Preview>
<div class="flex flex-wrap gap-2">
<span class="badge" data-variant="destructive"><svg aria-label="Loading" role="status" data-icon="inline-start" class="animate-spin lucide lucide-loader-circle" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
Deleting
</span>
<span class="badge" data-variant="secondary">
Generating
<svg aria-label="Loading" role="status" data-icon="inline-end" class="animate-spin lucide lucide-loader-circle" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
</span>
</div>
</Preview>
### Link
<Preview>
<a href="#" class="badge">
Open Link
<svg data-icon="inline-end" class="lucide lucide-arrow-up-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h10v10" /><path d="M7 17 17 7" /></svg>
</a>
</Preview>
### Custom colors
<Preview>
<div class="flex flex-wrap gap-2">
<span class="badge bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-300">Blue</span>
<span class="badge bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300">Green</span>
<span class="badge bg-sky-50 text-sky-700 dark:bg-sky-950 dark:text-sky-300">Sky</span>
<span class="badge bg-purple-50 text-purple-700 dark:bg-purple-950 dark:text-purple-300">Purple</span>
<span class="badge bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300">Red</span>
</div>
</Preview>
### RTL
<Preview>
<div dir="rtl" class="flex flex-wrap gap-2">
<span class="badge">شارة</span>
<span class="badge" data-variant="secondary">ثانوي</span>
<span class="badge" data-variant="destructive">مدمر</span>
<span class="badge" data-variant="outline">مخطط</span>
<span class="badge"><svg data-icon="inline-start" class="lucide lucide-badge-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z" /><path d="m9 12 2 2 4-4" /></svg>
متحقق
</span>
<span class="badge" data-variant="secondary">
إشارة مرجعية
<svg data-icon="inline-end" class="lucide lucide-bookmark" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" /></svg>
</span>
</div>
</Preview>
-93
View File
@@ -1,93 +0,0 @@
---
layout: layouts/page.njk
title: Badge
description: Displays a badge or a component that looks like a badge.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Primary
id: example-primary
- label: Secondary
id: example-secondary
- label: Destructive
id: example-destructive
- label: Outline
id: example-outline
- label: With icon
id: example-with-icon
- label: Link
id: example-link
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}
<div class="flex flex-col items-center gap-2">
<div class="flex w-full flex-wrap gap-2">
<span class="badge">Badge</span>
<span class="badge-secondary">Secondary</span>
<span class="badge-destructive">Destructive</span>
<span class="badge-outline">Outline</span>
</div>
<div class="flex w-full flex-wrap gap-2">
<span class="badge-secondary bg-blue-500 text-white dark:bg-blue-600">
{% lucide "badge-check" %}
Verified
</span>
<span class="badge rounded-full h-5 min-w-5 px-1 font-mono tabular-nums">8</span>
<span class="badge-destructive rounded-full h-5 min-w-5 px-1 font-mono tabular-nums">99</span>
<span class="badge-outline rounded-full h-5 min-w-5 px-1 font-mono tabular-nums">20+</span>
</div>
</div>
{% endset %}
{{ code_preview("badge", code) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Use one of the badge classes: <code>badge</code>, <code>badge-primary</code>, <code>badge-secondary</code>, <code>badge-destructive</code> or <code>badge-outline</code>.
</section>
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-primary"><a href="#example-primary">Primary</a></h3>
{% set code %}<span class="badge">Primary</span>{% endset %}
{{ code_preview("badge-primary", code) }}
<h3 id="example-secondary"><a href="#example-secondary">Secondary</a></h3>
{% set code %}<span class="badge-secondary">Secondary</span>{% endset %}
{{ code_preview("badge-secondary", code) }}
<h3 id="example-destructive"><a href="#example-destructive">Destructive</a></h3>
{% set code %}<span class="badge-destructive">Destructive</span>{% endset %}
{{ code_preview("badge-destructive", code) }}
<h3 id="example-outline"><a href="#example-outline">Outline</a></h3>
{% set code %}<span class="badge-outline">Outline</span>{% endset %}
{{ code_preview("badge-outline", code) }}
<h3 id="example-with-icon"><a href="#example-with-icon">With icon</a></h3>
{% set code %}<span class="badge-destructive">
{% lucide "circle-alert" %}
With icon
</span>{% endset %}
{{ code_preview("badge-with-icon", code) }}
<h3 id="example-link"><a href="#example-link">Link</a></h3>
{% set code %}<a href="#" class="badge-outline">
Link
{% lucide "arrow-right" %}
</a>{% endset %}
{{ code_preview("badge-link", code) }}
+154
View File
@@ -0,0 +1,154 @@
# Breadcrumb
<Preview>
<nav class="breadcrumb" aria-label="Breadcrumb">
<ol>
<li><a href="#">Home</a></li>
<li aria-hidden="true"><svg data-rtl-flip class="lucide lucide-chevron-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg></li>
<li><a href="#">Components</a></li>
<li aria-hidden="true"><svg data-rtl-flip class="lucide lucide-chevron-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg></li>
<li><span aria-current="page">Breadcrumb</span></li>
</ol>
</nav>
</Preview>
## Usage
<Steps>
<Step title="Include CSS">
Import Tailwind and one full Basecoat style bundle.
```css
@import "tailwindcss";
@import "basecoat-css/vega.css";
```
Or import only the base CSS, Breadcrumb component CSS, and one style pack.
```css
@import "tailwindcss";
@import "basecoat-css/base.css";
@import "basecoat-css/components/breadcrumb.css";
@import "basecoat-css/styles/vega.css";
```
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Add your breadcrumb HTML">
Use a navigation landmark with an ordered list. Mark the current page with `aria-current="page"`.
```html
<nav class="breadcrumb" aria-label="Breadcrumb">
<ol>
<li><a href="#">Home</a></li>
<li aria-hidden="true"><svg data-rtl-flip class="lucide lucide-chevron-right" ...></svg></li>
<li><span aria-current="page">Breadcrumb</span></li>
</ol>
</nav>
```
</Step>
</Steps>
## HTML structure
- `nav.breadcrumb` is the root landmark and should include `aria-label="Breadcrumb"`.
- `ol` contains the breadcrumb items in hierarchy order.
- `li > a` represents navigable ancestors.
- `li[aria-hidden="true"]` represents a visual separator.
- `[aria-current="page"]` represents the current page.
- `li > span[aria-hidden="true"]` represents a collapsed ellipsis.
## Examples
### Basic
<Preview>
<nav class="breadcrumb" aria-label="Breadcrumb">
<ol>
<li><a href="#">Home</a></li>
<li aria-hidden="true"><svg data-rtl-flip class="lucide lucide-chevron-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg></li>
<li><a href="#">Components</a></li>
<li aria-hidden="true"><svg data-rtl-flip class="lucide lucide-chevron-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg></li>
<li><span aria-current="page">Breadcrumb</span></li>
</ol>
</nav>
</Preview>
### Custom separator
<Preview>
<nav class="breadcrumb" aria-label="Breadcrumb">
<ol>
<li><a href="#">Home</a></li>
<li aria-hidden="true"><svg class="lucide lucide-dot" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12.1" cy="12.1" r="1" /></svg></li>
<li><a href="#">Components</a></li>
<li aria-hidden="true"><svg class="lucide lucide-dot" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12.1" cy="12.1" r="1" /></svg></li>
<li><span aria-current="page">Breadcrumb</span></li>
</ol>
</nav>
</Preview>
### Dropdown
<Preview>
<nav class="breadcrumb" aria-label="Breadcrumb">
<ol>
<li><a href="#">Home</a></li>
<li aria-hidden="true"><svg data-rtl-flip class="lucide lucide-chevron-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg></li>
<li>
<div id="breadcrumb-dropdown" class="dropdown-menu">
<button type="button" id="breadcrumb-dropdown-trigger" class="btn" data-variant="ghost" data-size="icon-sm" aria-haspopup="menu" aria-controls="breadcrumb-dropdown-menu" aria-expanded="false"><svg class="lucide lucide-ellipsis" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" /></svg>
<span class="sr-only">Toggle menu</span>
</button>
<div id="breadcrumb-dropdown-popover" data-popover aria-hidden="true">
<div role="menu" id="breadcrumb-dropdown-menu" aria-labelledby="breadcrumb-dropdown-trigger">
<nav role="menu">
<button type="button" role="menuitem">Documentation</button>
<button type="button" role="menuitem">Themes</button>
<button type="button" role="menuitem">GitHub</button>
</nav>
</div>
</div>
</div>
</li>
<li aria-hidden="true"><svg data-rtl-flip class="lucide lucide-chevron-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg></li>
<li><a href="#">Components</a></li>
<li aria-hidden="true"><svg data-rtl-flip class="lucide lucide-chevron-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg></li>
<li><span aria-current="page">Breadcrumb</span></li>
</ol>
</nav>
</Preview>
### Collapsed
<Preview>
<nav class="breadcrumb" aria-label="Breadcrumb">
<ol>
<li><a href="#">Home</a></li>
<li aria-hidden="true"><svg data-rtl-flip class="lucide lucide-chevron-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg></li>
<li> <span aria-hidden="true"><svg class="lucide lucide-ellipsis" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" /></svg></span></li>
<li aria-hidden="true"><svg data-rtl-flip class="lucide lucide-chevron-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg></li>
<li><a href="#">Components</a></li>
<li aria-hidden="true"><svg data-rtl-flip class="lucide lucide-chevron-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg></li>
<li><span aria-current="page">Breadcrumb</span></li>
</ol>
</nav>
</Preview>
### RTL
<Preview>
<nav class="breadcrumb" aria-label="Breadcrumb" dir="rtl">
<ol>
<li><a href="#">الرئيسية</a></li>
<li aria-hidden="true"><svg data-rtl-flip class="lucide lucide-chevron-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg></li>
<li><a href="#">المكونات</a></li>
<li aria-hidden="true"><svg data-rtl-flip class="lucide lucide-chevron-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6" /></svg></li>
<li><span aria-current="page">مسار التنقل</span></li>
</ol>
</nav>
</Preview>
-71
View File
@@ -1,71 +0,0 @@
---
layout: layouts/page.njk
title: Breadcrumb
description: Displays the path to the current resource using a hierarchy of links.
toc:
- label: Usage
id: usage
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "dropdown-menu.njk" import dropdown_menu %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated Breadcrumb component in Basecoat.</h2>
</div>
{% set code_html %}
<ol class="text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5">
<li class="inline-flex items-center gap-1.5">
<a href="#" class="hover:text-foreground transition-colors">Home</a>
</li>
<li>{% lucide "chevron-right", { "class": "size-3.5" } %}</li>
<li class="inline-flex items-center gap-1.5">
{% set trigger %}
{% lucide "ellipsis" %}
{% endset %}
{% call dropdown_menu(
id="demo-breadcrumb-menu",
trigger=trigger,
trigger_attrs={"class": "flex size-9 items-center justify-center h-4 w-4 hover:text-foreground cursor-pointer"}
) %}
<nav role="menu">
<button type="button" role="menuitem">Documentation</button>
<button type="button" role="menuitem">Themes</button>
<button type="button" role="menuitem">GitHub</button>
</nav>
{% endcall %}
</li>
<li>{% lucide "chevron-right", { "class": "size-3.5" } %}</li>
<li class="inline-flex items-center gap-1.5">
<a href="#" class="hover:text-foreground transition-colors">Components</a>
</li>
<li>{% lucide "chevron-right", { "class": "size-3.5" } %}</li>
<li class="inline-flex items-center gap-1.5">
<span class="text-foreground font-normal">Breadcrumb</span>
</li>
</ol>
{% endset %}
{{ code_preview("dropdown-menu", code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>You can style an ordered list of links combined with chevron icons as show below. The example above demonstrates a more complex use case using the <a href="/components/dropdown-menu">Dropdown Menu</a> component.</p>
</section>
{% set code_simple %}
<ol class="text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5">
<li class="inline-flex items-center gap-1.5">
<a href="#" class="hover:text-foreground transition-colors">Home</a>
</li>
<li>{% lucide "chevron-right", { "class": "size-3.5" } %}</li>
<li class="inline-flex items-center gap-1.5">
<a href="#" class="hover:text-foreground transition-colors">Components</a>
</li>
</ol>
{% endset %}
{{ code_block(code_simple | prettyHtml, "html") }}
+307
View File
@@ -0,0 +1,307 @@
# Button Group
<Preview>
<div class="flex w-fit items-stretch gap-2">
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Go Back"><svg class="lucide lucide-arrow-left" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg></button>
<div role="group" class="button-group">
<button type="button" class="btn" data-variant="outline">Archive</button>
<button type="button" class="btn" data-variant="outline">Report</button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn" data-variant="outline">Snooze</button>
<div id="dropdown-menu-277805" class="dropdown-menu">
<button type="button" id="dropdown-menu-277805-trigger" aria-label="More archive options" aria-haspopup="menu" aria-controls="dropdown-menu-277805-menu" aria-expanded="false" class="btn" data-variant="outline" data-size="icon"><svg class="lucide lucide-ellipsis" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" /></svg></button>
<div id="dropdown-menu-277805-popover" data-popover aria-hidden="true" data-align="end">
<div role="menu" id="dropdown-menu-277805-menu" aria-labelledby="dropdown-menu-277805-trigger">
<div role="menuitem"><svg class="lucide lucide-mail-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8" /><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" /><path d="m16 19 2 2 4-4" /></svg>
Mark as Read
</div>
<div role="menuitem"><svg class="lucide lucide-archive" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="5" x="2" y="3" rx="1" /><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8" /><path d="M10 12h4" /></svg>
Archive
</div>
<hr role="separator" />
<div role="menuitem"><svg class="lucide lucide-clock" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" /></svg>
Snooze
</div>
<div role="menuitem"><svg class="lucide lucide-calendar" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4" /><path d="M16 2v4" /><rect width="18" height="18" x="3" y="4" rx="2" /><path d="M3 10h18" /></svg>
Add to Calendar
</div>
<div role="menuitem"><svg class="lucide lucide-list-filter-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5H2" /><path d="M6 12h12" /><path d="M9 19h6" /><path d="M16 5h6" /><path d="M19 8V2" /></svg>
Add to List
</div>
<hr role="separator" />
<div role="menuitem" class="text-destructive hover:bg-destructive/10 dark:hover:bg-destructive/20 focus:bg-destructive/10 dark:focus:bg-destructive/20 focus:text-destructive [&_svg]:!text-destructive"><svg class="lucide lucide-trash-2" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /><path d="M14 11v6" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /><path d="M3 6h18" /><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></svg>
Trash
</div>
</div>
</div>
</div>
</div>
</div>
</Preview>
## Usage
<Steps>
<Step title="Include CSS">
Import Tailwind and one full Basecoat style bundle.
```css
@import "tailwindcss";
@import "basecoat-css/vega.css";
```
Or import only the base CSS, the component CSS files it composes, and one style pack.
```css
@import "tailwindcss";
@import "basecoat-css/base.css";
@import "basecoat-css/components/button-group.css";
@import "basecoat-css/components/button.css";
@import "basecoat-css/styles/vega.css";
```
Button Group composes Button styles for the child controls.
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Add your button group HTML">
Wrap related controls in a `button-group`.
```html
<div role="group" aria-label="Message actions" class="button-group">
<button type="button" class="btn" data-variant="outline">Archive</button>
<button type="button" class="btn" data-variant="outline">Report</button>
</div>
```
</Step>
</Steps>
Use `data-orientation="vertical"` to stack items. Use `<hr role="separator" />` between items when you need a visible divider, such as a split action. Use direct child `<span>`, `<label>`, or `<output>` elements for non-interactive text segments. Direct child `<div>` elements are left unstyled for custom layouts.
## Examples
### Orientation
Use `data-orientation="vertical"` to stack buttons vertically.
<Preview>
<div role="group" aria-label="Media controls" class="button-group h-fit" data-orientation="vertical">
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Zoom in"><svg class="lucide lucide-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /><path d="M12 5v14" /></svg></button>
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Zoom out"><svg class="lucide lucide-minus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /></svg></button>
</div>
</Preview>
### Size
<Preview>
<div class="flex flex-col items-start gap-8">
<div role="group" class="button-group">
<button type="button" class="btn" data-variant="outline" data-size="sm">Small</button>
<button type="button" class="btn" data-variant="outline" data-size="sm">Button</button>
<button type="button" class="btn" data-variant="outline" data-size="sm">Group</button>
<button type="button" class="btn" data-variant="outline" data-size="icon-sm" aria-label="Add small item"><svg class="lucide lucide-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /><path d="M12 5v14" /></svg></button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn" data-variant="outline">Default</button>
<button type="button" class="btn" data-variant="outline">Button</button>
<button type="button" class="btn" data-variant="outline">Group</button>
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Add item"><svg class="lucide lucide-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /><path d="M12 5v14" /></svg></button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn" data-variant="outline" data-size="lg">Large</button>
<button type="button" class="btn" data-variant="outline" data-size="lg">Button</button>
<button type="button" class="btn" data-variant="outline" data-size="lg">Group</button>
<button type="button" class="btn" data-variant="outline" data-size="icon-lg" aria-label="Add large item"><svg class="lucide lucide-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /><path d="M12 5v14" /></svg></button>
</div>
</div>
</Preview>
### Nested
Nest `button-group` components to keep controls joined within each group while adding spacing between groups.
<Preview>
<div role="group" aria-label="Pagination controls" class="button-group">
<div role="group" class="button-group">
<button type="button" class="btn" data-variant="outline" data-size="sm">1</button>
<button type="button" class="btn" data-variant="outline" data-size="sm">2</button>
<button type="button" class="btn" data-variant="outline" data-size="sm">3</button>
<button type="button" class="btn" data-variant="outline" data-size="sm">4</button>
<button type="button" class="btn" data-variant="outline" data-size="sm">5</button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn" data-variant="outline" data-size="icon-sm" aria-label="Previous page"><svg class="lucide lucide-arrow-left" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg></button>
<button type="button" class="btn" data-variant="outline" data-size="icon-sm" aria-label="Next page"><svg class="lucide lucide-arrow-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg></button>
</div>
</div>
</Preview>
### Separator
Use `<hr role="separator" />` to visually divide buttons within a group.
<Preview>
<div role="group" class="button-group">
<button type="button" class="btn" data-variant="secondary" data-size="sm">Copy</button>
<hr role="separator" />
<button type="button" class="btn" data-variant="secondary" data-size="sm">Paste</button>
</div>
</Preview>
### Split
Use a separator to create a split action.
<Preview>
<div role="group" class="button-group">
<button type="button" class="btn" data-variant="secondary">Button</button>
<hr role="separator" />
<button type="button" class="btn" data-variant="secondary" data-size="icon" aria-label="Add"><svg class="lucide lucide-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /><path d="M12 5v14" /></svg></button>
</div>
</Preview>
### Input
<Preview>
<div role="group" aria-label="Search" class="button-group">
<input type="text" class="input" placeholder="Search..." />
<button type="button" class="btn" data-variant="outline" aria-label="Search"><svg class="lucide lucide-search" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /><circle cx="11" cy="11" r="8" /></svg></button>
</div>
</Preview>
### Input group
<Preview>
<div role="group" class="button-group">
<div role="group" class="button-group">
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Add attachment"><svg class="lucide lucide-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /><path d="M12 5v14" /></svg></button>
</div>
<div role="group" class="button-group">
<div class="input-group">
<input type="text" placeholder="Send a message..." />
<div role="group" data-align="inline-end">
<button type="button" class="btn" data-variant="ghost" data-size="icon-xs" aria-label="Voice mode" aria-pressed="false"><svg class="lucide lucide-audio-lines" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 10v3" /><path d="M6 6v11" /><path d="M10 3v18" /><path d="M14 8v7" /><path d="M18 5v13" /><path d="M22 10v3" /></svg></button>
</div>
</div>
</div>
</div>
</Preview>
### Dropdown menu
<Preview>
<div role="group" class="button-group">
<button type="button" class="btn" data-variant="outline">Follow</button>
<div id="dropdown-menu-609880" class="dropdown-menu">
<button type="button" id="dropdown-menu-609880-trigger" aria-label="More follow options" aria-haspopup="menu" aria-controls="dropdown-menu-609880-menu" aria-expanded="false" class="btn" data-variant="outline" data-size="icon"><svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg></button>
<div id="dropdown-menu-609880-popover" data-popover aria-hidden="true" data-align="end">
<div role="menu" id="dropdown-menu-609880-menu" aria-labelledby="dropdown-menu-609880-trigger">
<div role="menuitem"><svg class="lucide lucide-volume-off" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 9a5 5 0 0 1 .95 2.293" /><path d="M19.364 5.636a9 9 0 0 1 1.889 9.96" /><path d="m2 2 20 20" /><path d="m7 7-.587.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298V11" /><path d="M9.828 4.172A.686.686 0 0 1 11 4.657v.686" /></svg>
Mute Conversation
</div>
<div role="menuitem"><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
Mark as Read
</div>
<div role="menuitem"><svg class="lucide lucide-triangle-alert" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /><path d="M12 9v4" /><path d="M12 17h.01" /></svg>
Report Conversation
</div>
<div role="menuitem"><svg class="lucide lucide-user-round-x" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 21a8 8 0 0 1 11.873-7" /><circle cx="10" cy="8" r="5" /><path d="m17 17 5 5" /><path d="m22 17-5 5" /></svg>
Block User
</div>
<div role="menuitem"><svg class="lucide lucide-share" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v13" /><path d="m16 6-4-4-4 4" /><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" /></svg>
Share Conversation
</div>
<div role="menuitem"><svg class="lucide lucide-copy" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></svg>
Copy Conversation
</div>
<hr role="separator" />
<div role="menuitem" class="text-destructive hover:bg-destructive/10 dark:hover:bg-destructive/20 focus:bg-destructive/10 dark:focus:bg-destructive/20 focus:text-destructive [&_svg]:!text-destructive"><svg class="lucide lucide-trash-2" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /><path d="M14 11v6" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /><path d="M3 6h18" /><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></svg>
Delete Conversation
</div>
</div>
</div>
</div>
</div>
</Preview>
### Select
<Preview>
<div class="flex items-stretch gap-2">
<div role="group" aria-label="Currency amount" class="button-group">
<div id="select-797405" class="select">
<button type="button" class="[&amp;_[data-name]]:hidden" id="select-797405-trigger" aria-haspopup="listbox" aria-expanded="false" aria-controls="select-797405-listbox">
<span class="truncate">$</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down-icon lucide-chevron-down text-muted-foreground opacity-50 shrink-0"><path d="m6 9 6 6 6-6" /></svg>
</button>
<div id="select-797405-popover" data-popover aria-hidden="true">
<div role="listbox" id="select-797405-listbox" aria-orientation="vertical" aria-labelledby="select-797405-trigger">
<div role="option" data-value="$" data-label="$" aria-selected="true">
$
<span class="text-muted-foreground">US Dollar</span>
</div>
<div role="option" data-value="€" data-label="€">
€
<span class="text-muted-foreground">Euro</span>
</div>
<div role="option" data-value="£" data-label="£">
£
<span class="text-muted-foreground">British Pound</span>
</div>
</div>
</div>
<input type="hidden" name="select-797405-value" value="$" />
</div>
<input type="text" class="input" placeholder="10.00" />
</div>
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Submit amount"><svg class="lucide lucide-arrow-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg></button>
</div>
</Preview>
### Popover
<Preview>
<div role="group" class="button-group">
<button type="button" class="btn" data-variant="outline"><svg class="lucide lucide-bot" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8" /><rect width="16" height="12" x="4" y="8" rx="2" /><path d="M2 14h2" /><path d="M20 14h2" /><path d="M15 13v2" /><path d="M9 13v2" /></svg>
Copilot
</button>
<div id="popover-788465" class="popover">
<button id="popover-788465-trigger" type="button" aria-label="Open Popover" aria-expanded="false" aria-controls="popover-788465-popover" class="btn" data-variant="outline" data-size="icon"><svg class="lucide lucide-chevron-down" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg></button>
<div id="popover-788465-popover" data-popover aria-hidden="true" class="w-72 text-sm" data-align="end">
<div>
<p class="font-medium">Start a new task with Copilot</p>
<p class="text-muted-foreground">Describe your task in natural language.</p>
</div>
<textarea class="textarea min-h-16 w-full resize-none" placeholder="I need to..."></textarea>
<p class="text-muted-foreground text-xs">Copilot will open a pull request for review.</p>
</div>
</div>
</div>
</Preview>
### RTL
Add `dir="rtl"` to the group or an ancestor. Flip directional icons explicitly.
<Preview>
<div dir="rtl">
<div role="group" aria-label="إجراءات الرسالة" class="button-group">
<div role="group" class="button-group">
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="رجوع"><svg class="rtl:rotate-180 lucide lucide-arrow-left" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg></button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn" data-variant="outline">أرشفة</button>
<button type="button" class="btn" data-variant="outline">تقرير</button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn" data-variant="outline">تأجيل</button>
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="المزيد من الخيارات"><svg class="lucide lucide-ellipsis" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" /></svg></button>
</div>
</div>
</div>
</Preview>
-290
View File
@@ -1,290 +0,0 @@
---
layout: layouts/page.njk
title: Button Group
description: A container that groups related buttons together with consistent styling.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Orientation
id: example-orientation
- label: Nested
id: example-nested
- label: Separator
id: example-separator
- label: Input
id: example-input
- label: Dropdown Menu
id: example-dropdown-menu
- label: Select
id: example-select
- label: Popover
id: example-popover
---
{% from "dropdown-menu.njk" import dropdown_menu %}
{% from "popover.njk" import popover %}
{% from "select.njk" import select %}
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code_default %}
<div class="flex w-fit items-stretch gap-2">
<button type="button" class="btn-icon-outline" aria-label="Go Back">{% lucide "arrow-left" %}</button>
<div role="group" class="button-group">
<button type="button" class="btn-outline">Archive</button>
<button type="button" class="btn-outline">Report</button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn-outline">Snooze</button>
{% set trigger %}
{% lucide "ellipsis" %}
{% endset %}
{% call dropdown_menu(
trigger=trigger,
trigger_attrs={"class": "btn-icon-outline"},
popover_attrs={"data-align": "end"}
) %}
<div role="menuitem">
{% lucide "mail-check" %}
Mark as Read
</div>
<div role="menuitem">
{% lucide "archive" %}
Archive
</div>
<hr role="separator">
<div role="menuitem">
{% lucide "clock" %}
Snooze
</div>
<div role="menuitem">
{% lucide "calendar" %}
Add to Calendar
</div>
<div role="menuitem">
{% lucide "list-filter-plus" %}
Add to List
</div>
<hr role="separator">
<div role="menuitem" class="text-destructive hover:bg-destructive/10 dark:hover:bg-destructive/20 focus:bg-destructive/10 dark:focus:bg-destructive/20 focus:text-destructive [&_svg]:!text-destructive">
{% lucide "trash-2" %}
Trash
</div>
{% endcall %}
</div>
{% endset %}
{{ code_preview("button-group-default", code_default | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<p class="prose">Simply wrap buttons you want to group together in a <code>&lt;div role="group" class="button-group"&gt;</code> element. You can also use the <code>data-orientation="vertical"</code> attribute to stack buttons vertically. You can also group buttons with <a href="#example-dropdown-menu">dropdown menus</a> or <a href="#example-popover">popovers</a> (provided the trigger is a button). Finally, you can use a <a href="#example-separator">separator</a> to visually divide buttons within a group (for secondary buttons for example).</p>
{% set code_simple %}
<div role="group" class="button-group">
<button type="button" class="btn-outline">Archive</button>
<button type="button" class="btn-outline">Report</button>
</div>
{% endset %}
{{ code_block(code_simple | prettyHtml, "html") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-orientation"><a href="#example-orientation">Orientation</a></h3>
<section class="prose">
<p>Use <code>data-orientation="vertical"</code> to stack buttons vertically.</p>
</section>
{% set code_orientation %}
<div role="group" class="button-group" data-orientation="vertical">
<button type="button" class="btn-icon-outline" aria-label="Zoom in">
{% lucide "plus" %}
</button>
<button type="button" class="btn-icon-outline" aria-label="Zoom out">
{% lucide "minus" %}
</button>
</div>
{% endset %}
{{ code_preview("button-group-orientation", code_orientation | prettyHtml) }}
<h3 id="example-size"><a href="#example-size">Size</a></h3>
{% set code_size %}
<div class="flex flex-col items-start gap-8">
<div role="group" class="button-group">
<button type="button" class="btn-sm-outline">Small</button>
<button type="button" class="btn-sm-outline">Button</button>
<button type="button" class="btn-sm-outline">Group</button>
<button type="button" class="btn-sm-icon-outline">{% lucide "plus" %}</button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn-outline">Default</button>
<button type="button" class="btn-outline">Button</button>
<button type="button" class="btn-outline">Group</button>
<button type="button" class="btn-icon-outline">{% lucide "plus" %}</button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn-lg-outline">Large</button>
<button type="button" class="btn-lg-outline">Button</button>
<button type="button" class="btn-lg-outline">Group</button>
<button type="button" class="btn-lg-icon-outline">{% lucide "plus" %}</button>
</div>
</div>
{% endset %}
{{ code_preview("button-group-size", code_size | prettyHtml) }}
<h3 id="example-nested"><a href="#example-nested">Nested</a></h3>
{% set code_nested %}
<div role="group" aria-label="Pagination controls" class="flex w-fit items-stretch gap-2">
<div role="group" class="button-group">
<button type="button" class="btn-sm-outline">1</button>
<button type="button" class="btn-sm-outline">2</button>
<button type="button" class="btn-sm-outline">3</button>
<button type="button" class="btn-sm-outline">4</button>
<button type="button" class="btn-sm-outline">5</button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn-sm-icon-outline" aria-label="Previous page">
{% lucide "chevron-left" %}
</button>
<button type="button" class="btn-sm-icon-outline" aria-label="Next page">
{% lucide "chevron-right" %}
</button>
</div>
</div>
{% endset %}
{{ code_preview("button-group-nested", code_nested | prettyHtml) }}
<h3 id="example-separator"><a href="#example-separator">Separator</a></h3>
<section class="prose">
<p>Use <code>&lt;hr role="separator"&gt;</code> to visually divide buttons within a group.</p>
</section>
{% set code_separator %}
<div role="group" class="button-group">
<button type="button" class="btn-secondary">Copy</button>
<hr role="separator">
<button type="button" class="btn-secondary">Paste</button>
</div>
{% endset %}
{{ code_preview("button-group-separator", code_separator | prettyHtml) }}
<h3 id="example-input"><a href="#example-input">Input</a></h3>
{% set code_input %}
<div class="button-group">
<input type="text" class="input" placeholder="Search...">
<button type="button" class="btn-icon-outline">{% lucide "search" %}</button>
</div>
{% endset %}
{{ code_preview("button-group-input", code_input | prettyHtml) }}
<h3 id="example-dropdown-menu"><a href="#example-dropdown-menu">Dropdown Menu</a></h3>
{% set code_dropdown_menu %}
<div role="group" class="button-group">
<button type="button" class="btn-outline">Follow</button>
{% set trigger %}
{% lucide "chevron-down" %}
{% endset %}
{% call dropdown_menu(
trigger=trigger,
trigger_attrs={"class": "btn-icon-outline"},
popover_attrs={"data-align": "end"}
) %}
<div role="menuitem">
{% lucide "volume-off" %}
Mute Conversation
</div>
<div role="menuitem">
{% lucide "check" %}
Mark as Read
</div>
<div role="menuitem">
{% lucide "triangle-alert" %}
Report Conversation
</div>
<div role="menuitem">
{% lucide "user-round-x" %}
Block User
</div>
<div role="menuitem">
{% lucide "share" %}
Share Conversation
</div>
<div role="menuitem">
{% lucide "copy" %}
Copy Conversation
</div>
<hr role="separator">
<div role="menuitem" class="text-destructive hover:bg-destructive/10 dark:hover:bg-destructive/20 focus:bg-destructive/10 dark:focus:bg-destructive/20 focus:text-destructive [&_svg]:!text-destructive">
{% lucide "trash-2" %}
Delete Conversation
</div>
{% endcall %}
</div>
{% endset %}
{{ code_preview("button-group-dropdown-menu", code_dropdown_menu | prettyHtml) }}
<h3 id="example-select"><a href="#example-select">Select</a></h3>
{% set code_select %}
<div class="flex w-fit items-stretch gap-2">
<div class="button-group">
{% call select(trigger_attrs={"class": "[&_[data-name]]:hidden"}) %}
<div role="option" data-value="$">$ <span class="text-muted-foreground" data-name>US Dollar</span></div>
<div role="option" data-value="€">€ <span class="text-muted-foreground" data-name>Euro</span></div>
<div role="option" data-value="£">£ <span class="text-muted-foreground" data-name>British Pound</span></div>
{% endcall %}
<input type="text" class="input" placeholder="10.00">
</div>
<button type="button" class="btn-icon-outline">{% lucide "arrow-right" %}</button>
</div>
{% endset %}
{{ code_preview("button-group-select", code_select | prettyHtml) }}
<h3 id="example-popover"><a href="#example-popover">Popover</a></h3>
{% set code_popover %}
<div role="group" class="button-group">
<button type="button" class="btn-outline">
{% lucide "bot" %}
Copilot
</button>
{% call popover(
trigger=trigger,
trigger_attrs={"class": "btn-icon-outline"},
popover_attrs={"class": "w-72 p-0", "data-align": "end"}
) %}
<header class="px-4 py-3 text-sm font-medium border-b">Agent Tasks</header>
<section class="p-4 text-sm space-y-2">
<textarea class="textarea w-full mb-4" placeholder="Describe your taks in natural language"></textarea>
<p class="font-medium">Start a new task with Copilot</p>
<p class="text-muted-foreground">
Describe your task in natural language. Copilot will work in the
background and open a pull request for your review.
</p>
</section>
{% endcall %}
</div>
{% endset %}
{{ code_preview("button-group-popover", code_popover | prettyHtml) }}
+243
View File
@@ -0,0 +1,243 @@
# Button
<Preview>
<div class="flex flex-wrap items-center gap-2">
<button type="button" class="btn" data-variant="outline">Button</button>
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Submit"><svg class="lucide lucide-arrow-up" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 7-7 7 7" /><path d="M12 19V5" /></svg></button>
</div>
</Preview>
## Usage
<Steps>
<Step title="Include CSS">
Import Tailwind and one full Basecoat style bundle.
```css
@import "tailwindcss";
@import "basecoat-css/vega.css";
```
Or import only the base CSS, Button component CSS, and one style pack.
```css
@import "tailwindcss";
@import "basecoat-css/base.css";
@import "basecoat-css/components/button.css";
@import "basecoat-css/styles/vega.css";
```
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Add your button HTML">
```html
<button type="button" class="btn">Primary</button>
<button type="button" class="btn" data-variant="outline">Outline</button>
<button type="button" class="btn" data-variant="secondary" data-size="sm">Small secondary</button>
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Settings"><svg class="lucide lucide-settings" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /><circle cx="12" cy="12" r="3" /></svg></button>
```
</Step>
</Steps>
Add `class="btn"` to a `<button>` or `<a>`. Add `data-variant` and `data-size` when needed. Add `data-icon="inline-start"` or `data-icon="inline-end"` to inline icons when a text button needs icon-aware spacing.
#### HTML structure
<dl>
<dt><code>&lt;button class="btn"&gt;</code></dt>
<dd>Button root. Use a native <code>&lt;button&gt;</code> for actions and <code>&lt;a&gt;</code> for navigation.</dd>
<dt><code>data-variant="primary|secondary|outline|ghost|link|destructive"</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Visual variant. Omit it for the primary variant.</dd>
<dt><code>data-size="xs|sm|default|lg|icon|icon-xs|icon-sm|icon-lg"</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Button size. Omit it for the default size.</dd>
</dl>
### Variants
| Variant | Markup |
| --- | --- |
| Primary | `<button class="btn">` |
| Secondary | `<button class="btn" data-variant="secondary">` |
| Outline | `<button class="btn" data-variant="outline">` |
| Ghost | `<button class="btn" data-variant="ghost">` |
| Link | `<button class="btn" data-variant="link">` |
| Destructive | `<button class="btn" data-variant="destructive">` |
### Sizes
| Size | Markup |
| --- | --- |
| Extra small | `<button class="btn" data-size="xs">` |
| Small | `<button class="btn" data-size="sm">` |
| Default | `<button class="btn">` |
| Large | `<button class="btn" data-size="lg">` |
| Icon extra small | `<button class="btn" data-size="icon-xs">` |
| Icon small | `<button class="btn" data-size="icon-sm">` |
| Icon | `<button class="btn" data-size="icon">` |
| Icon large | `<button class="btn" data-size="icon-lg">` |
Use `data-size="icon"` or one of the icon size variants for square buttons. Icon-only buttons need an accessible name, usually with `aria-label`.
Use `data-icon="inline-start"` or `data-icon="inline-end"` on inline icons when a text button needs icon-aware spacing. Use icon size variants only for icon-only buttons.
To group related buttons, use the [Button Group](/components/button-group/) component.
## Examples
### Size
Use `data-size` to change the size of the button.
<Preview>
<div class="flex flex-wrap items-center gap-2">
<button type="button" class="btn" data-variant="outline" data-size="xs">Extra Small</button>
<button type="button" class="btn" data-variant="outline" data-size="icon-xs" aria-label="Submit"><svg class="lucide lucide-arrow-up-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h10v10" /><path d="M7 17 17 7" /></svg></button>
<button type="button" class="btn" data-variant="outline" data-size="sm">Small</button>
<button type="button" class="btn" data-variant="outline" data-size="icon-sm" aria-label="Submit"><svg class="lucide lucide-arrow-up-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h10v10" /><path d="M7 17 17 7" /></svg></button>
<button type="button" class="btn" data-variant="outline">Default</button>
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Submit"><svg class="lucide lucide-arrow-up-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h10v10" /><path d="M7 17 17 7" /></svg></button>
<button type="button" class="btn" data-variant="outline" data-size="lg">Large</button>
<button type="button" class="btn" data-variant="outline" data-size="icon-lg" aria-label="Submit"><svg class="lucide lucide-arrow-up-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h10v10" /><path d="M7 17 17 7" /></svg></button>
</div>
</Preview>
### Default
<Preview>
<button type="button" class="btn">Button</button>
</Preview>
### Outline
<Preview>
<button type="button" class="btn" data-variant="outline">Outline</button>
</Preview>
### Secondary
<Preview>
<button type="button" class="btn" data-variant="secondary">Secondary</button>
</Preview>
### Ghost
<Preview>
<button type="button" class="btn" data-variant="ghost">Ghost</button>
</Preview>
### Destructive
<Preview>
<button type="button" class="btn" data-variant="destructive">Destructive</button>
</Preview>
### Link
<Preview>
<button type="button" class="btn" data-variant="link">Link</button>
</Preview>
### Icon
<Preview>
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Open"><svg class="lucide lucide-circle-fading-arrow-up" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a10 10 0 0 1 7.38 16.75" /><path d="m16 12-4-4-4 4" /><path d="M12 16V8" /><path d="M2.5 8.875a10 10 0 0 0-.5 3" /><path d="M2.83 16a10 10 0 0 0 2.43 3.4" /><path d="M4.636 5.235a10 10 0 0 1 .891-.857" /><path d="M8.644 21.42a10 10 0 0 0 7.631-.38" /></svg></button>
</Preview>
### With icon
Use `data-icon="inline-start"` or `data-icon="inline-end"` on inline icons to match upstream icon-aware spacing.
<Preview>
<button type="button" class="btn" data-variant="outline" data-size="sm"><svg data-icon="inline-start" class="lucide lucide-git-branch" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6a9 9 0 0 0-9 9V3" /><circle cx="18" cy="6" r="3" /><circle cx="6" cy="18" r="3" /></svg>
New Branch
</button>
</Preview>
### Rounded
Use the `rounded-full` class to make the button rounded.
<Preview>
<button type="button" class="btn rounded-full" data-variant="outline" data-size="icon" aria-label="Scroll up"><svg class="lucide lucide-arrow-up" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 7-7 7 7" /><path d="M12 19V5" /></svg></button>
</Preview>
### Spinner
<Preview>
<div class="flex flex-wrap items-center gap-2">
<button type="button" class="btn" disabled><svg data-icon="inline-start" class="animate-spin lucide lucide-loader-circle" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
Generating
</button>
<button type="button" class="btn" data-variant="secondary" disabled>
Downloading
<svg data-icon="inline-end" class="animate-spin lucide lucide-loader-circle" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
</button>
</div>
</Preview>
### Button group
To group related buttons, use the [Button Group](/components/button-group/) component.
<Preview>
<div class="flex w-fit items-stretch gap-2">
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Go back"><svg class="lucide lucide-arrow-left" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg></button>
<div role="group" class="button-group">
<button type="button" class="btn" data-variant="outline">Archive</button>
<button type="button" class="btn" data-variant="outline">Report</button>
</div>
<div role="group" class="button-group">
<button type="button" class="btn" data-variant="outline">Snooze</button>
<div id="button-docs-actions" class="dropdown-menu">
<button type="button" id="button-docs-actions-trigger" aria-label="More options" aria-haspopup="menu" aria-controls="button-docs-actions-menu" aria-expanded="false" class="btn" data-variant="outline" data-size="icon"><svg class="lucide lucide-ellipsis" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /><circle cx="5" cy="12" r="1" /></svg></button>
<div id="button-docs-actions-popover" data-popover aria-hidden="true" data-align="end">
<div role="menu" id="button-docs-actions-menu" aria-labelledby="button-docs-actions-trigger">
<div role="menuitem"><svg class="lucide lucide-mail-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8" /><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" /><path d="m16 19 2 2 4-4" /></svg>
Mark as Read
</div>
<div role="menuitem"><svg class="lucide lucide-archive" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="5" x="2" y="3" rx="1" /><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8" /><path d="M10 12h4" /></svg>
Archive
</div>
<hr role="separator" />
<div role="menuitem"><svg class="lucide lucide-clock" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" /></svg>
Snooze
</div>
<div role="menuitem"><svg class="lucide lucide-calendar" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4" /><path d="M16 2v4" /><rect width="18" height="18" x="3" y="4" rx="2" /><path d="M3 10h18" /></svg>
Add to Calendar
</div>
<div role="menuitem"><svg class="lucide lucide-list-filter-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5H2" /><path d="M6 12h12" /><path d="M9 19h6" /><path d="M16 5h6" /><path d="M19 8V2" /></svg>
Add to List
</div>
<hr role="separator" />
<div role="menuitem" class="text-destructive hover:bg-destructive/10 dark:hover:bg-destructive/20 focus:bg-destructive/10 dark:focus:bg-destructive/20 focus:text-destructive [&_svg]:!text-destructive"><svg class="lucide lucide-trash-2" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 11v6" /><path d="M14 11v6" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /><path d="M3 6h18" /><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></svg>
Trash
</div>
</div>
</div>
</div>
</div>
</div>
</Preview>
### RTL
Add `dir="rtl"` to the button or an ancestor. Flip directional icons explicitly.
<Preview>
<div dir="rtl" class="flex flex-wrap items-center gap-2">
<button type="button" class="btn" data-variant="outline">زر</button>
<button type="button" class="btn" data-variant="destructive">حذف</button>
<button type="button" class="btn" data-variant="outline">
إرسال
<svg data-icon="inline-end" class="rtl:rotate-180 lucide lucide-arrow-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg>
</button>
<button type="button" class="btn" data-variant="outline" data-size="icon" aria-label="Add"><svg class="lucide lucide-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /><path d="M12 5v14" /></svg></button>
<button type="button" class="btn" data-variant="secondary" disabled><svg data-icon="inline-start" class="animate-spin lucide lucide-loader-circle" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
جاري التحميل
</button>
</div>
</Preview>
-141
View File
@@ -1,141 +0,0 @@
---
layout: layouts/page.njk
title: Button
description: Displays a button or a component that looks like a button.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Primary
id: example-primary
- label: Secondary
id: example-secondary
- label: Destructive
id: example-destructive
- label: Outline
id: example-outline
- label: Ghost
id: example-ghost
- label: Link
id: example-link
- label: Icon
id: example-icon
- label: With Icon
id: example-with-icon
- label: Loading
id: example-loading
- label: Large
id: example-large
- label: Small
id: example-small
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<button class="btn">Button</button>{% endset %}
{{ code_preview("button", code) }}
<h2 id="usage">Usage</h2>
<section class="prose">
<p>You can use one of the <code>btn</code> classes to style any element as a button:</p>
<ul>
<li>
<b>Variants</b>:
<ul>
<li><code>btn</code> or <code>btn-primary</code> for primary
buttons,</li>
<li><code>btn-secondary</code> for secondary buttons.</li>
<li><code>btn-destructive</code> for destructive buttons.</li>
<li><code>btn-outline</code> for outline buttons.</li>
<li><code>btn-ghost</code> for ghost buttons.</li>
<li><code>btn-link</code> for link buttons.</li>
<li><code>btn-icon</code> for icon buttons. This can be combined with other variants, for example <code>btn-icon-destructive</code>.</li>
</ul>
</li>
<li>
<b>Sizes</b>:
<ul>
<li><code>btn-sm</code> for small buttons.</li>
<li><code>btn-lg</code> for large buttons.</li>
<li>You can combine sizes with any variant, for example <code>btn-lg-destructive</code> or <code>btn-sm-icon-outline</code>.</li>
</ul>
</li>
</ul>
</section>
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-primary"><a href="#example-primary">Primary</a></h3>
{% set code %}<button class="btn">Primary</button>{% endset %}
{{ code_preview("button-primary", code) }}
<h3 id="example-secondary"><a href="#example-secondary">Secondary</a></h3>
{% set code %}<button class="btn-secondary">Button</button>{% endset %}
{{ code_preview("button-secondary", code) }}
<h3 id="example-destructive"><a href="#example-destructive">Destructive</a></h3>
{% set code %}<button class="btn-destructive">Destructive</button>{% endset %}
{{ code_preview("button-destructive", code) }}
<h3 id="example-outline"><a href="#example-outline">Outline</a></h3>
{% set code %}<button class="btn-outline">Outline</button>{% endset %}
{{ code_preview("button-outline", code) }}
<h3 id="example-ghost"><a href="#example-ghost">Ghost</a></h3>
{% set code %}<button class="btn-ghost">Ghost</button>{% endset %}
{{ code_preview("button-ghost", code) }}
<h3 id="example-link"><a href="#example-link">Link</a></h3>
{% set code %}<button class="btn-link">Link</button>{% endset %}
{{ code_preview("button-link", code) }}
<h3 id="example-icon"><a href="#example-icon">Icon</a></h3>
{% set code %}<button class="btn-icon-outline">
{% lucide "chevron-right" %}
</button>{% endset %}
{{ code_preview("button-icon", code) }}
<h3 id="example-with-icon"><a href="#example-with-icon">With Icon</a></h3>
{% set code %}<button class="btn">
{% lucide "send" %}
Send email
</button>{% endset %}
{{ code_preview("button-with-icon", code) }}
<h3 id="example-loading"><a href="#example-loading">Loading</a></h3>
{% set code %}<button class="btn-outline" disabled>
{% lucide "loader", { "class": "animate-spin" } %}
Loading
</button>{% endset %}
{{ code_preview("button-loading", code) }}
<h3 id="example-large"><a href="#example-large">Large</a></h3>
{% set code %}<button class="btn-lg">
{% lucide "shopping-cart" %}
Buy
</button>{% endset %}
{{ code_preview("button-large", code) }}
<h3 id="example-small"><a href="#example-small">Small</a></h3>
{% set code %}<button class="btn-sm-icon-destructive">
{% lucide "trash" %}
</button>{% endset %}
{{ code_preview("button-small", code) }}
+180
View File
@@ -0,0 +1,180 @@
# Card
<Preview class="w-full max-w-sm">
<div class="card">
<header>
<h2>Login to your account</h2>
<p>Enter your email below to login to your account</p>
<div class="card-action">
<button type="button" class="btn" data-variant="link">Sign Up</button>
</div>
</header>
<section>
<form class="grid gap-6">
<div class="grid gap-2">
<label class="label" for="demo-card-email">Email</label>
<input class="input" type="email" id="demo-card-email" placeholder="m@example.com" required />
</div>
<div class="grid gap-2">
<div class="flex items-center">
<label class="label" for="demo-card-password">Password</label>
<a href="#" class="ms-auto inline-block text-sm underline-offset-4 hover:underline">Forgot your password?</a>
</div>
<input class="input" type="password" id="demo-card-password" required />
</div>
</form>
</section>
<footer class="flex-col gap-2">
<button type="button" class="btn w-full">Login</button>
<button type="button" class="btn w-full" data-variant="outline">Login with Google</button>
</footer>
</div>
</Preview>
## Usage
<Steps>
<Step title="Include CSS">
Import Tailwind and one full Basecoat style bundle.
```css
@import "tailwindcss";
@import "basecoat-css/vega.css";
```
Or import only the base CSS, Card component CSS, and one style pack.
```css
@import "tailwindcss";
@import "basecoat-css/base.css";
@import "basecoat-css/components/card.css";
@import "basecoat-css/styles/vega.css";
```
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Add your card HTML">
```html
<div class="card">
<header>
<h2>Card Title</h2>
<p>Card Description</p>
<div class="card-action">Card Action</div>
</header>
<section> <p>Card Content</p></section>
<footer>
<p>Card Footer</p>
</footer>
</div>
```
</Step>
</Steps>
## HTML structure
<dl>
<dt><code>&lt;div class="card"&gt;</code></dt>
<dd>
The card container. Use <code>data-size="sm"</code> for a denser layout.
<dl>
<dt><code>&lt;header&gt;</code></dt>
<dd>
The card header.
<dl>
<dt><code>&lt;h2&gt;</code> or <code>&lt;h3&gt;</code></dt>
<dd>The card title.</dd>
<dt><code>&lt;p&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>The card description.</dd>
<dt><code>&lt;div class="card-action"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Action or metadata placed in the top-right of the header. Use <code>&lt;menu class="card-action"&gt;</code> when the content is a list of commands.</dd>
</dl>
</dd>
<dt><code>&lt;section&gt;</code></dt>
<dd>The main card content.</dd>
<dt><code>&lt;footer&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Footer actions or secondary content at the bottom of the card.</dd>
</dl>
</dd>
</dl>
## Examples
### Size
Use `data-size="sm"` to set the size of the card to small. The small size variant uses smaller spacing.
<Preview class="w-full max-w-sm">
<div class="card" data-size="sm">
<header>
<h2>Small Card</h2>
<p>This card uses the small size variant.</p>
<div class="card-action">
<button type="button" class="btn" data-size="sm" data-variant="outline">Action</button>
</div>
</header>
<section> <p>The card component supports a size attribute that can be set to <code>sm</code> for a more compact appearance.</p></section>
</div>
</Preview>
### Image
Add an image before the card header to create a card with an image.
<Preview class="w-full max-w-sm">
<div class="card">
<img
alt="Event cover"
class="aspect-video w-full object-cover"
src="https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=1200&auto=format&fit=crop&q=80"
/>
<header>
<h2>Design systems meetup</h2>
<p>A practical talk on component APIs, accessibility, and shipping faster.</p>
<div class="card-action">
<span class="badge" data-variant="secondary">Featured</span>
</div>
</header>
<footer>
<button type="button" class="btn w-full">View Event</button>
</footer>
</div>
</Preview>
## RTL
Add `dir="rtl"` to the card or an ancestor.
<Preview class="w-full max-w-sm">
<div class="card" dir="rtl">
<header>
<h2>تسجيل الدخول إلى حسابك</h2>
<p>أدخل بريدك الإلكتروني أدناه لتسجيل الدخول إلى حسابك</p>
<div class="card-action">
<button type="button" class="btn" data-variant="link">إنشاء حساب</button>
</div>
</header>
<section>
<form class="grid gap-6">
<div class="grid gap-2">
<label class="label" for="demo-card-rtl-email">البريد الإلكتروني</label>
<input class="input" type="email" id="demo-card-rtl-email" placeholder="m@example.com" required />
</div>
<div class="grid gap-2">
<div class="flex items-center">
<label class="label" for="demo-card-rtl-password">كلمة المرور</label>
<a href="#" class="ms-auto inline-block text-sm underline-offset-4 hover:underline">نسيت كلمة المرور؟</a>
</div>
<input class="input" type="password" id="demo-card-rtl-password" required />
</div>
</form>
</section>
<footer class="flex-col gap-2">
<button type="button" class="btn w-full">تسجيل الدخول</button>
<button type="button" class="btn w-full" data-variant="outline">تسجيل الدخول باستخدام Google</button>
</footer>
</div>
</Preview>
-156
View File
@@ -1,156 +0,0 @@
---
layout: layouts/page.njk
title: Card
description: Displays a card with header, content, and footer.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: With content and footer
id: example-with-content-and-footer
- label: With image
id: example-with-image
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<div class="card w-full">
<header>
<h2>Login to your account</h2>
<p>Enter your details below to login to your account</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-2">
<label for="demo-card-form-email">Email</label>
<input type="email" id="demo-card-form-email">
</div>
<div class="grid gap-2">
<div class="flex items-center gap-2">
<label for="demo-card-form-password">Password</label>
<a href="#" class="ml-auto inline-block text-sm underline-offset-4 hover:underline">Forgot your password?</a>
</div>
<input type="password" id="demo-card-form-password">
</div>
</form>
</section>
<footer class="flex flex-col items-center gap-2">
<button type="button" class="btn w-full">Login</button>
<button type="button" class="btn-outline w-full">Login with Google</button>
<p class="mt-4 text-center text-sm">Don't have an account? <a href="#" class="underline-offset-4 hover:underline">Sign up</a></p>
</footer>
</div>{% endset %}
{{ code_preview("card", code, "w-full max-w-sm") }}
<h2 id="usage">Usage</h2>
{% set code %}<div class="card">
<header>
<h2>Card Title</h2>
<p>Card Description</p>
</header>
<section>
<p>Card Content</p>
</section>
<footer>
<p>Card Footer</p>
</footer>
</div>{% endset %}
{{ code_block(code) }}
<section class="prose">
<p>The component has the following HTML structure:</p>
<dl>
<dt><code class="highlight language-html">&lt;div class="card"&gt;</code></dt>
<dd>The card container.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dd>The header of the card.
<dl>
<dt><code class="highlight language-html">&lt;h2&gt;</code></dt>
<dd>The title of the card.</dd>
<dt><code class="highlight language-html">&lt;p&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The description of the card.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code></dt>
<dd>The content of the card.</dd>
<dt><code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The footer of the card.</dd>
</dl>
</dd>
</dl>
</section>
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-with-content-and-footer"><a href="#example-with-content-and-footer">With content and footer</a></h3>
{% set code %}<div class="card">
<header>
<h2>Meeting Notes</h2>
<p>Transcript from the meeting with the client.</p>
</header>
<section class="text-sm">
<p>Client requested dashboard redesign with focus on mobile responsiveness.</p>
<ol class="mt-4 flex list-decimal flex-col gap-2 pl-6">
<li>New analytics widgets for daily/weekly metrics</li>
<li>Simplified navigation menu</li>
<li>Dark mode support</li>
<li>Timeline: 6 weeks</li>
<li>Follow-up meeting scheduled for next Tuesday</li>
</ol>
</section>
<footer class="flex items-center">
<div class="flex -space-x-2 [&_img]:ring-card [&_img]:ring-2 [&_img]:grayscale [&_img]:size-8 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
</footer>
</div>{% endset %}
{{ code_preview("card-with-content-and-footer", code) }}
<h3 id="example-with-image"><a href="#example-with-image">With image</a></h3>
{% set code %}<div class="card">
<header>
<h2>Is this an image?</h2>
<p>This is a card with an image.</p>
</header>
<section class="px-0">
<img
alt="Photo by Drew Beamer"
loading="lazy"
width="500"
height="500"
class="aspect-video object-cover" style="color:transparent"
srcset="
https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80&w=640&q=75 1x,
https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80&w=1080&q=75 2x
"
src="https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80&w=1080&q=75"
/>
</section>
<footer class="flex items-center gap-2">
<span class="badge-outline">
{% lucide "bed" %}
1
</span>
<span class="badge-outline">
{% lucide "bath" %}
2
</span>
<span class="badge-outline">
{% lucide "land-plot" %}
350m²
</span>
<span class="ml-auto font-medium tabular-nums">
$135,000
</span>
</footer>
</div>{% endset %}
{{ code_preview("card-with-image", code) }}
-102
View File
@@ -1,102 +0,0 @@
---
layout: layouts/page.njk
title: Carousel
description: A carousel with motion and swipe.
toc:
- label: Usage
id: usage
---
<style>
.slider {
width: 300px;
text-align: center;
overflow: hidden;
}
.slides {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
/*
scroll-snap-points-x: repeat(300px);
scroll-snap-type: mandatory;
*/
}
.slides::-webkit-scrollbar {
display: none;
}
.slides {
scrollbar-width: none; /* Firefox */
}
.slides > div {
scroll-snap-align: start;
flex-shrink: 0;
width: 300px;
height: 300px;
margin-right: 50px;
border-radius: 10px;
background: #eee;
transform-origin: center center;
transform: scale(1);
transition: transform 0.5s;
position: relative;
display: flex;
justify-content: center;
align-items: center;
font-size: 100px;
}
</style>
<div class="slider">
<a href="#slide-1">1</a>
<a href="#slide-2">2</a>
<a href="#slide-3">3</a>
<a href="#slide-4">4</a>
<a href="#slide-5">5</a>
<a href="#slide-6">6</a>
<a href="#slide-7">7</a>
<a href="#slide-8">8</a>
<a href="#slide-9">9</a>
<div class="slides">
<div id="slide-1">
1
</div>
<div id="slide-2">
2
</div>
<div id="slide-3">
3
</div>
<div id="slide-4">
4
</div>
<div id="slide-5">
5
</div>
<div id="slide-6">
6
</div>
<div id="slide-7">
7
</div>
<div id="slide-8">
8
</div>
<div id="slide-9">
9
</div>
</div>
</div>
+268
View File
@@ -0,0 +1,268 @@
import basecoatRuntimeSource from "../../../src/js/basecoat.js?raw"
import chartRuntimeSource from "../../../src/js/chart.js?raw"
import chartExamplesSource from "../site/chart-examples.js?raw"
# Chart
<Callout type="warning" title="Alpha API" icon="triangle-alert">
Chart support is alpha. The JavaScript API, generated markup, and option names may change before the stable 1.0 release.
</Callout>
Use Chart.js with Basecoat defaults for themed colors, external tooltips, and optional generated legends.
<Preview class="w-full">
<canvas id="chart-example-overview" aria-label="Visitors by device"></canvas>
</Preview>
## Usage
<Steps>
<Step title="Include CSS">
Import Tailwind and one full Basecoat style bundle.
```css
@import "tailwindcss";
@import "basecoat-css/vega.css";
```
Or import only the base CSS, Chart component CSS, and one style pack.
```css
@import "tailwindcss";
@import "basecoat-css/base.css";
@import "basecoat-css/components/chart.css";
@import "basecoat-css/styles/vega.css";
```
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Include JavaScript">
Copy or serve Chart.js, the Basecoat runtime, and Chart script.
```html
<script src="/assets/js/chart.umd.min.js"></script>
<script src="/assets/js/basecoat.min.js" defer></script>
<script src="/assets/js/chart.min.js" defer></script>
```
Chart is not included in the full Basecoat JavaScript bundle.
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Add your chart HTML">
```html
<canvas id="visitors-chart" aria-label="Monthly visitors"></canvas>
<script>
document.addEventListener("DOMContentLoaded", () => {
window.basecoat.chart("#visitors-chart", {
type: "bar",
labelKey: "month",
data: [
{ month: "Jan", desktop: 186, mobile: 80 },
{ month: "Feb", desktop: 305, mobile: 200 },
{ month: "Mar", desktop: 237, mobile: 120 },
{ month: "Apr", desktop: 73, mobile: 190 },
{ month: "May", desktop: 209, mobile: 130 },
{ month: "Jun", desktop: 214, mobile: 140 },
],
series: {
desktop: { label: "Desktop", color: "var(--chart-1)" },
mobile: { label: "Mobile", color: "var(--chart-2)" },
},
})
})
</script>
```
</Step>
</Steps>
`basecoat.chart()` targets the `<canvas>` directly and creates Basecoat's internal `.chart` container for Chart.js sizing. You do not need to add a wrapper or class in your HTML.
Charts default to `aspect-video`. Use a height, `min-h-*`, or `aspect-*` utility on a parent element when you need a different measuring box.
## Legend
Set `legend: true` to generate a Basecoat-styled legend after the canvas.
The generated legend is display-only by default, matching shadcn/ui. Use a Chart.js plugin or custom legend markup if you need click-to-toggle behavior.
```js
window.basecoat.chart("#visitors-chart", {
type: "bar",
labelKey: "month",
data,
series,
legend: true,
})
```
## Series
`series` maps object keys from each data row to labels, colors, and optional Chart.js dataset settings.
```js
window.basecoat.chart("#revenue-chart", {
type: "line",
labelKey: "month",
data,
series: {
recurring: {
label: "Recurring",
color: "var(--chart-1)",
surface: "gradient",
dataset: {
fill: true,
tension: 0.35,
},
},
new: {
label: "New",
color: "var(--chart-2)",
dataset: {
borderDash: [4, 4],
},
},
},
})
```
## Escape Hatches
Pass raw Chart.js options and plugins through `options` and `plugins`.
```js
const chart = window.basecoat.chart("#visitors-chart", {
type: "bar",
labelKey: "month",
data,
series,
options: {
indexAxis: "x",
scales: {
y: {
beginAtZero: true,
},
},
plugins: {
tooltip: {
mode: "index",
},
},
},
plugins: [myChartJsPlugin],
})
```
Pass complete Chart.js data through `chartData` when the simplified `data` plus `series` mapper is not enough.
```js
window.basecoat.chart("#custom-chart", {
type: "scatter",
chartData: {
datasets: [
{
label: "Samples",
data: [{ x: 1, y: 2 }, { x: 2, y: 5 }],
},
],
},
options: {
scales: {
x: { type: "linear" },
},
},
})
```
`basecoat.chart()` returns the Chart.js instance for single targets and an array of instances for multi-target selectors.
## Examples
### Bar chart - multiple
<Preview class="w-full">
<canvas id="chart-example-visitors" aria-label="Monthly visitors by device"></canvas>
</Preview>
### Line chart - linear
<Preview class="w-full">
<canvas id="chart-example-line" aria-label="Revenue line chart"></canvas>
</Preview>
### Line chart - step
<Preview class="w-full">
<canvas id="chart-example-step" aria-label="Support queue step chart"></canvas>
</Preview>
### Bar chart - stacked
<Preview class="w-full">
<canvas id="chart-example-stacked" aria-label="Stacked pipeline bar chart"></canvas>
</Preview>
### Pie chart - donut
<Preview class="w-full">
<canvas id="chart-example-donut" aria-label="Traffic source donut chart"></canvas>
</Preview>
### Radar chart
<Preview class="w-full">
<canvas id="chart-example-radar" aria-label="Channel performance radar chart"></canvas>
</Preview>
<script is:inline src="https://cdn.jsdelivr.net/npm/chart.js/dist/chart.umd.min.js"></script>
<script is:inline>{`
(() => {
const initChartPage = () => {
if (!window.Chart) {
requestAnimationFrame(initChartPage)
return
}
if (!window.basecoat) {
${basecoatRuntimeSource}
}
if (!window.basecoat.chart) {
${chartRuntimeSource}
}
${chartExamplesSource}
}
initChartPage()
})()
`}</script>
## API
| Option | Default | Description |
| --- | --- | --- |
| `type` | `"bar"` | Chart.js chart type. |
| `labelKey` | `"label"` | Row key used for labels. |
| `data` | `[]` | Array of row objects, or raw Chart.js data with `datasets`. |
| `series` | `{}` | Series config mapped from row keys. |
| `legend` | `false` | Generates a Basecoat legend after the canvas. |
| `tooltip` | `true` | Uses the Basecoat external tooltip. |
| `options` | `{}` | Raw Chart.js options. |
| `plugins` | `[]` | Raw Chart.js plugins. |
| `chartData` | `undefined` | Complete Chart.js data object. Overrides the data mapper. |
### Series options
| Option | Description |
| --- | --- |
| `label` | Display label for legends and tooltips. |
| `color` | Base color for the series stroke, fill, legend marker, and tooltip marker. |
| `surface` | Derives a fill from `color`. Use `true` for a translucent fill, `"gradient"` for an upstream-style vertical fade, or `{ from, to }` for custom opacity stops. |
| `dataset` | Raw Chart.js dataset options. |
-814
View File
@@ -1,814 +0,0 @@
---
layout: layouts/page.njk
title: Chart
description: Displays the path to the current resource using a hierarchy of links.
toc:
- label: Usage
id: usage
---
<div class="grid gap-6">
<div class="grid grid-cols-2 gap-6">
<div class="card">
<header>
<h2>Area Chart</h2>
<p>Showing total visitors for the last 6 months</p>
</header>
<section>
<canvas id="chart-area-default"></canvas>
</section>
<footer>
<div class="grid gap-2 text-sm">
<div class="flex items-center gap-2 leading-none font-medium">
Trending up by 5.2% this month
{% lucide "trending-up", { "class": "size-4" } %}
</div>
<div class="text-muted-foreground flex items-center gap-2 leading-none">January - June 2024</div>
</div>
</footer>
</div>
<div class="card">
<header>
<h2>Area Chart - Stacked</h2>
<p>Showing total visitors for the last 6 months</p>
</header>
<section>
<canvas id="chart-area-stacked"></canvas>
</section>
<footer>
<div class="grid gap-2 text-sm">
<div class="flex items-center gap-2 leading-none font-medium">
Trending up by 5.2% this month
{% lucide "trending-up", { "class": "size-4" } %}
</div>
<div class="text-muted-foreground flex items-center gap-2 leading-none">January - June 2024</div>
</div>
</footer>
</div>
<div class="card">
<header>
<h2>Bar Chart - Multiple</h2>
<p>Showing total visitors for the last 6 months</p>
</header>
<section>
<canvas id="chart-bar-multiple"></canvas>
</section>
<footer>
<div class="grid gap-2 text-sm">
<div class="flex items-center gap-2 leading-none font-medium">
Trending up by 5.2% this month
{% lucide "trending-up", { "class": "size-4" } %}
</div>
<div class="text-muted-foreground flex items-center gap-2 leading-none">January - June 2024</div>
</div>
</footer>
</div>
<div class="card">
<header>
<h2>Doughnut Chart</h2>
<p>Showing total visitors for the last 6 months</p>
</header>
<section class="relative">
<div>
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center">
<div class="text-3xl font-semibold">1,125</div>
<div class="text-muted-foreground text-xs">Visitors</div>
</div>
<canvas id="chart-doughnut" class="max-h-[200px] mx-auto"></canvas>
</div>
</section>
</div>
</div>
<div class="card">
<header>
<h2>Bar Chart - Interactive</h2>
<p>Showing total visitors for the last 6 months</p>
</header>
<section>
<canvas id="chart-bar-interactive"></canvas>
</section>
</div>
<div class="card">
<header>
<h2>Bar Chart - Interactive</h2>
<p>Showing total visitors for the last 6 months</p>
</header>
<section class="max-h-[150px]">
<canvas id="chart-line-interactive"></canvas>
</section>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Helper function to MULTIPLY the alpha channel of oklch colors by a factor
function setAlpha(colorString, alphaFactor) {
colorString = colorString.trim();
if (colorString.startsWith('oklch(')) {
const match = colorString.match(/^oklch\(([^\)]+?)\s*(?:\/\s*([\d.]+)(%?)\s*)?\)$/);
if (match && match[1]) {
const lchPart = match[1];
let currentAlpha = 1.0;
if (match[2]) {
currentAlpha = parseFloat(match[2]);
if (match[3] === '%') {
currentAlpha /= 100;
}
}
let newAlpha = currentAlpha * alphaFactor;
newAlpha = Math.max(0, Math.min(1, newAlpha));
return `oklch(${lchPart} / ${newAlpha.toFixed(3)})`;
} else {
console.warn('Failed to parse oklch string:', colorString);
return colorString;
}
}
return colorString;
}
const computedStyle = getComputedStyle(document.documentElement);
const themeColors = {
background: computedStyle.getPropertyValue('--background').trim(),
border: computedStyle.getPropertyValue('--border').trim(),
mutedForeground: computedStyle.getPropertyValue('--muted-foreground').trim(),
foreground: computedStyle.getPropertyValue('--foreground').trim(),
chart1: computedStyle.getPropertyValue('--chart-1').trim(),
chart2: computedStyle.getPropertyValue('--chart-2').trim(),
chart3: computedStyle.getPropertyValue('--chart-3').trim(),
chart4: computedStyle.getPropertyValue('--chart-4').trim(),
chart5: computedStyle.getPropertyValue('--chart-5').trim(),
};
const getOrCreateTooltip = (chart) => {
let tooltipEl = chart.canvas.parentNode.querySelector('div[role="tooltip"]');
if (!tooltipEl) {
const tooltipHTML = `
<div
role="tooltip"
class="border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl opacity-0 absolute pointer-events-none transition-all duration-400 space-y-1"
>
<h3 class="font-medium"></h3>
<ul class="grid gap-1"></ul>
</div>`;
const parser = new DOMParser();
const doc = parser.parseFromString(tooltipHTML.trim(), 'text/html');
tooltipEl = doc.body.firstChild;
chart.canvas.parentNode.appendChild(tooltipEl);
}
return tooltipEl;
};
const externalTooltipHandler = (context) => {
const {chart, tooltip} = context;
const chartType = chart.config._config.type;
const tooltipEl = getOrCreateTooltip(chart);
const titleEl = tooltipEl.querySelector('h3');
const bodyEl = tooltipEl.querySelector('ul');
// Hide if no tooltip
if (tooltip.opacity === 0) {
tooltipEl.classList.remove('opacity-100');
tooltipEl.classList.add('opacity-0');
return;
}
let bodyHtml = '';
if (tooltip.body) {
const titleLines = tooltip.title || [];
const bodyLines = tooltip.body.map(b => b.lines);
if (titleEl) {
if (chartType === 'doughnut') {
titleEl.remove();
} else {
titleEl.textContent = titleLines[0] || '';
}
}
bodyLines.forEach((body, i) => {
const colors = tooltip.labelColors[i];
const parts = body[0].split(': ');
const label = chartType === 'doughnut'
? titleLines[0] || ''
: parts[0] || '';
const value = parts[1] || '';
let color;
switch (chartType) {
case 'doughnut':
case 'pie':
case 'bar':
color = colors.backgroundColor;
break;
default:
color = colors.borderColor;
}
bodyHtml += `<li class="flex w-full flex-wrap items-center justify-between leading-none gap-x-2">
<div class="flex items-center gap-x-1.5">
<div class="shrink-0 rounded-[2px] size-2.5 block" style="background:${color}"></div>
<span class="text-muted-foreground">${label}</span>
</div>
<span class="text-foreground font-medium font-mono tabular-nums">${value}</span>
</li>`;
});
}
if (bodyEl) bodyEl.innerHTML = bodyHtml;
const {offsetLeft: positionX, offsetTop: positionY} = chart.canvas;
const tooltipHeight = tooltipEl.offsetHeight;
const tooltipWidth = tooltipEl.offsetWidth;
const caretX = tooltip.caretX;
const caretY = tooltip.caretY;
const xAlign = tooltip.xAlign;
const yAlign = tooltip.yAlign;
const tooltipOffset = 8;
let left = positionX + caretX;
let top = positionY + caretY;
if (xAlign === 'center') {
left -= tooltipWidth / 2;
} else if (xAlign === 'right') {
left -= (tooltipWidth + tooltipOffset);
} else { // xAlign === 'left'
left += tooltipOffset;
}
if (yAlign === 'center') {
top -= tooltipHeight / 2;
} else if (yAlign === 'top') {
top -= (tooltipHeight + tooltipOffset);
} else {
top += tooltipOffset;
}
const canvasBounds = chart.canvas.getBoundingClientRect();
if (left < 0) {
left = 0;
} else if (left + tooltipWidth > canvasBounds.width) {
left = canvasBounds.width - tooltipWidth;
}
if (top < 0) {
top = 0;
} else if (top + tooltipHeight > window.innerHeight) {
const flippedTop = positionY + caretY - tooltipHeight - tooltipOffset;
if (flippedTop >= 0) top = flippedTop;
}
tooltipEl.classList.remove('opacity-0');
tooltipEl.classList.add('opacity-100');
tooltipEl.style.left = left + 'px';
tooltipEl.style.top = top + 'px';
};
let ctx = document.getElementById('chart-area-default');
let data = {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [{
label: 'Desktop',
data: [186, 305, 237, 73, 209, 214],
borderWidth: 1,
backgroundColor: setAlpha(themeColors.chart1, 0.2),
borderColor: themeColors.chart1,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBackgroundColor: themeColors.chart1,
pointHoverBorderColor: themeColors.chart1,
fill: 'origin'
}]
};
let config = {
type: 'line',
data: data,
options: {
tension: 0.4,
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
},
scales: {
y: {
border: { display: false },
grid: {
color: setAlpha(themeColors.border, 0.5),
lineWidth: 1
},
ticks: { display: false }
},
x: {
grid: { display: false },
ticks: { color: themeColors.mutedForeground }
}
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
position: 'nearest',
external: externalTooltipHandler,
}
}
}
};
new Chart(ctx, config);
ctx = document.getElementById('chart-area-stacked');
data = {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [{
label: 'Desktop',
data: [186, 305, 237, 73, 209, 214],
borderWidth: 1,
backgroundColor: setAlpha(themeColors.chart1, 0.2),
borderColor: themeColors.chart1,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBackgroundColor: themeColors.chart1,
pointHoverBorderColor: themeColors.chart1,
fill: 'origin'
}, {
label: 'Mobile',
data: [80, 200, 120, 190, 130, 140],
borderWidth: 1,
backgroundColor: setAlpha(themeColors.chart2, 0.2),
borderColor: themeColors.chart2,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBackgroundColor: themeColors.chart2,
pointHoverBorderColor: themeColors.chart2,
fill: 'origin'
}]
};
config = {
type: 'line',
data: data,
options: {
tension: 0.4,
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
},
scales: {
y: {
border: { display: false },
grid: {
color: setAlpha(themeColors.border, 0.5),
lineWidth: 1
},
ticks: { display: false }
},
x: {
grid: { display: false },
ticks: { color: themeColors.mutedForeground }
}
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
position: 'nearest',
external: externalTooltipHandler,
}
}
}
};
new Chart(ctx, config);
ctx = document.getElementById('chart-bar-multiple');
data = {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [{
label: 'Desktop',
data: [186, 305, 237, 73, 209, 214],
backgroundColor: themeColors.chart1,
borderWidth: 0,
borderRadius: 4,
barPercentage: 0.7,
categoryPercentage: 0.8
}, {
label: 'Mobile',
data: [80, 200, 120, 190, 130, 140],
backgroundColor: themeColors.chart2,
borderWidth: 0,
borderRadius: 4,
barPercentage: 0.7,
categoryPercentage: 0.8
}]
};
config = {
type: 'bar',
data: data,
options: {
interaction: {
mode: 'index',
axis: 'x',
intersect: false
},
scales: {
y: {
border: { display: false },
grid: {
color: setAlpha(themeColors.border, 0.5),
lineWidth: 1
},
ticks: { display: false }
},
x: {
grid: { display: false },
ticks: { color: themeColors.mutedForeground }
}
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
position: 'nearest',
external: externalTooltipHandler,
}
}
}
};
new Chart(ctx, config);
const chartData = [
{ date: "2024-04-01", desktop: 222, mobile: 150 },
{ date: "2024-04-02", desktop: 97, mobile: 180 },
{ date: "2024-04-03", desktop: 167, mobile: 120 },
{ date: "2024-04-04", desktop: 242, mobile: 260 },
{ date: "2024-04-05", desktop: 373, mobile: 290 },
{ date: "2024-04-06", desktop: 301, mobile: 340 },
{ date: "2024-04-07", desktop: 245, mobile: 180 },
{ date: "2024-04-08", desktop: 409, mobile: 320 },
{ date: "2024-04-09", desktop: 59, mobile: 110 },
{ date: "2024-04-10", desktop: 261, mobile: 190 },
{ date: "2024-04-11", desktop: 327, mobile: 350 },
{ date: "2024-04-12", desktop: 292, mobile: 210 },
{ date: "2024-04-13", desktop: 342, mobile: 380 },
{ date: "2024-04-14", desktop: 137, mobile: 220 },
{ date: "2024-04-15", desktop: 120, mobile: 170 },
{ date: "2024-04-16", desktop: 138, mobile: 190 },
{ date: "2024-04-17", desktop: 446, mobile: 360 },
{ date: "2024-04-18", desktop: 364, mobile: 410 },
{ date: "2024-04-19", desktop: 243, mobile: 180 },
{ date: "2024-04-20", desktop: 89, mobile: 150 },
{ date: "2024-04-21", desktop: 137, mobile: 200 },
{ date: "2024-04-22", desktop: 224, mobile: 170 },
{ date: "2024-04-23", desktop: 138, mobile: 230 },
{ date: "2024-04-24", desktop: 387, mobile: 290 },
{ date: "2024-04-25", desktop: 215, mobile: 250 },
{ date: "2024-04-26", desktop: 75, mobile: 130 },
{ date: "2024-04-27", desktop: 383, mobile: 420 },
{ date: "2024-04-28", desktop: 122, mobile: 180 },
{ date: "2024-04-29", desktop: 315, mobile: 240 },
{ date: "2024-04-30", desktop: 454, mobile: 380 },
{ date: "2024-05-01", desktop: 165, mobile: 220 },
{ date: "2024-05-02", desktop: 293, mobile: 310 },
{ date: "2024-05-03", desktop: 247, mobile: 190 },
{ date: "2024-05-04", desktop: 385, mobile: 420 },
{ date: "2024-05-05", desktop: 481, mobile: 390 },
{ date: "2024-05-06", desktop: 498, mobile: 520 },
{ date: "2024-05-07", desktop: 388, mobile: 300 },
{ date: "2024-05-08", desktop: 149, mobile: 210 },
{ date: "2024-05-09", desktop: 227, mobile: 180 },
{ date: "2024-05-10", desktop: 293, mobile: 330 },
{ date: "2024-05-11", desktop: 335, mobile: 270 },
{ date: "2024-05-12", desktop: 197, mobile: 240 },
{ date: "2024-05-13", desktop: 197, mobile: 160 },
{ date: "2024-05-14", desktop: 448, mobile: 490 },
{ date: "2024-05-15", desktop: 473, mobile: 380 },
{ date: "2024-05-16", desktop: 338, mobile: 400 },
{ date: "2024-05-17", desktop: 499, mobile: 420 },
{ date: "2024-05-18", desktop: 315, mobile: 350 },
{ date: "2024-05-19", desktop: 235, mobile: 180 },
{ date: "2024-05-20", desktop: 177, mobile: 230 },
{ date: "2024-05-21", desktop: 82, mobile: 140 },
{ date: "2024-05-22", desktop: 81, mobile: 120 },
{ date: "2024-05-23", desktop: 252, mobile: 290 },
{ date: "2024-05-24", desktop: 294, mobile: 220 },
{ date: "2024-05-25", desktop: 201, mobile: 250 },
{ date: "2024-05-26", desktop: 213, mobile: 170 },
{ date: "2024-05-27", desktop: 420, mobile: 460 },
{ date: "2024-05-28", desktop: 233, mobile: 190 },
{ date: "2024-05-29", desktop: 78, mobile: 130 },
{ date: "2024-05-30", desktop: 340, mobile: 280 },
{ date: "2024-05-31", desktop: 178, mobile: 230 },
{ date: "2024-06-01", desktop: 178, mobile: 200 },
{ date: "2024-06-02", desktop: 470, mobile: 410 },
{ date: "2024-06-03", desktop: 103, mobile: 160 },
{ date: "2024-06-04", desktop: 439, mobile: 380 },
{ date: "2024-06-05", desktop: 88, mobile: 140 },
{ date: "2024-06-06", desktop: 294, mobile: 250 },
{ date: "2024-06-07", desktop: 323, mobile: 370 },
{ date: "2024-06-08", desktop: 385, mobile: 320 },
{ date: "2024-06-09", desktop: 438, mobile: 480 },
{ date: "2024-06-10", desktop: 155, mobile: 200 },
{ date: "2024-06-11", desktop: 92, mobile: 150 },
{ date: "2024-06-12", desktop: 492, mobile: 420 },
{ date: "2024-06-13", desktop: 81, mobile: 130 },
{ date: "2024-06-14", desktop: 426, mobile: 380 },
{ date: "2024-06-15", desktop: 307, mobile: 350 },
{ date: "2024-06-16", desktop: 371, mobile: 310 },
{ date: "2024-06-17", desktop: 475, mobile: 520 },
{ date: "2024-06-18", desktop: 107, mobile: 170 },
{ date: "2024-06-19", desktop: 341, mobile: 290 },
{ date: "2024-06-20", desktop: 408, mobile: 450 },
{ date: "2024-06-21", desktop: 169, mobile: 210 },
{ date: "2024-06-22", desktop: 317, mobile: 270 },
{ date: "2024-06-23", desktop: 480, mobile: 530 },
{ date: "2024-06-24", desktop: 132, mobile: 180 },
{ date: "2024-06-25", desktop: 141, mobile: 190 },
{ date: "2024-06-26", desktop: 434, mobile: 380 },
{ date: "2024-06-27", desktop: 448, mobile: 490 },
{ date: "2024-06-28", desktop: 149, mobile: 200 },
{ date: "2024-06-29", desktop: 103, mobile: 160 },
{ date: "2024-06-30", desktop: 446, mobile: 400 },
]
data = {
labels: chartData.map(d => d.date),
datasets: [{
label: 'Desktop',
data: chartData.map(d => d.desktop),
backgroundColor: themeColors.chart2,
borderWidth: 0,
borderRadius: 0, // Stacking usually looks better without individual radius
}, {
label: 'Mobile',
data: chartData.map(d => d.mobile),
backgroundColor: themeColors.chart1,
borderWidth: 0,
borderRadius: 0, // Stacking usually looks better without individual radius
}]
};
ctx = document.getElementById('chart-bar-interactive');
// Restore helper function to format date for labels/tooltips
const formatDateLabel = (dateString) => {
try {
const date = new Date(dateString + 'T00:00:00');
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); // e.g., "Apr 1"
} catch (event) {
return dateString; // Fallback
}
};
// Restore data preparation with formatted date labels
data = {
labels: chartData.map(d => formatDateLabel(d.date)),
datasets: [{
label: 'Desktop',
data: chartData.map(d => d.desktop),
backgroundColor: themeColors.chart4,
borderWidth: 0,
borderRadius: 0,
}, {
label: 'Mobile',
data: chartData.map(d => d.mobile),
backgroundColor: themeColors.chart5,
borderWidth: 0,
borderRadius: 0,
}]
};
config = {
type: 'bar',
data: data,
options: {
interaction: {
mode: 'index',
axis: 'x',
intersect: false
},
scales: {
y: {
stacked: true,
border: { display: false },
grid: {
color: setAlpha(themeColors.border, 0.5),
lineWidth: 1
},
ticks: { display: false }
},
x: {
stacked: true,
grid: { display: false },
ticks: {
color: themeColors.mutedForeground,
callback: function(value, index, ticks) {
const label = this.getLabelForValue(value);
return index % 7 === 0 ? label : null;
},
maxRotation: 0,
minRotation: 0
}
}
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
position: 'nearest',
external: externalTooltipHandler,
}
}
}
};
new Chart(ctx, config);
// Same chart but with line chart
// We start off from the previous config and just change the type to line
ctx = document.getElementById('chart-line-interactive');
// Prepare data with formatted date labels AND offset for Mobile
const desktopData = chartData.map(d => d.desktop + 400); // Store raw desktop data
const mobileDataOffset = chartData.map(d => d.mobile); // Store offset mobile data
// Prepare data with gradient background
data = {
labels: chartData.map(d => formatDateLabel(d.date)),
datasets: [{
label: 'Desktop',
data: desktopData,
borderColor: themeColors.chart1,
// Make backgroundColor scriptable
backgroundColor: function(context) {
const chart = context.chart;
const { ctx, chartArea } = chart;
// Use the helper function defined above
return createGradient(ctx, chartArea, themeColors.chart1);
},
borderWidth: 1,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBackgroundColor: themeColors.chart1,
pointHoverBorderColor: themeColors.chart1,
fill: 'start' // Fill from the start (usually y=0 or axis baseline)
}, {
label: 'Mobile (Offset)', // Adjusted label
data: mobileDataOffset,
borderColor: themeColors.chart2,
// Make backgroundColor scriptable
backgroundColor: function(context) {
const chart = context.chart;
const { ctx, chartArea } = chart;
// Use the helper function defined above
return createGradient(ctx, chartArea, themeColors.chart2);
},
borderWidth: 1,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBackgroundColor: themeColors.chart2,
pointHoverBorderColor: themeColors.chart2,
fill: 'start'
}]
};
// Add the createGradient helper function before the data object
function createGradient(ctx, chartArea, color) {
if (!chartArea) {
return setAlpha(color, 0.1); // Fallback during initial setup
}
const gradient = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
gradient.addColorStop(0, setAlpha(color, 0.5)); // Start color at top
gradient.addColorStop(1, setAlpha(color, 0)); // End color (transparent) at bottom
return gradient;
}
// Calculate min/max for Y-axis scaling using the PLOTTED data
const allPlottedValues = [...desktopData, ...mobileDataOffset]; // Combine actual plotted values
const dataMin = Math.min(...allPlottedValues);
const dataMax = Math.max(...allPlottedValues);
const yPadding = (dataMax - dataMin) * 0.1; // Add 10% padding
config = {
type: 'line',
data: data,
options: {
maintainAspectRatio: false,
aspectRatio: 1,
tension: 0.4, // Keep tension from original line config
interaction: {
mode: 'index', // Use 'index' mode for better hover on multiple lines
axis: 'x',
intersect: false
},
scales: {
y: {
border: { display: false },
grid: {
color: setAlpha(themeColors.border, 0.5),
lineWidth: 1
},
ticks: { display: false },
// Set min and max for Y-axis
min: Math.max(0, dataMin - yPadding), // Ensure min is not negative
max: dataMax + yPadding
},
x: {
grid: { display: false },
ticks: {
color: themeColors.mutedForeground,
// Restore callback to reduce ticks
callback: function(value, index, ticks) {
const label = this.getLabelForValue(value);
return index % 7 === 0 ? label : null;
},
maxRotation: 0,
minRotation: 0
}
}
},
plugins: {
legend: { display: false },
tooltip: {
enabled: false,
position: 'nearest',
external: externalTooltipHandler,
}
}
}
};
new Chart(ctx, config);
const chartDataDoughnut = [
{ browser: "chrome", visitors: 275, fill: themeColors.chart1 }, // Use resolved colors
{ browser: "safari", visitors: 200, fill: themeColors.chart2 },
{ browser: "firefox", visitors: 287, fill: themeColors.chart3 },
{ browser: "edge", visitors: 173, fill: themeColors.chart4 },
{ browser: "other", visitors: 190, fill: themeColors.chart5 },
]
// Prepare data for the doughnut chart
const doughnutData = {
labels: chartDataDoughnut.map(d => d.browser),
datasets: [{
label: 'Visitors by Browser',
data: chartDataDoughnut.map(d => d.visitors),
backgroundColor: chartDataDoughnut.map(d => d.fill),
borderColor: themeColors.background, // Use background for border
borderWidth: 0, // Add border between segments
hoverBorderColor: themeColors.background,
hoverBorderWidth: 0
}]
};
// Configure the doughnut chart
const doughnutConfig = {
type: 'doughnut',
data: doughnutData,
options: {
cutout: '70%', // Adjust hole size
plugins: {
legend: {
display: false, // Hide default legend
},
tooltip: {
enabled: false, // Keep this false as we use external handler
position: 'nearest', // Keep this for external handler logic
external: externalTooltipHandler, // Use the existing handler
},
centerText: {} // Keep the custom plugin for center text
}
}
};
// Assume you have a canvas with id="chart-doughnut" in your HTML
const doughnutCtx = document.getElementById('chart-doughnut').getContext('2d');
new Chart(doughnutCtx, doughnutConfig);
</script>
+227
View File
@@ -0,0 +1,227 @@
# Checkbox
<Preview>
<div role="group" class="fieldset">
<div role="group" class="field" data-orientation="horizontal">
<input type="checkbox" id="terms-checkbox" name="terms-checkbox" class="input" />
<label for="terms-checkbox">Accept terms and conditions</label>
</div>
<div role="group" class="field" data-orientation="horizontal">
<input type="checkbox" id="terms-checkbox-2" name="terms-checkbox-2" class="input" checked />
<section>
<label for="terms-checkbox-2">Accept terms and conditions</label>
<p>By clicking this checkbox, you agree to the terms.</p>
</section>
</div>
<div role="group" class="field" data-orientation="horizontal" data-disabled>
<input type="checkbox" id="toggle-checkbox" name="toggle-checkbox" class="input" disabled />
<label for="toggle-checkbox">Enable notifications</label>
</div>
<label>
<div role="group" class="field" data-orientation="horizontal">
<input type="checkbox" id="toggle-checkbox-2" name="toggle-checkbox-2" class="input" />
<section>
<h3>Enable notifications</h3>
<p>You can enable or disable notifications at any time.</p>
</section>
</div>
</label>
</div>
</Preview>
## Usage
<Steps>
<Step title="Include CSS">
Import Tailwind and one full Basecoat style bundle.
```css
@import "tailwindcss";
@import "basecoat-css/vega.css";
```
Or import only the base CSS, Checkbox component CSS, and one style pack.
```css
@import "tailwindcss";
@import "basecoat-css/base.css";
@import "basecoat-css/components/checkbox.css";
@import "basecoat-css/styles/vega.css";
```
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Add your checkbox HTML">
Add `class="input"` to checkbox inputs. Pair it with [Field](/components/field) and a native `<label>` for layout and labeling.
```html
<input type="checkbox" class="input" />
```
</Step>
</Steps>
## Examples
### Basic
Pair the checkbox with `field` and a native `<label>` for proper layout and labeling.
<Preview>
<div role="group" class="fieldset">
<div role="group" class="field" data-orientation="horizontal">
<input type="checkbox" id="terms-checkbox-basic" name="terms-checkbox-basic" class="input" />
<label for="terms-checkbox-basic">Accept terms and conditions</label>
</div>
</div>
</Preview>
### Description
Use a `section` and paragraph for helper text.
<Preview>
<div role="group" class="fieldset">
<div role="group" class="field" data-orientation="horizontal">
<input type="checkbox" id="terms-checkbox-desc" name="terms-checkbox-desc" class="input" checked aria-describedby="terms-checkbox-desc-description" />
<section>
<label for="terms-checkbox-desc">Accept terms and conditions</label>
<p id="terms-checkbox-desc-description">By clicking this checkbox, you agree to the terms and conditions.</p>
</section>
</div>
</div>
</Preview>
### Disabled
Use the `disabled` attribute to prevent interaction and add `data-disabled` to the `.field` component for disabled styles.
<Preview>
<div role="group" class="fieldset">
<div role="group" class="field" data-orientation="horizontal" data-disabled>
<input type="checkbox" id="toggle-checkbox-disabled" name="toggle-checkbox-disabled" class="input" disabled />
<label for="toggle-checkbox-disabled">Enable notifications</label>
</div>
</div>
</Preview>
### Invalid
Set `aria-invalid="true"` on the checkbox and `data-invalid` on the field wrapper to show invalid styles.
<Preview>
<div role="group" class="fieldset">
<div role="group" class="field" data-orientation="horizontal" data-invalid>
<input type="checkbox" id="terms-checkbox-invalid" name="terms-checkbox-invalid" class="input" aria-invalid="true" />
<label for="terms-checkbox-invalid">Accept terms and conditions</label>
</div>
</div>
</Preview>
### Group
Use multiple fields to create a checkbox list.
<Preview>
<fieldset class="fieldset">
<legend data-variant="label">Show these items on the desktop:</legend>
<p>Select the items you want to show on the desktop.</p>
<div role="group" aria-label="Desktop items" data-slot="checkbox-group">
<div role="group" class="field" data-orientation="horizontal">
<input class="input" type="checkbox" id="finder-pref-9k2-hard-disks-ljj-checkbox" name="finder-pref-9k2-hard-disks-ljj-checkbox" checked />
<label for="finder-pref-9k2-hard-disks-ljj-checkbox" class="font-normal">Hard disks</label>
</div>
<div role="group" class="field" data-orientation="horizontal">
<input class="input" type="checkbox" id="finder-pref-9k2-external-disks-1yg-checkbox" name="finder-pref-9k2-external-disks-1yg-checkbox" checked />
<label for="finder-pref-9k2-external-disks-1yg-checkbox" class="font-normal">External disks</label>
</div>
<div role="group" class="field" data-orientation="horizontal">
<input class="input" type="checkbox" id="finder-pref-9k2-cds-dvds-fzt-checkbox" name="finder-pref-9k2-cds-dvds-fzt-checkbox" />
<label for="finder-pref-9k2-cds-dvds-fzt-checkbox" class="font-normal">CDs, DVDs, and iPods</label>
</div>
<div role="group" class="field" data-orientation="horizontal">
<input class="input" type="checkbox" id="finder-pref-9k2-connected-servers-6l2-checkbox" name="finder-pref-9k2-connected-servers-6l2-checkbox" />
<label for="finder-pref-9k2-connected-servers-6l2-checkbox" class="font-normal">Connected servers</label>
</div>
</div>
</fieldset>
</Preview>
### Table
<Preview class="w-full">
<div class="table-container">
<table class="table" data-checkbox-table>
<thead>
<tr>
<th class="w-8"><input type="checkbox" id="select-all-checkbox" name="select-all-checkbox" class="input" aria-label="Select all" /></th>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr data-state="selected">
<td><input type="checkbox" id="row-1-checkbox" name="row-1-checkbox" class="input" aria-label="Select Sarah Chen" checked /></td>
<td class="font-medium">Sarah Chen</td>
<td>sarah.chen@example.com</td>
<td>Admin</td>
</tr>
<tr>
<td><input type="checkbox" id="row-2-checkbox" name="row-2-checkbox" class="input" aria-label="Select Marcus Rodriguez" /></td>
<td class="font-medium">Marcus Rodriguez</td>
<td>marcus.rodriguez@example.com</td>
<td>User</td>
</tr>
<tr>
<td><input type="checkbox" id="row-3-checkbox" name="row-3-checkbox" class="input" aria-label="Select Priya Patel" /></td>
<td class="font-medium">Priya Patel</td>
<td>priya.patel@example.com</td>
<td>User</td>
</tr>
<tr>
<td><input type="checkbox" id="row-4-checkbox" name="row-4-checkbox" class="input" aria-label="Select David Kim" /></td>
<td class="font-medium">David Kim</td>
<td>david.kim@example.com</td>
<td>Editor</td>
</tr>
</tbody>
</table>
</div>
</Preview>
## RTL
Add `dir="rtl"` to the field group or an ancestor.
<Preview>
<div role="group" class="fieldset" dir="rtl">
<div role="group" class="field" data-orientation="horizontal">
<input type="checkbox" id="terms-checkbox-rtl" name="terms-checkbox" class="input" />
<label for="terms-checkbox-rtl">قبول الشروط والأحكام</label>
</div>
<div role="group" class="field" data-orientation="horizontal">
<input type="checkbox" id="terms-checkbox-2-rtl" name="terms-checkbox-2" class="input" checked aria-describedby="terms-checkbox-2-rtl-description" />
<section>
<label for="terms-checkbox-2-rtl">قبول الشروط والأحكام</label>
<p id="terms-checkbox-2-rtl-description">بالنقر على هذا المربع، فإنك توافق على الشروط.</p>
</section>
</div>
<div role="group" class="field" data-orientation="horizontal" data-disabled>
<input type="checkbox" id="toggle-checkbox-rtl" name="toggle-checkbox" class="input" disabled />
<label for="toggle-checkbox-rtl">تفعيل الإشعارات</label>
</div>
<label>
<div role="group" class="field" data-orientation="horizontal">
<input type="checkbox" id="toggle-checkbox-2-rtl" name="toggle-checkbox-2" class="input" />
<section>
<h3>تفعيل الإشعارات</h3>
<p>يمكنك تفعيل أو إلغاء تفعيل الإشعارات في أي وقت.</p>
</section>
</div>
</label>
</div>
</Preview>
-101
View File
@@ -1,101 +0,0 @@
---
layout: layouts/page.njk
title: Checkbox
description: A control that allows the user to toggle between checked and not checked.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Disabled
id: example-disabled
- label: With text
id: example-with-text
- label: Form
id: example-form
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<label class="label gap-3">
<input type="checkbox" class="input">
Accept terms and conditions
</label>{% endset %}
{{ code_preview("checkbox", code) }}
<h2 id="usage">Usage</h2>
<section class="prose">
<p>Add the <code>input</code> class to your <code>&lt;input type="radio"&gt;</code> elements or have a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>). You can also set the <code>aria-invalid</code> attribute to <code>true</code> to make the input invalid.</p>
</section>
{% set code %}<input type="checkbox" class="input">{% endset %}
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-disabled"><a href="#example-disabled">Disabled</a></h3>
{% set code %}<label class="label gap-3">
<input type="checkbox" class="input" disabled>
Accept terms and conditions
</label>{% endset %}
{{ code_preview("checkbox-disabled", code) }}
<h3 id="example-with-text"><a href="#example-with-text">With text</a></h3>
{% set code %}<div class="flex items-start gap-3">
<input type="checkbox" id="checkbox-with-text" class="input">
<div class="grid gap-2">
<label for="checkbox-with-text" class="label">Accept terms and conditions</label>
<p class="text-muted-foreground text-sm">By clicking this checkbox, you agree to the terms and conditions.</p>
</div>
</div>{% endset %}
{{ code_preview("checkbox-with-text", code) }}
<h3 id="example-form"><a href="#example-form">Form</a></h3>
{% set code %}<form class="form flex flex-row items-start gap-3 rounded-md border p-4 shadow-xs">
<input type="checkbox" id="checkbox-form-1">
<div class="flex flex-col gap-1">
<label for="checkbox-form-1" class="leading-snug">Use different settings for my mobile devices</label>
<p class="text-muted-foreground text-sm leading-snug">You can manage your mobile notifications in the mobile settings page.</p>
</div>
</form>{% endset %}
{{ code_preview("input-form", code) }}
{% set code %}<form class="form flex flex-col gap-4">
<header>
<label for="demo-form-checkboxes" class="text-base leading-normal">Sidebar</label>
<p class="text-muted-foreground text-sm">Select the items you want to display in the sidebar.</p>
</header>
<fieldset id="demo-form-checkboxes" class="flex flex-col gap-2">
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="1" checked>
Recents
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="2" checked>
Home
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="3">
Applications
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="4">
Desktop
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="5">
Download
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="6">
Documents
</label>
</fieldset>
</form>{% endset %}
{{ code_preview("input-form", code) }}
+353
View File
@@ -0,0 +1,353 @@
# Combobox
<Preview>
<div id="framework-combobox" class="combobox">
<input type="text" role="combobox" placeholder="Select a framework" autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" aria-expanded="false" aria-controls="framework-combobox-listbox" /><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="combobox-trigger-icon"><path d="m6 9 6 6 6-6" /></svg>
<div id="framework-combobox-popover" data-popover aria-hidden="true">
<div role="listbox" id="framework-combobox-listbox" aria-orientation="vertical" data-empty="No items found.">
<div role="option" data-value="Next.js">Next.js</div>
<div role="option" data-value="SvelteKit">SvelteKit</div>
<div role="option" data-value="Nuxt.js">Nuxt.js</div>
<div role="option" data-value="Remix">Remix</div>
<div role="option" data-value="Astro">Astro</div>
</div>
</div>
<input type="hidden" name="framework" value="" />
</div>
</Preview>
## Usage
<Callout title="Template macros available" icon="code" action={{ label: "Read more", href: "/cli#combobox" }}>
This component ships a `combobox()` macro for Jinja and Nunjucks.
</Callout>
<Steps>
<Step title="Include CSS">
Import Tailwind and one full Basecoat style bundle.
```css
@import "tailwindcss";
@import "basecoat-css/vega.css";
```
Or import only the base CSS, Combobox component CSS, and one style pack.
```css
@import "tailwindcss";
@import "basecoat-css/base.css";
@import "basecoat-css/components/combobox.css";
@import "basecoat-css/styles/vega.css";
```
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Include JavaScript">
Copy or serve the full Basecoat JavaScript bundle.
```html
<script src="/assets/js/all.min.js" defer></script>
```
Or copy or serve the Basecoat runtime and Combobox script.
```html
<script src="/assets/js/basecoat.min.js" defer></script>
<script src="/assets/js/combobox.min.js" defer></script>
```
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Add your combobox HTML">
Combobox is input-first. The visible input filters the list; the hidden input stores the submitted value.
```html
<div id="framework-combobox" class="combobox w-[240px]">
<input type="text" role="combobox" placeholder="Select a framework" autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" aria-expanded="false" aria-controls="framework-combobox-listbox" /><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="combobox-trigger-icon"><path d="m6 9 6 6 6-6" /></svg>
<div id="framework-combobox-popover" data-popover aria-hidden="true">
<div role="listbox" id="framework-combobox-listbox" aria-orientation="vertical" data-empty="No items found.">
<div role="option" data-value="Next.js">Next.js</div>
<div role="option" data-value="SvelteKit">SvelteKit</div>
<div role="option" data-value="Nuxt.js">Nuxt.js</div>
<div role="option" data-value="Remix">Remix</div>
<div role="option" data-value="Astro">Astro</div>
</div>
</div>
<input type="hidden" name="framework" value="" />
</div>
```
</Step>
</Steps>
### HTML structure
<dl>
<dt><code>&lt;div class="combobox"&gt;</code></dt>
<dd>
Root element. Add <code>data-auto-highlight="true"</code> to make the first visible option active when the list opens or filters. Add <code>data-format="object"</code> to serialize selected values as objects. Add <code>data-filter="manual"</code> when your app owns filtering.
<dl>
<dt><code>&lt;input type="text" role="combobox"&gt;</code></dt>
<dd>The editable control. It can be a direct child, inside <code>.input-group</code>, or inside <code>data-popover</code> for popup mode.</dd>
<dt><code>&lt;button aria-haspopup="listbox"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Opens the list from an input group or popup trigger. In popup mode, include a child with <code>data-value</code> and optional <code>data-placeholder</code> to display the selected value.</dd>
<dt><code>&lt;button data-clear&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Clears the current selection. Put the clear icon inside the button.</dd>
<dt><code>&lt;svg class="combobox-trigger-icon"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Decorative trailing icon for input-style comboboxes. Replace the SVG to customize the icon.</dd>
<dt><code>&lt;div data-popover&gt;</code></dt>
<dd>The suggestions popup. Use <code>data-side</code> and <code>data-align</code> like Popover.</dd>
<dt><code>&lt;div role="listbox"&gt;</code></dt>
<dd>The suggestions list. Add <code>aria-multiselectable="true"</code> in multiple mode. The component uses that attribute to render selected chips around the input.</dd>
<dt><code>&lt;div role="option" data-value="..."&gt;</code></dt>
<dd>Selectable option. Options can contain custom HTML; use <code>data-label</code> when the input should display different text than the rendered content.</dd>
<dt><code>&lt;input type="hidden"&gt;</code></dt>
<dd>Submitted value. Single mode stores a string; multiple mode stores a JSON array. With <code>data-format="object"</code>, selected values are stored as objects.</dd>
</dl>
</dd>
</dl>
### JavaScript API
| API | Type | Description |
| --- | --- | --- |
| `change` | Event | Dispatched on selection changes with `event.detail.value` and `event.detail.selected`. |
| `combobox.setValue(value)` | Method | Sets the selected value from a plain value or `{ value, label }` object. |
| `combobox.clear()` | Method | Clears the selected value. |
| `combobox.refresh()` | Method | Rescans options after children change inside the existing `role="listbox"` element. |
## Examples
### Multiple
<Preview class="w-full max-w-xs">
<div id="frameworks-combobox" class="combobox w-full" data-auto-highlight="true">
<input type="text" role="combobox" autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" aria-expanded="false" aria-controls="frameworks-combobox-listbox" />
<div id="frameworks-combobox-popover" data-popover aria-hidden="true">
<div role="listbox" id="frameworks-combobox-listbox" aria-orientation="vertical" aria-multiselectable="true" data-empty="No items found.">
<div role="option" data-value="Next.js">Next.js</div>
<div role="option" data-value="SvelteKit">SvelteKit</div>
<div role="option" data-value="Nuxt.js">Nuxt.js</div>
<div role="option" data-value="Remix">Remix</div>
<div role="option" data-value="Astro">Astro</div>
</div>
</div>
<input type="hidden" name="frameworks" value='["Next.js"]' />
</div>
</Preview>
### Clear button
<Preview>
<div id="clear-combobox" class="combobox">
<div class="input-group">
<input type="text" role="combobox" placeholder="Select a framework" autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" aria-expanded="false" aria-controls="clear-combobox-listbox" />
<span role="group" data-align="inline-end">
<button type="button" data-clear aria-label="Clear selection"><svg class="size-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
<button type="button" class="btn" data-variant="ghost" data-size="icon-xs" aria-label="Open suggestions" aria-haspopup="listbox" aria-expanded="false" aria-controls="clear-combobox-listbox"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg></button>
</span>
</div>
<div id="clear-combobox-popover" data-popover aria-hidden="true">
<div role="listbox" id="clear-combobox-listbox" aria-orientation="vertical" data-empty="No items found.">
<div role="option" data-value="Next.js">Next.js</div>
<div role="option" data-value="SvelteKit">SvelteKit</div>
<div role="option" data-value="Nuxt.js">Nuxt.js</div>
<div role="option" data-value="Remix">Remix</div>
<div role="option" data-value="Astro">Astro</div>
</div>
</div>
<input type="hidden" name="framework-clear" value="Next.js" />
</div>
</Preview>
### Groups
<Preview>
<div id="timezone-combobox" class="combobox">
<input type="text" role="combobox" placeholder="Select a timezone" autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" aria-expanded="false" aria-controls="timezone-combobox-listbox" /><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="combobox-trigger-icon"><path d="m6 9 6 6 6-6" /></svg>
<div id="timezone-combobox-popover" data-popover aria-hidden="true">
<div role="listbox" id="timezone-combobox-listbox" class="scrollbar-sm" aria-orientation="vertical" data-empty="No timezones found.">
<div role="group" aria-labelledby="timezone-americas">
<div role="heading" id="timezone-americas">Americas</div>
<div role="option" data-value="(GMT-5) New York">(GMT-5) New York</div>
<div role="option" data-value="(GMT-8) Los Angeles">(GMT-8) Los Angeles</div>
<div role="option" data-value="(GMT-6) Chicago">(GMT-6) Chicago</div>
<div role="option" data-value="(GMT-5) Toronto">(GMT-5) Toronto</div>
<div role="option" data-value="(GMT-8) Vancouver">(GMT-8) Vancouver</div>
<div role="option" data-value="(GMT-3) São Paulo">(GMT-3) São Paulo</div>
</div>
<hr role="separator" />
<div role="group" aria-labelledby="timezone-europe">
<div role="heading" id="timezone-europe">Europe</div>
<div role="option" data-value="(GMT+0) London">(GMT+0) London</div>
<div role="option" data-value="(GMT+1) Paris">(GMT+1) Paris</div>
<div role="option" data-value="(GMT+1) Berlin">(GMT+1) Berlin</div>
<div role="option" data-value="(GMT+1) Rome">(GMT+1) Rome</div>
<div role="option" data-value="(GMT+1) Madrid">(GMT+1) Madrid</div>
<div role="option" data-value="(GMT+1) Amsterdam">(GMT+1) Amsterdam</div>
</div>
<hr role="separator" />
<div role="group" aria-labelledby="timezone-asia-pacific">
<div role="heading" id="timezone-asia-pacific">Asia/Pacific</div>
<div role="option" data-value="(GMT+9) Tokyo">(GMT+9) Tokyo</div>
<div role="option" data-value="(GMT+8) Shanghai">(GMT+8) Shanghai</div>
<div role="option" data-value="(GMT+8) Singapore">(GMT+8) Singapore</div>
<div role="option" data-value="(GMT+4) Dubai">(GMT+4) Dubai</div>
<div role="option" data-value="(GMT+11) Sydney">(GMT+11) Sydney</div>
<div role="option" data-value="(GMT+9) Seoul">(GMT+9) Seoul</div>
</div>
</div>
</div>
<input type="hidden" value="" />
</div>
</Preview>
### Custom items
<Preview>
<div id="country-combobox" class="combobox">
<input type="text" role="combobox" placeholder="Search countries" autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" aria-expanded="false" aria-controls="country-combobox-listbox" /><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="combobox-trigger-icon"><path d="m6 9 6 6 6-6" /></svg>
<div id="country-combobox-popover" data-popover aria-hidden="true">
<div role="listbox" id="country-combobox-listbox" aria-orientation="vertical" data-empty="No items found.">
<div role="option" data-value="argentina" data-label="Argentina" data-filter="Argentina South America"><span class="flex flex-col"><span>Argentina</span><span class="text-muted-foreground text-xs">South America</span></span></div>
<div role="option" data-value="australia" data-label="Australia" data-filter="Australia Oceania"><span class="flex flex-col"><span>Australia</span><span class="text-muted-foreground text-xs">Oceania</span></span></div>
<div role="option" data-value="japan" data-label="Japan" data-filter="Japan Asia"><span class="flex flex-col"><span>Japan</span><span class="text-muted-foreground text-xs">Asia</span></span></div>
</div>
</div>
<input type="hidden" value="" />
</div>
</Preview>
### Invalid
<Preview>
<div role="group" class="field" data-invalid="true">
<label for="invalid-combobox-input">Framework</label>
<div id="invalid-combobox" class="combobox">
<input type="text" role="combobox" placeholder="Select a framework" autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" aria-expanded="false" aria-controls="invalid-combobox-listbox" id="invalid-combobox-input" aria-invalid="true" /><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="combobox-trigger-icon"><path d="m6 9 6 6 6-6" /></svg>
<div id="invalid-combobox-popover" data-popover aria-hidden="true">
<div role="listbox" id="invalid-combobox-listbox" aria-orientation="vertical" data-empty="No items found.">
<div role="option" data-value="Next.js">Next.js</div>
<div role="option" data-value="SvelteKit">SvelteKit</div>
<div role="option" data-value="Nuxt.js">Nuxt.js</div>
<div role="option" data-value="Remix">Remix</div>
<div role="option" data-value="Astro">Astro</div>
</div>
</div>
<input type="hidden" value="" />
</div>
<p role="alert">Select a framework to continue.</p>
</div>
</Preview>
### Disabled
<Preview>
<div id="disabled-combobox" class="combobox">
<input type="text" role="combobox" placeholder="Select a framework" autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" aria-expanded="false" aria-controls="disabled-combobox-listbox" disabled /><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="combobox-trigger-icon"><path d="m6 9 6 6 6-6" /></svg>
<div id="disabled-combobox-popover" data-popover aria-hidden="true">
<div role="listbox" id="disabled-combobox-listbox" aria-orientation="vertical">
<div role="option" data-value="Next.js">Next.js</div>
</div>
</div>
<input type="hidden" value="" />
</div>
</Preview>
### Auto highlight
Auto highlight makes the first visible option active when the list opens or filters, so pressing Enter can select it without arrowing first.
<Preview>
<div id="auto-highlight-combobox" class="combobox" data-auto-highlight="true">
<input type="text" role="combobox" placeholder="Select a framework" autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" aria-expanded="false" aria-controls="auto-highlight-combobox-listbox" /><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="combobox-trigger-icon"><path d="m6 9 6 6 6-6" /></svg>
<div id="auto-highlight-combobox-popover" data-popover aria-hidden="true">
<div role="listbox" id="auto-highlight-combobox-listbox" aria-orientation="vertical" data-empty="No items found.">
<div role="option" data-value="Next.js">Next.js</div>
<div role="option" data-value="SvelteKit">SvelteKit</div>
<div role="option" data-value="Nuxt.js">Nuxt.js</div>
<div role="option" data-value="Remix">Remix</div>
<div role="option" data-value="Astro">Astro</div>
</div>
</div>
<input type="hidden" value="" />
</div>
</Preview>
### Popup
<Preview>
<div id="popup-combobox" class="combobox">
<button type="button" class="btn w-64 justify-between font-normal" data-variant="outline" aria-haspopup="listbox" aria-expanded="false" aria-controls="popup-combobox-listbox">
<span data-value data-placeholder="Select country">Select country</span>
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
</button>
<div id="popup-combobox-popover" data-popover aria-hidden="true">
<div class="input-group">
<input type="text" role="combobox" placeholder="Search" autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" aria-expanded="false" aria-controls="popup-combobox-listbox" />
</div>
<div role="listbox" id="popup-combobox-listbox" aria-orientation="vertical" data-empty="No items found.">
<div role="option" data-value="argentina" data-label="Argentina">Argentina</div>
<div role="option" data-value="australia" data-label="Australia">Australia</div>
<div role="option" data-value="brazil" data-label="Brazil">Brazil</div>
<div role="option" data-value="canada" data-label="Canada">Canada</div>
<div role="option" data-value="japan" data-label="Japan">Japan</div>
<div role="option" data-value="united-states" data-label="United States">United States</div>
</div>
</div>
<input type="hidden" value="" />
</div>
</Preview>
### Input group
<Preview>
<div id="input-group-combobox" class="combobox w-60">
<div class="input-group">
<span data-align="inline-start"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg></span>
<input type="text" role="combobox" placeholder="Select a timezone" autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" aria-expanded="false" aria-controls="input-group-combobox-listbox" />
<button type="button" class="btn" data-variant="ghost" data-size="icon-xs" data-align="inline-end" aria-label="Open suggestions" aria-haspopup="listbox" aria-expanded="false" aria-controls="input-group-combobox-listbox"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg></button>
</div>
<div id="input-group-combobox-popover" data-popover aria-hidden="true">
<div role="listbox" id="input-group-combobox-listbox" aria-orientation="vertical" data-empty="No timezones found.">
<div role="group" aria-labelledby="input-group-americas">
<div role="heading" id="input-group-americas">Americas</div>
<div role="option" data-value="(GMT-5) New York">(GMT-5) New York</div>
<div role="option" data-value="(GMT-8) Los Angeles">(GMT-8) Los Angeles</div>
</div>
<hr role="separator" />
<div role="group" aria-labelledby="input-group-europe">
<div role="heading" id="input-group-europe">Europe</div>
<div role="option" data-value="(GMT+0) London">(GMT+0) London</div>
<div role="option" data-value="(GMT+1) Paris">(GMT+1) Paris</div>
</div>
</div>
</div>
<input type="hidden" value="" />
</div>
</Preview>
### RTL
<Preview>
<div dir="rtl" class="field w-full max-w-xs">
<label for="rtl-combobox-input">الفئات</label>
<div id="rtl-combobox" class="combobox w-full" data-auto-highlight="true">
<input id="rtl-combobox-input" type="text" role="combobox" placeholder="أضف فئات" autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" aria-expanded="false" aria-controls="rtl-combobox-listbox" />
<div id="rtl-combobox-popover" data-popover aria-hidden="true">
<div role="listbox" id="rtl-combobox-listbox" aria-orientation="vertical" aria-multiselectable="true" data-empty="لم يتم العثور على فئات.">
<div role="option" data-value="technology" data-label="التكنولوجيا">التكنولوجيا</div>
<div role="option" data-value="design" data-label="التصميم">التصميم</div>
<div role="option" data-value="business" data-label="الأعمال">الأعمال</div>
<div role="option" data-value="marketing" data-label="التسويق">التسويق</div>
</div>
</div>
<input type="hidden" value='["technology"]' />
</div>
</div>
</Preview>
-293
View File
@@ -1,293 +0,0 @@
---
layout: layouts/page.njk
title: Combobox
description: Autocomplete input and command palette with a list of suggestions.
toc:
- label: Usage
id: usage
children:
- label: HTML + JavaScript
id: usage-html-js
children:
- label: "Step 1: Include the JavaScript files"
id: usage-html-js-1
- label: "Step 2: Add your combobox HTML"
id: usage-html-js-2
- label: HTML structure
id: usage-html-js-3
- label: JavaScript events
id: usage-html-js-4
- label: JavaScript methods
id: usage-html-js-5
- label: Jinja and Nunjucks
id: usage-macro
- label: Examples
id: examples
children:
- label: Scrollable
id: example-scrollable
- label: Top side
id: example-top
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "select.njk" import select %}
{% set options_frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"] %}
{% set code_html %}
{% call select(
trigger_attrs={"class": "w-[200px]"},
listbox_attrs={"data-empty": "No framework found."},
is_combobox=true,
search_placeholder="Search framework..."
) %}
{% for framework in options_frameworks %}
<div role="option" data-value="{{ framework }}">{{ framework }}</div>
{% endfor %}
{% endcall %}
{% endset %}
{{ code_preview("combobox", code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-html-js"><a href="#usage-html-js">HTML + JavaScript</a></h3>
<h4 id="usage-html-js-1"><a href="#usage-html-js-1">Step 1: Include the JavaScript files</a></h4>
<section class="prose">
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/select.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/js/select.js" target="_blank">
select.js
{% lucide "arrow-right" %}
</a>
</div>
<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add your combobox HTML</a></h4>
<section class="prose">
<p>Combobox uses the same markup and JavaScript as the <a href="/components/select#usage-html-js">Select component</a>, the only difference being the search box at the top of the listbox:</p>
</section>
{{ code_block(code_html | prettyHtml, "html") }}
<h4 id="usage-html-js-3"><a href="#usage-html-js-3">HTML structure</a></h4>
<section class="prose">
<dl>
<dt><code class="highlight language-html">&lt;div class="select"&gt;</code></dt>
<dd>Wraps around the entire component.
<dl>
<dt><code class="highlight language-html">&lt;button type="button" popovertarget="{ POPOVER_ID }"&gt;</code></dt>
<dd>
<p>The trigger to open the popover, can also have the following attributes:</p>
<ul>
<li><code>id="{BUTTON_ID}"</code>: linked to by the <code>aria-labelledby</code> attribute of the listbox.</li>
<li><code>aria-haspopup="listbox"</code>: indicates that the button opens a listbox.</li>
<li><code>aria-controls="{ LISTBOX_ID }"</code>: points to the listbox's id.</li>
<li><code>aria-expanded="false"</code>: tracks the popover's state.</li>
<li><code>aria-activedescendant="{ OPTION_ID }"</code>: points to the active option's id.</li>
</ul>
</dd>
<dt><code class="highlight language-html">&lt;div popover class="popover" id="{ POPOVER_ID }"&gt;</code></dt>
<dd>As with the <a href="/components/popover">Popover</a> component, you can set up the side and alignment of the popover using the <code>data-side</code> and <code>data-align</code> attributes.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dd>Contains the search input to filter the options in the listbox.</dd>
<dt><code class="highlight language-html">&lt;div role="listbox"&gt;</code></dt>
<dd>The listbox containing the options. Should have the following attributes:
<ul>
<li><code>id="{ LISTBOX_ID }"</code>: refered to by the <code>aria-controls</code> attribute of the trigger.</li>
<li><code>aria-labelledby="{ BUTTON_ID }"</code>: linked to by the button's <code>id</code> attribute.</li>
</ul>
<dl>
<dt><code class="highlight language-html">&lt;div role="option" data-value="{ VALUE }"&gt;</code></dt>
<dd>Option that can be selected. Should have a unique id if you use the <code>aria-activedescendant</code> attribute on the trigger. Optional attributes:
<ul>
<li><code>data-filter</code>: custom text used for filtering (if different from displayed content, e.g., <code>data-filter="united states"</code>)</li>
<li><code>data-keywords</code>: additional keywords for enhanced filtering (e.g., <code>data-keywords="bread fruit"</code>)</li>
<li><code>data-force</code>: forces the option to always display, regardless of filter (e.g., <code>data-force</code>)</li>
<li><code>aria-disabled="true"</code>: marks option as disabled (excluded from selection and keyboard navigation)</li>
</ul>
</dd>
<dt><code class="highlight language-html">&lt;hr role="separator"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Separator between groups/options.</dd>
<dt><code class="highlight language-html">&lt;div role="group"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Group of options, can have a <code>aria-labelledby</code> attribute to link to a heading.</dd>
<dt><code class="highlight language-html">&lt;span role="heading"&gt;</code></dt>
<dd>Group heading, must have an <code>id</code> attribute if you use the <code>aria-labelledby</code> attribute on the group.</dd>
</dl>
</dd>
</dl>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;input type="hidden" name="{ NAME }" value="{ VALUE }"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The hidden input that holds the value of the field (if needed).</dd>
</dl>
</section>
<h4 id="usage-html-js-4"><a href="#usage-html-js-4">JavaScript events</a></h4>
<section class="prose">
<dl>
<dt><code>basecoat:initialized</code></dt>
<dd>Once the component is fully initialized, it dispatches a custom (non-bubbling) <code>basecoat:initialized</code> event on itself.</dd>
<dt><code>basecoat:popover</code></dt>
<dd>When the popover opens, the component dispatches a custom (non-bubbling) <code>basecoat:popover</code> event on <code>document</code>. Other popover components (Combobox, Dropdown Menu, Popover and Select) listen for this to close any open popovers.</dd>
<dt><code>change</code></dt>
<dd>When the selected value changes, the component dispatches a custom (bubbling) <code>change</code> event on itself, with the selected value in <code>event.detail</code> (e.g. <code>{detail: {value: "something"}}</code>).</dd>
</dl>
</section>
<h4 id="usage-html-js-5"><a href="#usage-html-js-4">JavaScript methods</a></h4>
<section class="prose">
<dl>
<dt><code>selectByValue</code></dt>
<dd>
<p>You can call this method on the component after it is initialized to select an option by value (i.e. the option with the matching <code>data-value</code> attribute):</p>
{% set code_script %}<script>
const comboboxComponent = document.querySelector('#my-combobox');
comboboxComponent.addEventListener('basecoat:initialized', () => {
comboboxComponent.selectByValue('apple');
});
</script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
</dd>
</dl>
</section>
<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>
<div class="prose">
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">select()</code> Nunjucks or Jinja macro for this component. If you use one of the macros, make sure to set <code>is_combobox</code> to <code>True</code> (or <code>true</code> for Nunjucks).</p>
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/select.html.jinja" target="_blank">
Jinja macro
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/select.njk" target="_blank">
Nunjucks macro
{% lucide "arrow-right" %}
</a>
</div>
{% set raw_code %}{% raw %}{% call select(
listbox_attrs={"data-empty": "No framework found."},
is_combobox=true
) %}
<div role="option" data-value="nextjs">Next.js</div>
<div role="option" data-value="sveltekit">SvelteKit</div>
<div role="option" data-value="nuxtjs">Nuxt.js</div>
<div role="option" data-value="remix">Remix</div>
<div role="option" data-value="astro">Astro</div>
{% endcall %}{% endraw %}{% endset %}
{{ code_block(raw_code, "jinja") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-scrollable"><a href="#example-scrollable">Scrollable</a></h3>
{% set options_timezones = [
{
label: "Americas",
timezones: [
{ value: "America/New_York", label: "(GMT-5) New York" },
{ value: "America/Los_Angeles", label: "(GMT-8) Los Angeles" },
{ value: "America/Chicago", label: "(GMT-6) Chicago" },
{ value: "America/Toronto", label: "(GMT-5) Toronto" },
{ value: "America/Vancouver", label: "(GMT-8) Vancouver" },
{ value: "America/Sao_Paulo", label: "(GMT-3) São Paulo" }
]
},
{
label: "Europe",
timezones: [
{ value: "Europe/London", label: "(GMT+0) London" },
{ value: "Europe/Paris", label: "(GMT+1) Paris" },
{ value: "Europe/Berlin", label: "(GMT+1) Berlin" },
{ value: "Europe/Rome", label: "(GMT+1) Rome" },
{ value: "Europe/Madrid", label: "(GMT+1) Madrid" },
{ value: "Europe/Amsterdam", label: "(GMT+1) Amsterdam" }
]
},
{
label: "Asia/Pacific",
timezones: [
{ value: "Asia/Tokyo", label: "(GMT+9) Tokyo" },
{ value: "Asia/Shanghai", label: "(GMT+8) Shanghai" },
{ value: "Asia/Singapore", label: "(GMT+8) Singapore" },
{ value: "Asia/Dubai", label: "(GMT+4) Dubai" },
{ value: "Australia/Sydney", label: "(GMT+11) Sydney" },
{ value: "Asia/Seoul", label: "(GMT+9) Seoul" }
]
}
] %}
{% set code %}
{% call select(
trigger_attrs={"class": "w-[200px]"},
popover_attrs={"class": "w-72"},
listbox_attrs={"data-empty": "No timezone found."},
is_combobox=true,
search_placeholder="Search timezone..."
) %}
<div class="max-h-64 overflow-y-auto scrollbar">
{% for group in options_timezones %}
<div role="group" aria-labelledby="demo-combobox-timezones-group-{{ loop.index0 }}">
<div role="heading" id="demo-combobox-timezones-group-{{ loop.index0 }}">{{ group.label }}</div>
{% for timezone in group.timezones %}
<div role="option" data-value="{{ timezone.value}}">
{{ timezone.label }}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<hr role="separator">
<div role="option">
{% lucide "circle-plus" %}
Create timezone
</div>
{% endcall %}
{% endset %}
{{ code_preview("combobox-with-groups", code | prettyHtml) }}
<h3 id="example-top"><a href="#example-top">Top side</a></h3>
{% set options_frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"] %}
{% set code %}
{% call select(
trigger_attrs={"class": "w-[200px]"},
listbox_attrs={"data-empty": "No framework found."},
is_combobox=true,
search_placeholder="Search framework...",
popover_attrs={"data-side": "top"}
) %}
{% for framework in options_frameworks %}
<div role="option" data-value="{{ framework }}">{{ framework }}</div>
{% endfor %}
{% endcall %}{% endset %}
{{ code_preview("combobox", code | prettyHtml) }}
+390
View File
@@ -0,0 +1,390 @@
# Command
<Preview class="w-full max-w-sm">
<div id="demo-command-standalone" class="command rounded-lg border" aria-label="Command menu">
<header><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>
<input type="text" id="demo-command-standalone-input" placeholder="Type a command or search..." autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" role="combobox" aria-expanded="true" aria-controls="demo-command-standalone-menu" />
</header>
<div role="menu" id="demo-command-standalone-menu" aria-orientation="vertical" data-empty="No results found.">
<div role="group" aria-labelledby="suggestions">
<span role="heading" id="suggestions">Suggestions</span>
<div role="menuitem" data-filter="Calendar" data-keywords="date event schedule"><svg class="lucide lucide-calendar" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4" /><path d="M16 2v4" /><rect width="18" height="18" x="3" y="4" rx="2" /><path d="M3 10h18" /></svg>
<span>Calendar</span>
</div>
<div role="menuitem" data-filter="Search Emoji" data-keywords="emoji smile reaction"><svg class="lucide lucide-smile" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M8 14s1.5 2 4 2 4-2 4-2" /><line x1="9" x2="9.01" y1="9" y2="9" /><line x1="15" x2="15.01" y1="9" y2="9" /></svg>
<span>Search Emoji</span>
</div>
<div role="menuitem" aria-disabled="true"><svg class="lucide lucide-calculator" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="16" height="20" x="4" y="2" rx="2" /><line x1="8" x2="16" y1="6" y2="6" /><line x1="16" x2="16" y1="14" y2="18" /><path d="M16 10h.01" /><path d="M12 10h.01" /><path d="M8 10h.01" /><path d="M12 14h.01" /><path d="M8 14h.01" /><path d="M12 18h.01" /><path d="M8 18h.01" /></svg>
<span>Calculator</span>
</div>
</div>
<hr role="separator" />
<div role="group" aria-labelledby="settings">
<span role="heading" id="settings">Settings</span>
<div role="menuitem" data-filter="Profile" data-keywords="user account"><svg class="lucide lucide-user" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" /></svg>
<span>Profile</span>
<span data-shortcut>⌘P</span>
</div>
<div role="menuitem" data-filter="Billing" data-keywords="invoice payment"><svg class="lucide lucide-credit-card" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2" /><line x1="2" x2="22" y1="10" y2="10" /></svg>
<span>Billing</span>
<span data-shortcut>⌘B</span>
</div>
<div role="menuitem" data-filter="Settings" data-keywords="config preferences"><svg class="lucide lucide-settings" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /><circle cx="12" cy="12" r="3" /></svg>
<span>Settings</span>
<span data-shortcut>⌘S</span>
</div>
</div>
</div>
</div>
</Preview>
## Usage
<Callout title="Template macros available" icon="code" action={{ label: "Read more", href: "/cli#command" }}>
This component ships `command()` and `command_dialog()` macros for Jinja and Nunjucks.
</Callout>
<Steps>
<Step title="Include CSS">
Import Tailwind and one full Basecoat style bundle.
```css
@import "tailwindcss";
@import "basecoat-css/vega.css";
```
Or import only the base CSS, Command component CSS, and one style pack.
```css
@import "tailwindcss";
@import "basecoat-css/base.css";
@import "basecoat-css/components/command.css";
@import "basecoat-css/styles/vega.css";
```
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Include JavaScript">
Copy or serve the full Basecoat JavaScript bundle.
```html
<script src="/assets/js/all.min.js" defer></script>
```
Or copy or serve the Basecoat runtime and Command script.
```html
<script src="/assets/js/basecoat.min.js" defer></script>
<script src="/assets/js/command.min.js" defer></script>
```
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Add your command HTML">
```html
<div id="demo-command-standalone" class="command border" aria-label="Command menu">
<header><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>
<input type="text" id="demo-command-standalone-input" placeholder="Type a command or search..." autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" role="combobox" aria-expanded="true" aria-controls="demo-command-standalone-menu" />
</header>
<div role="menu" id="demo-command-standalone-menu" aria-orientation="vertical" data-empty="No results found.">
<div role="group" aria-labelledby="suggestions">
<span role="heading" id="suggestions">Suggestions</span>
<div role="menuitem" data-filter="Calendar" data-keywords="date event schedule"><svg class="lucide lucide-calendar" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4" /><path d="M16 2v4" /><rect width="18" height="18" x="3" y="4" rx="2" /><path d="M3 10h18" /></svg>
<span>Calendar</span>
</div>
<div role="menuitem" data-filter="Search Emoji" data-keywords="emoji smile reaction"><svg class="lucide lucide-smile" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M8 14s1.5 2 4 2 4-2 4-2" /><line x1="9" x2="9.01" y1="9" y2="9" /><line x1="15" x2="15.01" y1="9" y2="9" /></svg>
<span>Search Emoji</span>
</div>
<div role="menuitem" aria-disabled="true"><svg class="lucide lucide-calculator" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="16" height="20" x="4" y="2" rx="2" /><line x1="8" x2="16" y1="6" y2="6" /><line x1="16" x2="16" y1="14" y2="18" /><path d="M16 10h.01" /><path d="M12 10h.01" /><path d="M8 10h.01" /><path d="M12 14h.01" /><path d="M8 14h.01" /><path d="M12 18h.01" /><path d="M8 18h.01" /></svg>
<span>Calculator</span>
</div>
</div>
<hr role="separator" />
<div role="group" aria-labelledby="settings">
<span role="heading" id="settings">Settings</span>
<div role="menuitem" data-filter="Profile" data-keywords="user account"><svg class="lucide lucide-user" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" /></svg>
<span>Profile</span>
<span data-shortcut>⌘P</span>
</div>
<div role="menuitem" data-filter="Billing" data-keywords="invoice payment"><svg class="lucide lucide-credit-card" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2" /><line x1="2" x2="22" y1="10" y2="10" /></svg>
<span>Billing</span>
<span data-shortcut>⌘B</span>
</div>
<div role="menuitem" data-filter="Settings" data-keywords="config preferences"><svg class="lucide lucide-settings" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /><circle cx="12" cy="12" r="3" /></svg>
<span>Settings</span>
<span data-shortcut>⌘S</span>
</div>
</div>
</div>
</div>
```
Use `<dialog class="command-dialog">` for the command palette variant.
```html
<button class="btn" data-variant="outline" onclick="document.getElementById('command-palette').showModal()">Open</button>
<dialog id="command-palette" class="command-dialog" aria-label="Command menu">
<div class="command">
<!-- Command content -->
</div>
</dialog>
```
</Step>
</Steps>
### HTML structure
The command menu can be used standalone or inside a native dialog. The structure is:
<dl>
<dt><code>&lt;dialog class="command-dialog"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>
Dialog variant. Wraps one direct child <code>&lt;div class="command"&gt;</code>. Add <code>aria-label</code> or <code>aria-labelledby</code> for an accessible name.
<dl>
<dt><code>&lt;div class="command"&gt;</code></dt>
<dd>
Root command menu. Can also be used standalone without the dialog wrapper. Add <code>data-filter="manual"</code> when your app owns filtering, such as remote search or a local Lunr index.
<dl>
<dt><code>&lt;header&gt;</code></dt>
<dd>
Search input wrapper. The macro includes a search icon and input.
<dl>
<dt><code>&lt;input type="text" role="combobox"&gt;</code></dt>
<dd>Filter input. Use <code>aria-expanded="true"</code> and <code>aria-controls="&#123; MENU_ID &#125;"</code> to point to the command list.</dd>
</dl>
</dd>
<dt><code>&lt;div role="menu" id="&#123; MENU_ID &#125;"&gt;</code></dt>
<dd>
Command list. Set <code>data-empty</code> to customize the empty state.
<dl>
<dt><code>&lt;div role="group" aria-labelledby="&#123; HEADING_ID &#125;"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Groups related command items. Hidden automatically when every item in the group is filtered out.</dd>
<dt><code>&lt;span role="heading" id="&#123; HEADING_ID &#125;"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Group heading. Use an <code>id</code> when the parent group uses <code>aria-labelledby</code>.</dd>
<dt><code>&lt;div role="menuitem"&gt;</code> or <code>&lt;a role="menuitem"&gt;</code></dt>
<dd>Selectable command item. Use <code>&lt;a&gt;</code> for navigation and <code>&lt;div&gt;</code> for actions. Supports <code>data-filter</code>, <code>data-keywords</code>, <code>data-force</code>, <code>data-keep-command-open</code>, <code>aria-disabled="true"</code>, and <code>data-disabled="true"</code>.</dd>
<dt><code>&lt;span data-shortcut&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Shortcut hint aligned to the inline end of the item.</dd>
<dt><code>&lt;span data-indicator&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Item indicator. It becomes visible with <code>data-checked="true"</code> or <code>aria-selected="true"</code>.</dd>
<dt><code>&lt;hr role="separator"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Separator between groups/items. Hidden while filtering.</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
### JavaScript API
| API | Type | Description |
| --- | --- | --- |
| `command.refresh()` | Method | Rescans command items after children change inside the existing `role="menu"` list. |
## Examples
### Basic
A simple command menu in a dialog.
<Preview>
<button type="button" class="btn" data-variant="outline" onclick="document.getElementById('command-basic').showModal()">Open Menu</button>
<dialog id="command-basic" class="command-dialog" aria-label="Command menu" onclick="if (event.target === this) this.close()">
<div class="command">
<header><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>
<input type="text" id="command-basic-input" placeholder="Type a command or search..." autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" role="combobox" aria-expanded="true" aria-controls="command-basic-menu" />
</header>
<div role="menu" id="command-basic-menu" aria-orientation="vertical" data-empty="No results found.">
<div role="group" aria-labelledby="command-basic-suggestions">
<span role="heading" id="command-basic-suggestions">Suggestions</span>
<div role="menuitem" data-filter="Calendar">Calendar</div>
<div role="menuitem" data-filter="Search Emoji">Search Emoji</div>
<div role="menuitem" data-filter="Calculator">Calculator</div>
</div>
</div>
</div>
</dialog>
</Preview>
### Shortcuts
<Preview>
<button type="button" class="btn" data-variant="outline" onclick="document.getElementById('command-shortcuts-dialog').showModal()">Open Menu</button>
<dialog id="command-shortcuts-dialog" class="command-dialog" aria-label="Command menu" onclick="if (event.target === this) this.close()">
<div class="command">
<header><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>
<input type="text" id="command-shortcuts-input" placeholder="Type a command or search..." autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" role="combobox" aria-expanded="true" aria-controls="command-shortcuts-menu" />
</header>
<div role="menu" id="command-shortcuts-menu" aria-orientation="vertical" data-empty="No results found.">
<div role="group" aria-labelledby="command-shortcuts-settings">
<span role="heading" id="command-shortcuts-settings">Settings</span>
<div role="menuitem" data-filter="Profile" data-keywords="user account"><svg class="lucide lucide-user" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" /></svg>
<span>Profile</span>
<span data-shortcut>⌘P</span>
</div>
<div role="menuitem" data-filter="Billing" data-keywords="invoice payment"><svg class="lucide lucide-credit-card" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2" /><line x1="2" x2="22" y1="10" y2="10" /></svg>
<span>Billing</span>
<span data-shortcut>⌘B</span>
</div>
<div role="menuitem" data-filter="Settings" data-keywords="config preferences"><svg class="lucide lucide-settings" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /><circle cx="12" cy="12" r="3" /></svg>
<span>Settings</span>
<span data-shortcut>⌘S</span>
</div>
</div>
</div>
</div>
</dialog>
</Preview>
### Groups
<Preview>
<button type="button" class="btn" data-variant="outline" onclick="document.getElementById('command-groups-dialog').showModal()">Open Menu</button>
<dialog id="command-groups-dialog" class="command-dialog" aria-label="Command menu" onclick="if (event.target === this) this.close()">
<div class="command">
<header><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>
<input type="text" id="command-groups-input" placeholder="Type a command or search..." autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" role="combobox" aria-expanded="true" aria-controls="command-groups-menu" />
</header>
<div role="menu" id="command-groups-menu" aria-orientation="vertical" data-empty="No results found.">
<div role="group" aria-labelledby="command-groups-suggestions">
<span role="heading" id="command-groups-suggestions">Suggestions</span>
<div role="menuitem" data-filter="Calendar" data-keywords="date event schedule"><svg class="lucide lucide-calendar" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4" /><path d="M16 2v4" /><rect width="18" height="18" x="3" y="4" rx="2" /><path d="M3 10h18" /></svg>
<span>Calendar</span>
</div>
<div role="menuitem" data-filter="Search Emoji" data-keywords="emoji smile reaction"><svg class="lucide lucide-smile" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M8 14s1.5 2 4 2 4-2 4-2" /><line x1="9" x2="9.01" y1="9" y2="9" /><line x1="15" x2="15.01" y1="9" y2="9" /></svg>
<span>Search Emoji</span>
</div>
<div role="menuitem" data-filter="Calculator"><svg class="lucide lucide-calculator" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="16" height="20" x="4" y="2" rx="2" /><line x1="8" x2="16" y1="6" y2="6" /><line x1="16" x2="16" y1="14" y2="18" /><path d="M16 10h.01" /><path d="M12 10h.01" /><path d="M8 10h.01" /><path d="M12 14h.01" /><path d="M8 14h.01" /><path d="M12 18h.01" /><path d="M8 18h.01" /></svg>
<span>Calculator</span>
</div>
</div>
<hr role="separator" />
<div role="group" aria-labelledby="command-groups-settings">
<span role="heading" id="command-groups-settings">Settings</span>
<div role="menuitem" data-filter="Profile" data-keywords="user account"><svg class="lucide lucide-user" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" /></svg>
<span>Profile</span>
<span data-shortcut>⌘P</span>
</div>
<div role="menuitem" data-filter="Billing" data-keywords="invoice payment"><svg class="lucide lucide-credit-card" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2" /><line x1="2" x2="22" y1="10" y2="10" /></svg>
<span>Billing</span>
<span data-shortcut>⌘B</span>
</div>
<div role="menuitem" data-filter="Settings" data-keywords="config preferences"><svg class="lucide lucide-settings" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /><circle cx="12" cy="12" r="3" /></svg>
<span>Settings</span>
<span data-shortcut>⌘S</span>
</div>
</div>
</div>
</div>
</dialog>
</Preview>
### Scrollable
<Preview>
<button type="button" class="btn" data-variant="outline" onclick="document.getElementById('command-scrollable-dialog').showModal()">Open Menu</button>
<dialog id="command-scrollable-dialog" class="command-dialog" aria-label="Command menu" onclick="if (event.target === this) this.close()">
<div class="command">
<header><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>
<input type="text" id="command-scrollable-input" placeholder="Type a command or search..." autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" role="combobox" aria-expanded="true" aria-controls="command-scrollable-menu" />
</header>
<div role="menu" id="command-scrollable-menu" aria-orientation="vertical" data-empty="No results found.">
<div role="group" aria-labelledby="command-scrollable-navigation">
<span role="heading" id="command-scrollable-navigation">Navigation</span>
<div role="menuitem" data-filter="Home"><svg class="lucide lucide-home" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" /><path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /></svg><span>Home</span><span data-shortcut>⌘H</span></div>
<div role="menuitem" data-filter="Inbox"><svg class="lucide lucide-inbox" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12" /><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /></svg><span>Inbox</span><span data-shortcut>⌘I</span></div>
<div role="menuitem" data-filter="Documents"><svg class="lucide lucide-file-text" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /><path d="M14 2v5a1 1 0 0 0 1 1h5" /><path d="M10 9H8" /><path d="M16 13H8" /><path d="M16 17H8" /></svg><span>Documents</span><span data-shortcut>⌘D</span></div>
<div role="menuitem" data-filter="Folders"><svg class="lucide lucide-folder" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" /></svg><span>Folders</span><span data-shortcut>⌘F</span></div>
</div>
<hr role="separator" />
<div role="group" aria-labelledby="command-scrollable-actions">
<span role="heading" id="command-scrollable-actions">Actions</span>
<div role="menuitem" data-filter="New File"><svg class="lucide lucide-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /><path d="M12 5v14" /></svg><span>New File</span><span data-shortcut>⌘N</span></div>
<div role="menuitem" data-filter="New Folder"><svg class="lucide lucide-folder-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 10v6" /><path d="M9 13h6" /><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" /></svg><span>New Folder</span><span data-shortcut>⇧⌘N</span></div>
<div role="menuitem" data-filter="Copy"><svg class="lucide lucide-copy" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></svg><span>Copy</span><span data-shortcut>⌘C</span></div>
<div role="menuitem" data-filter="Cut"><svg class="lucide lucide-scissors" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="3" /><path d="M8.12 8.12 12 12" /><path d="M20 4 8.12 15.88" /><circle cx="6" cy="18" r="3" /><path d="M14.8 14.8 20 20" /></svg><span>Cut</span><span data-shortcut>⌘X</span></div>
<div role="menuitem" data-filter="Paste"><svg class="lucide lucide-clipboard-paste" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 14h10" /><path d="M16 4h2a2 2 0 0 1 2 2v1.344" /><path d="m17 18 4-4-4-4" /><path d="M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 1.793-1.113" /><rect x="8" y="2" width="8" height="4" rx="1" /></svg><span>Paste</span><span data-shortcut>⌘V</span></div>
<div role="menuitem" data-filter="Delete"><svg class="lucide lucide-trash" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /><path d="M3 6h18" /><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></svg><span>Delete</span><span data-shortcut>⌫</span></div>
</div>
<hr role="separator" />
<div role="group" aria-labelledby="command-scrollable-view">
<span role="heading" id="command-scrollable-view">View</span>
<div role="menuitem" data-filter="Grid View"><svg class="lucide lucide-layout-grid" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="7" x="3" y="3" rx="1" /><rect width="7" height="7" x="14" y="3" rx="1" /><rect width="7" height="7" x="14" y="14" rx="1" /><rect width="7" height="7" x="3" y="14" rx="1" /></svg><span>Grid View</span></div>
<div role="menuitem" data-filter="List View"><svg class="lucide lucide-list" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 5h.01" /><path d="M3 12h.01" /><path d="M3 19h.01" /><path d="M8 5h13" /><path d="M8 12h13" /><path d="M8 19h13" /></svg><span>List View</span></div>
<div role="menuitem" data-filter="Zoom In"><svg class="lucide lucide-zoom-in" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8" /><line x1="21" x2="16.65" y1="21" y2="16.65" /><line x1="11" x2="11" y1="8" y2="14" /><line x1="8" x2="14" y1="11" y2="11" /></svg><span>Zoom In</span><span data-shortcut>⌘+</span></div>
<div role="menuitem" data-filter="Zoom Out"><svg class="lucide lucide-zoom-out" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8" /><line x1="21" x2="16.65" y1="21" y2="16.65" /><line x1="8" x2="14" y1="11" y2="11" /></svg><span>Zoom Out</span><span data-shortcut>⌘-</span></div>
</div>
<hr role="separator" />
<div role="group" aria-labelledby="command-scrollable-account">
<span role="heading" id="command-scrollable-account">Account</span>
<div role="menuitem" data-filter="Profile"><svg class="lucide lucide-user" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" /></svg><span>Profile</span><span data-shortcut>⌘P</span></div>
<div role="menuitem" data-filter="Billing"><svg class="lucide lucide-credit-card" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2" /><line x1="2" x2="22" y1="10" y2="10" /></svg><span>Billing</span><span data-shortcut>⌘B</span></div>
<div role="menuitem" data-filter="Settings"><svg class="lucide lucide-settings" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /><circle cx="12" cy="12" r="3" /></svg><span>Settings</span><span data-shortcut>⌘S</span></div>
<div role="menuitem" data-filter="Notifications"><svg class="lucide lucide-bell" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></svg><span>Notifications</span></div>
<div role="menuitem" data-filter="Help & Support"><svg class="lucide lucide-circle-help" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" /><path d="M12 17h.01" /></svg><span>Help &amp; Support</span></div>
</div>
<hr role="separator" />
<div role="group" aria-labelledby="command-scrollable-tools">
<span role="heading" id="command-scrollable-tools">Tools</span>
<div role="menuitem" data-filter="Calculator"><svg class="lucide lucide-calculator" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="16" height="20" x="4" y="2" rx="2" /><line x1="8" x2="16" y1="6" y2="6" /><line x1="16" x2="16" y1="14" y2="18" /><path d="M16 10h.01" /><path d="M12 10h.01" /><path d="M8 10h.01" /><path d="M12 14h.01" /><path d="M8 14h.01" /><path d="M12 18h.01" /><path d="M8 18h.01" /></svg><span>Calculator</span></div>
<div role="menuitem" data-filter="Calendar"><svg class="lucide lucide-calendar" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4" /><path d="M16 2v4" /><rect width="18" height="18" x="3" y="4" rx="2" /><path d="M3 10h18" /></svg><span>Calendar</span></div>
<div role="menuitem" data-filter="Image Editor"><svg class="lucide lucide-image" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2" /><circle cx="9" cy="9" r="2" /><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" /></svg><span>Image Editor</span></div>
<div role="menuitem" data-filter="Code Editor"><svg class="lucide lucide-code" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 18 6-6-6-6" /><path d="m8 6-6 6 6 6" /></svg><span>Code Editor</span></div>
</div>
</div>
</div>
</dialog>
</Preview>
## RTL
Add `dir="rtl"` to the command root or an ancestor.
<Preview class="w-full max-w-sm">
<div dir="rtl">
<div id="command-rtl" class="command border" aria-label="Command menu">
<header><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>
<input type="text" id="command-rtl-input" placeholder="اكتب أمرًا أو ابحث..." autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" role="combobox" aria-expanded="true" aria-controls="command-rtl-menu" />
</header>
<div role="menu" id="command-rtl-menu" aria-orientation="vertical" data-empty="لم يتم العثور على نتائج.">
<div role="group" aria-labelledby="command-rtl-suggestions">
<span role="heading" id="command-rtl-suggestions">اقتراحات</span>
<div role="menuitem" data-filter="Calendar"><svg class="lucide lucide-calendar" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4" /><path d="M16 2v4" /><rect width="18" height="18" x="3" y="4" rx="2" /><path d="M3 10h18" /></svg>
<span>التقويم</span>
<span data-shortcut>⌘K</span>
</div>
<div role="menuitem" data-filter="Search Emoji"><svg class="lucide lucide-smile" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M8 14s1.5 2 4 2 4-2 4-2" /><line x1="9" x2="9.01" y1="9" y2="9" /><line x1="15" x2="15.01" y1="9" y2="9" /></svg>
<span>البحث عن الرموز التعبيرية</span>
</div>
<div role="menuitem" aria-disabled="true" data-filter="Calculator"><svg class="lucide lucide-calculator" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="16" height="20" x="4" y="2" rx="2" /><line x1="8" x2="16" y1="6" y2="6" /><line x1="16" x2="16" y1="14" y2="18" /><path d="M16 10h.01" /><path d="M12 10h.01" /><path d="M8 10h.01" /><path d="M12 14h.01" /><path d="M8 14h.01" /><path d="M12 18h.01" /><path d="M8 18h.01" /></svg>
<span>الآلة الحاسبة</span>
</div>
</div>
<hr role="separator" />
<div role="group" aria-labelledby="command-rtl-settings">
<span role="heading" id="command-rtl-settings">الإعدادات</span>
<div role="menuitem" data-filter="Profile"><svg class="lucide lucide-user" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" /></svg>
<span>الملف الشخصي</span>
<span data-shortcut>⌘P</span>
</div>
<div role="menuitem" data-filter="Billing"><svg class="lucide lucide-credit-card" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2" /><line x1="2" x2="22" y1="10" y2="10" /></svg>
<span>الفوترة</span>
<span data-shortcut>⌘B</span>
</div>
<div role="menuitem" data-filter="Settings"><svg class="lucide lucide-settings" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /><circle cx="12" cy="12" r="3" /></svg>
<span>الإعدادات</span>
<span data-shortcut>⌘S</span>
</div>
</div>
</div>
</div>
</div>
</Preview>
-288
View File
@@ -1,288 +0,0 @@
---
layout: layouts/page.njk
title: Command
description: Fast, composable command menu for quick navigation and actions.
toc:
- label: Usage
id: usage
children:
- label: HTML + JavaScript
id: usage-html-js
children:
- label: "Step 1: Include the JavaScript files"
id: usage-html-js-1
- label: "Step 2: Add your command HTML"
id: usage-html-js-2
- label: HTML structure
id: usage-html-js-3
- label: JavaScript events
id: usage-html-js-4
- label: Jinja and Nunjucks
id: usage-macro
- label: Examples
id: examples
children:
- label: Dialog
id: example-dialog
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "command.njk" import command, command_dialog %}
{% macro command_items(suggestions_id="suggestions", settings_id="settings") %}
<div role="group" aria-labelledby="{{ suggestions_id }}">
<span role="heading" id="{{ suggestions_id }}">Suggestions</span>
<div role="menuitem" onclick="console.log('Open calendar')">
{% lucide "calendar" %}
<span>Calendar</span>
</div>
<div role="menuitem" onclick="console.log('Search emoji')">
{% lucide "smile" %}
<span>Search Emoji</span>
</div>
<div role="menuitem" aria-disabled="true">
{% lucide "calculator" %}
<span>Calculator</span>
</div>
</div>
<hr role="separator" />
<div role="group" aria-labelledby="{{ settings_id }}">
<span role="heading" id="{{ settings_id }}">Settings</span>
<div role="menuitem" onclick="console.log('Open profile')" data-filter="Profile">
{% lucide "user" %}
<span>Profile</span>
<kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘P</kbd>
</div>
<div role="menuitem" onclick="console.log('Open billing')" data-filter="Billing">
{% lucide "credit-card" %}
<span>Billing</span>
<kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘B</kbd>
</div>
<div role="menuitem" onclick="console.log('Open settings')" data-filter="Settings">
{% lucide "settings" %}
<span>Settings</span>
<kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘S</kbd>
</div>
</div>
{% endmacro %}
{% set code_standalone %}
{% call command(
id="demo-command-standalone",
main_attrs={ "class": "rounded-lg border shadow-md" }
) %}
{{ command_items("suggestions", "settings") }}
{% endcall %}
{% endset %}
{{ code_preview("command-standalone", code_standalone | prettyHtml, class=" w-full max-w-[450px]") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-html-js"><a href="#usage-html-js">HTML + JavaScript</a></h3>
<h4 id="usage-html-js-1"><a href="#usage-html-js-1">Step 1: Include the JavaScript files</a></h4>
<section class="prose">
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/command.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/js/command.js" target="_blank">
command.js
{% lucide "arrow-right" %}
</a>
</div>
<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add your command HTML</a></h4>
<section class="prose">
<p>You can use the command menu standalone:</p>
</section>
{{ code_block(code_standalone | prettyHtml, "html") }}
<section class="prose">
<p>Wrap the command menu in a <code>&lt;dialog class="command-dialog"&gt;</code> to display in a dialog:</p>
</section>
{% set code_dialog %}
<dialog class="command-dialog" aria-label="Command menu">
<div class="command">
<!-- Regular command content: header, menu, etc. -->
</div>
</dialog>
{% endset %}
{{ code_block(code_dialog | prettyHtml, "html") }}
<h4 id="usage-html-js-3"><a href="#usage-html-js-3">HTML structure</a></h4>
<section class="prose">
<dl>
<dt><code class="highlight language-html">&lt;dialog class="command-dialog"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>For dialog variant, wraps a <code>&lt;div class="command"&gt;</code> to display as a modal. Uses native <code>&lt;dialog&gt;</code> behavior and requires <code>aria-label</code> or <code>aria-labelledby</code> for accessibility.
<dl>
<dt><code class="highlight language-html">&lt;div class="command"&gt;</code></dt>
<dd>Wraps around the entire command component. Can be used standalone or inside a dialog.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dd>Contains the search input to filter command options.
<dl>
<dt><code class="highlight language-html">&lt;input type="text"&gt;</code></dt>
<dd>The search input. Should have:
<ul>
<li><code>role="combobox"</code>: indicates the input controls a menu with filtering</li>
<li><code>aria-expanded="true"</code>: the menu is always expanded</li>
<li><code>aria-controls="{ MENU_ID }"</code>: points to the menu's id</li>
</ul>
</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;div role="menu"&gt;</code></dt>
<dd>The menu containing the command items. Should have <code>id="{ MENU_ID }"</code> refered to by the input's <code>aria-controls</code> attribute.
<dl>
<dt><code class="highlight language-html">&lt;div role="menuitem"&gt;</code> or <code class="highlight language-html">&lt;a role="menuitem"&gt;</code></dt>
<dd>Command item that triggers an action when clicked. Use <code>&lt;a&gt;</code> for navigation links, or <code>&lt;div&gt;</code> for actions. In a command-dialog, clicking a menuitem automatically closes the dialog unless the <code>data-keep-command-open</code> attribute is present. Optional attributes:
<ul>
<li><code>href</code>: If present on an <code>&lt;a&gt;</code>, the item navigates to that URL</li>
<li><code>data-filter</code>: custom text used for filtering (if different from displayed content, e.g., <code>data-filter="calendar event"</code>)</li>
<li><code>data-keywords</code>: additional keywords for enhanced filtering (e.g., <code>data-keywords="calendar event date"</code>)</li>
<li><code>data-force</code>: forces the item to always display, regardless of filter (e.g., <code>data-force</code>)</li>
<li><code>aria-disabled="true"</code>: marks item as disabled (excluded from keyboard navigation and filtering)</li>
</ul>
</dd>
<dt><code class="highlight language-html">&lt;hr role="separator"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Separator between groups/items. Hidden when filtering is active.</dd>
<dt><code class="highlight language-html">&lt;div role="group"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Group of items, can have <code>aria-labelledby</code> attribute to link to a heading.</dd>
<dt><code class="highlight language-html">&lt;span role="heading"&gt;</code></dt>
<dd>Group heading, must have an <code>id</code> attribute if you use the <code>aria-labelledby</code> attribute on the group.</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
</section>
<h4 id="usage-html-js-4"><a href="#usage-html-js-4">JavaScript events</a></h4>
<section class="prose">
<dl>
<dt><code>basecoat:initialized</code></dt>
<dd>Once the component is fully initialized, it dispatches a custom (non-bubbling) <code>basecoat:initialized</code> event on itself.</dd>
</dl>
</section>
<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>
<div class="prose">
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">command()</code> or <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">command_dialog()</code> Nunjucks or Jinja macros for this component.</p>
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/command.html.jinja" target="_blank">
Jinja macro
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/command.njk" target="_blank">
Nunjucks macro
{% lucide "arrow-right" %}
</a>
</div>
{% set raw_code %}{% raw %}{% call command() %}
<div role="menuitem">
<svg>...</svg>
<span>Calendar</span>
</div>
<div role="menuitem" data-keywords="config preferences">
<svg>...</svg>
<span>Settings</span>
</div>
{% endcall %}{% endraw %}{% endset %}
{{ code_block(raw_code, "jinja") }}
<section class="prose">
<p>You can also use the <code>items</code> parameter to pass an array of items:</p>
</section>
{% set icon_calendar %}{% lucide "calendar" %}{% endset %}
{% set icon_settings %}{% lucide "settings" %}{% endset %}
{% set raw_code_items %}{% raw %}{% set icon_calendar %}{% lucide "calendar" %}{% endset %}
{% set icon_settings %}{% lucide "settings" %}{% endset %}
{% set items = [
{ type: "group", label: "Suggestions", items: [
{ label: "Calendar", icon: icon_calendar, keywords: "date event" },
{ label: "Search Emoji", icon: icon_settings },
{ label: "Calculator", disabled: true }
]},
{ type: "separator" },
{ type: "group", label: "Settings", items: [
{ label: "Profile", url: "/profile", icon: icon_settings, keywords: "user account" },
{ label: "Billing", url: "/billing", icon: icon_settings },
{ label: "Settings", url: "/settings", icon: icon_settings }
]}
] %}
{% call command(items=items) %}{% endcall %}{% endraw %}{% endset %}
{{ code_block(raw_code_items, "jinja") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-dialog"><a href="#example-dialog">Dialog</a></h3>
<section class="prose">
<p>You can use some simple JavaScript to open the command menu with a keyboard shortcut (in this case <kbd class="kbd">⌘J</kbd>).</p>
</section>
{% set code_dialog %}
<button
type="button"
class="btn-outline"
onclick="document.getElementById('demo-command-dialog').showModal()"
>
Open command menu
<kbd class="kbd">⌘J</kbd>
</button>
{% call command_dialog(id="demo-command-dialog") %}
{{ command_items("cmd-suggestions", "cmd-settings") }}
{% endcall %}
<script>
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'j') {
e.preventDefault();
const dialog = document.getElementById('demo-command-dialog');
if (dialog.open) {
dialog.close();
} else {
dialog.showModal();
dialog.querySelector('header input')?.focus();
}
}
});
</script>
{% endset %}
{{ code_preview("command-dialog", code_dialog | prettyHtml) }}
+258
View File
@@ -0,0 +1,258 @@
# Dialog
<Preview>
<button type="button" onclick="document.getElementById('demo-dialog-edit-profile').showModal()" class="btn" data-variant="outline">Open Dialog</button>
<dialog id="demo-dialog-edit-profile" class="dialog" aria-labelledby="demo-dialog-edit-profile-title" aria-describedby="demo-dialog-edit-profile-description" onclick="if (event.target === this) this.close()">
<div class="sm:max-w-sm">
<header>
<h2 id="demo-dialog-edit-profile-title">Edit profile</h2>
<p id="demo-dialog-edit-profile-description">Make changes to your profile here. Click save when you're done.</p>
</header>
<section>
<form class="grid gap-4">
<div class="grid gap-3">
<label class="label" for="demo-dialog-edit-profile-name">Name</label>
<input class="input" type="text" value="Pedro Duarte" id="demo-dialog-edit-profile-name" autofocus />
</div>
<div class="grid gap-3">
<label class="label" for="demo-dialog-edit-profile-username">Username</label>
<input class="input" type="text" value="@peduarte" id="demo-dialog-edit-profile-username" />
</div>
</form>
</section>
<footer>
<button class="btn" data-variant="outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Save changes</button>
</footer>
<button type="button" class="btn" data-variant="ghost" data-size="icon-sm" aria-label="Close dialog" onclick="this.closest('dialog').close()"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg></button>
</div>
</dialog>
</Preview>
## Usage
<Callout title="Template macros available" icon="code" action={{ label: "Read more", href: "/cli#dialog" }}>
This component ships a `dialog()` macro for Jinja and Nunjucks.
</Callout>
<Steps>
<Step title="Include CSS">
Import Tailwind and one full Basecoat style bundle.
```css
@import "tailwindcss";
@import "basecoat-css/vega.css";
```
Or import only the base CSS, Dialog component CSS, and one style pack.
```css
@import "tailwindcss";
@import "basecoat-css/base.css";
@import "basecoat-css/components/dialog.css";
@import "basecoat-css/styles/vega.css";
```
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Add your dialog HTML">
Basecoat uses the native `<dialog>` element and `showModal()`. This differs from shadcn/ui's portalled Base UI implementation, but preserves native modality, focus handling, and inert page content without component JavaScript.
```html
<button type="button" onclick="document.getElementById('demo-dialog-edit-profile').showModal()" class="btn" data-variant="outline">Open Dialog</button>
<dialog id="demo-dialog-edit-profile" class="dialog" aria-labelledby="demo-dialog-edit-profile-title" aria-describedby="demo-dialog-edit-profile-description" onclick="if (event.target === this) this.close()">
<div class="sm:max-w-sm">
<header>
<h2 id="demo-dialog-edit-profile-title">Edit profile</h2>
<p id="demo-dialog-edit-profile-description">Make changes to your profile here. Click save when you're done.</p>
</header>
<section>
<form class="grid gap-4">
<div class="grid gap-3">
<label class="label" for="demo-dialog-edit-profile-name">Name</label>
<input class="input" type="text" value="Pedro Duarte" id="demo-dialog-edit-profile-name" autofocus />
</div>
<div class="grid gap-3">
<label class="label" for="demo-dialog-edit-profile-username">Username</label>
<input class="input" type="text" value="@peduarte" id="demo-dialog-edit-profile-username" />
</div>
</form>
</section>
<footer>
<button class="btn" data-variant="outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Save changes</button>
</footer>
<button type="button" class="btn" data-variant="ghost" data-size="icon-sm" aria-label="Close dialog" onclick="this.closest('dialog').close()"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg></button>
</div>
</dialog>
```
</Step>
</Steps>
### HTML structure
<dl>
<dt><code>&lt;button type="button" onclick="dialog.showModal()"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Trigger button. Basecoat intentionally uses the native <code>HTMLDialogElement.showModal()</code> method.</dd>
<dt><code>&lt;dialog class="dialog" id="&#123; DIALOG_ID &#125;"&gt;</code></dt>
<dd>
Native modal dialog. Add <code>aria-labelledby="&#123; TITLE_ID &#125;"</code> and <code>aria-describedby="&#123; DESCRIPTION_ID &#125;"</code> when title and description are present. Add width utilities to the inner <code>&lt;div&gt;</code> when a dialog needs a custom size. The macro also adds backdrop-click close handling.
<dl>
<dt><code>&lt;div&gt;</code></dt>
<dd>
Dialog content surface.
<dl>
<dt><code>&lt;header&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>
Dialog header.
<dl>
<dt><code>&lt;h2 id="&#123; TITLE_ID &#125;"&gt;</code></dt>
<dd>Dialog title. Reference it from <code>aria-labelledby</code>.</dd>
<dt><code>&lt;p id="&#123; DESCRIPTION_ID &#125;"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Dialog description. Reference it from <code>aria-describedby</code>.</dd>
</dl>
</dd>
<dt><code>&lt;section&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Dialog body/content area. Add overflow utilities when the body should scroll.</dd>
<dt><code>&lt;footer&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Action area. It stacks actions on small screens and aligns them to the end on larger screens.</dd>
<dt><code>&lt;button type="button" onclick="this.closest('dialog').close()"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Close button. You can also wrap a button in <code>&lt;form method="dialog"&gt;</code>.</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
## Examples
### Custom close button
<Preview>
<button type="button" onclick="document.getElementById('dialog-custom-close').showModal()" class="btn" data-variant="outline">Share</button>
<dialog id="dialog-custom-close" class="dialog" aria-labelledby="dialog-custom-close-title" aria-describedby="dialog-custom-close-description" onclick="if (event.target === this) this.close()">
<div class="sm:max-w-md">
<header>
<h2 id="dialog-custom-close-title">Share link</h2>
<p id="dialog-custom-close-description">Anyone who has this link will be able to view this.</p>
</header>
<section class="flex items-center gap-2">
<div class="grid flex-1 gap-2">
<label class="label sr-only" for="dialog-custom-close-link">Link</label>
<input class="input" id="dialog-custom-close-link" value="https://basecoatui.com/installation/" readonly />
</div>
</section>
<footer class="sm:justify-start">
<button class="btn" onclick="this.closest('dialog').close()">Close</button>
</footer>
</div>
</dialog>
</Preview>
### No close button
<Preview>
<button type="button" onclick="document.getElementById('dialog-no-close').showModal()" class="btn" data-variant="outline">No Close Button</button>
<dialog id="dialog-no-close" class="dialog" aria-labelledby="dialog-no-close-title" aria-describedby="dialog-no-close-description" onclick="if (event.target === this) this.close()">
<div>
<header>
<h2 id="dialog-no-close-title">No Close Button</h2>
<p id="dialog-no-close-description">This dialog doesn't have a close button in the top-right corner.</p>
</header>
</div>
</dialog>
</Preview>
### Sticky footer
<Preview>
<button type="button" onclick="document.getElementById('dialog-sticky-footer').showModal()" class="btn" data-variant="outline">Sticky Footer</button>
<dialog id="dialog-sticky-footer" class="dialog" aria-labelledby="dialog-sticky-footer-title" aria-describedby="dialog-sticky-footer-description" onclick="if (event.target === this) this.close()">
<div>
<header>
<h2 id="dialog-sticky-footer-title">Sticky Footer</h2>
<p id="dialog-sticky-footer-description">This dialog has a sticky footer that stays visible while the content scrolls.</p>
</header>
<section class="-mx-4 max-h-[50vh] overflow-y-auto px-4 scrollbar">
<p class="mb-4 leading-normal">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
<p class="mb-4 leading-normal">Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p class="mb-4 leading-normal">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<p class="mb-4 leading-normal">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
<p class="mb-4 leading-normal">Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
<p class="mb-4 leading-normal">Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p class="mb-4 leading-normal">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<p class="mb-4 leading-normal">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
</section>
<footer>
<button class="btn" data-variant="outline" onclick="this.closest('dialog').close()">Close</button>
</footer>
<button type="button" class="btn" data-variant="ghost" data-size="icon-sm" aria-label="Close dialog" onclick="this.closest('dialog').close()"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg></button>
</div>
</dialog>
</Preview>
### Scrollable content
<Preview>
<button type="button" onclick="document.getElementById('dialog-scrollable').showModal()" class="btn" data-variant="outline">Scrollable Content</button>
<dialog id="dialog-scrollable" class="dialog" aria-labelledby="dialog-scrollable-title" aria-describedby="dialog-scrollable-description" onclick="if (event.target === this) this.close()">
<div>
<header>
<h2 id="dialog-scrollable-title">Scrollable Content</h2>
<p id="dialog-scrollable-description">This is a dialog with scrollable content.</p>
</header>
<section class="-mx-4 max-h-[50vh] overflow-y-auto px-4 scrollbar">
<p class="mb-4 leading-normal">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<p class="mb-4 leading-normal">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
<p class="mb-4 leading-normal">Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
<p class="mb-4 leading-normal">Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p class="mb-4 leading-normal">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<p class="mb-4 leading-normal">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
<p class="mb-4 leading-normal">Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
<p class="mb-4 leading-normal">Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p class="mb-4 leading-normal">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<p class="mb-4 leading-normal">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
</section>
<button type="button" class="btn" data-variant="ghost" data-size="icon-sm" aria-label="Close dialog" onclick="this.closest('dialog').close()"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg></button>
</div>
</dialog>
</Preview>
### RTL
Dialog positioning and close button placement use logical properties. Set `dir="rtl"` on the dialog or a parent element.
<Preview>
<div dir="rtl">
<button type="button" onclick="document.getElementById('dialog-rtl').showModal()" class="btn" data-variant="outline">فتح الحوار</button>
<dialog id="dialog-rtl" class="dialog" aria-labelledby="dialog-rtl-title" aria-describedby="dialog-rtl-description" onclick="if (event.target === this) this.close()">
<div class="sm:max-w-sm">
<header>
<h2 id="dialog-rtl-title">تعديل الملف الشخصي</h2>
<p id="dialog-rtl-description">قم بإجراء تغييرات على ملفك الشخصي هنا. انقر فوق حفظ عند الانتهاء.</p>
</header>
<section>
<form class="grid gap-4">
<div class="grid gap-3">
<label class="label" for="dialog-rtl-name">الاسم</label>
<input class="input" id="dialog-rtl-name" name="name" value="Pedro Duarte" />
</div>
<div class="grid gap-3">
<label class="label" for="dialog-rtl-username">اسم المستخدم</label>
<input class="input" id="dialog-rtl-username" name="username" value="@peduarte" />
</div>
</form>
</section>
<footer>
<button class="btn" data-variant="outline" onclick="this.closest('dialog').close()">إلغاء</button>
<button class="btn" onclick="this.closest('dialog').close()">حفظ التغييرات</button>
</footer>
<button type="button" class="btn" data-variant="ghost" data-size="icon-sm" aria-label="Close dialog" onclick="this.closest('dialog').close()"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg></button>
</div>
</dialog>
</div>
</Preview>
-177
View File
@@ -1,177 +0,0 @@
---
layout: layouts/page.njk
title: Dialog
description: A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.
toc:
- label: Usage
id: usage
children:
- label: HTML
id: usage-html
- label: Jinja and Nunjucks
id: usage-macro
- label: Examples
id: examples
children:
- label: Scrollable content
id: example-scrollable-content
---
{% from "macros/code_block.njk" import code_block %}
{% from "macros/code_preview.njk" import code_preview %}
{% from "dialog.njk" import dialog %}
{% set code_html %}
{% set footer %}
<button class="btn-outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Save changes</button>
{% endset %}
{% call dialog(
id="demo-dialog-edit-profile",
title="Edit profile",
description="Make changes to your profile here. Click save when you're done.",
trigger="Edit Profile",
trigger_attrs={"class": "btn-outline"},
dialog_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
footer=footer
) %}
<form class="form grid gap-4">
<div class="grid gap-3">
<label for="demo-dialog-edit-profile-name">Name</label>
<input type="text" value="Pedro Duarte" id="demo-dialog-edit-profile-name" autofocus/>
</div>
<div class="grid gap-3">
<label for="demo-dialog-edit-profile-username">Username</label>
<input type="text" value="@peduarte" id="demo-dialog-edit-profile-username" />
</div>
</form>
{% endcall %}
{% endset %}
{{ code_preview("dialog", code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-html"><a href="#usage-html">HTML</a></h3>
{{ code_block(code_html | prettyHtml, class="") }}
<section class="prose">
<p>The component has the following HTML structure:</p>
<dl>
<dt><code class="highlight language-html">&lt;button type="button"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The trigger button to open the dialog (using <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal" target="_blank">the native <code>showModal()</code> method</a>).</dd>
<dt><code class="highlight language-html">&lt;dialog class="dialog" id="{ DIALOG_ID }"&gt;</code></dt>
<dd>Wraps the whole component and acts as the backdrop. It can also have the following attributes:
<ul>
<li><code>aria-labelledby="{ TITLE_ID }"</code>: linked to by the <code>aria-labelledby</code> attribute of the <code>&lt;dialog&gt;</code>.</li>
<li><code>aria-describedby="{ DESCRIPTION_ID }"</code>: linked to by the <code>aria-describedby</code> attribute of the <code>&lt;dialog&gt;</code>.</li>
<li><code>onclick</code>: closes the dialog when the backdrop is clicked.</li>
</ul>
<dl>
<dt><code class="highlight language-html">&lt;div&gt;</code></dt>
<dd>Wraps the content of the dialog.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dd>Contains the header for the dialog:
<dl>
<dt><code class="highlight language-html">&lt;h2&gt;</code></dt>
<dd>The title of the dialog, must have an <code>id</code> if you use the <code>aria-labelledby</code> attribute on the <code>&lt;dialog&gt;</code>.</dd>
<dt><code class="highlight language-html">&lt;p&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The description of the dialog, must have an <code>id</code> if you use the <code>aria-describedby</code> attribute on the <code>&lt;dialog&gt;</code>.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>For the content.</dd>
<dt><code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Usually contains actions.</dd>
<dt><code class="highlight language-html">&lt;button type="button" onclick="this.closest('dialog').close()"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>A button to close the dialog. Alternatively, you can wrap a button in a <code>&lt;form method="dialog"&gt;</code> element.</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
</section>
<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>
<div class="prose">
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">dialog()</code> Nunjucks or Jinja macro for this component.</p>
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/dialog.html.jinja" target="_blank">
Jinja macro
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/dialog.njk" target="_blank">
Nunjucks macro
{% lucide "arrow-right" %}
</a>
</div>
{% set raw_code %}{% raw %}{% set code_html %}
{% set footer %}
<button class="btn-outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Save changes</button>
{% endset %}
{% call dialog(
id="demo-dialog-edit-profile",
title="Edit profile",
description="Make changes to your profile here. Click save when you're done.",
trigger="Edit Profile",
trigger_attrs={"class": "btn-outline"},
dialog_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
footer=footer
) %}
<form class="form grid gap-4">
<div class="grid gap-3">
<label for="demo-dialog-edit-profile-name">Name</label>
<input type="text" value="Pedro Duarte" id="demo-dialog-edit-profile-name" />
</div>
<div class="grid gap-3">
<label for="demo-dialog-edit-profile-username">Username</label>
<input type="text" value="@peduarte" id="demo-dialog-edit-profile-username" />
</div>
</form>
{% endcall %}{% endraw %}{% endset%}
{{ code_block(raw_code, "jinja") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-scrollable-content"><a href="#example-scrollable-content">Scrollable content</a></h3>
{% set code %}
{% set footer %}
<button class="btn-outline" onclick="this.closest('dialog').close()">Close</button>
{% endset %}
{% call dialog(
id="dialog-example",
title="Scrollable Content",
description="This is a dialog with scrollable content.",
trigger="Scrollable Content",
trigger_attrs={"class": "btn-outline"},
dialog_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
body_attrs={"class": "overflow-y-auto scrollbar"},
footer=footer
) %}
<div class="space-y-4 text-sm">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>
{% endcall %}
{% endset %}
{{ code_preview("dialog-scrollable-content", code | prettyHtml) }}
+510
View File
@@ -0,0 +1,510 @@
# Dropdown Menu
<Preview>
<div id="demo-dropdown-menu" class="dropdown-menu">
<button type="button" id="demo-dropdown-menu-trigger" aria-haspopup="menu" aria-controls="demo-dropdown-menu-menu" aria-expanded="false" class="btn" data-variant="outline">Open</button>
<div id="demo-dropdown-menu-popover" data-popover aria-hidden="true" class="w-40">
<div role="menu" id="demo-dropdown-menu-menu" aria-labelledby="demo-dropdown-menu-trigger">
<div role="group" aria-labelledby="demo-dropdown-account">
<div role="heading" id="demo-dropdown-account">My Account</div>
<div role="menuitem">
<span>Profile</span>
<kbd>⇧⌘P</kbd>
</div>
<div role="menuitem">
<span>Billing</span>
<kbd>⌘B</kbd>
</div>
<div role="menuitem">
<span>Settings</span>
<kbd>⌘S</kbd>
</div>
</div>
<hr role="separator" />
<div role="group">
<div role="menuitem">Team</div>
<div role="menuitem">
<span>New Team</span>
<kbd>⌘+T</kbd>
</div>
</div>
<hr role="separator" />
<div role="group">
<div role="menuitem">GitHub</div>
<div role="menuitem">Support</div>
<div role="menuitem" aria-disabled="true">API</div>
</div>
<hr role="separator" />
<div role="group">
<div role="menuitem">
<span>Log out</span>
<kbd>⇧⌘Q</kbd>
</div>
</div>
</div>
</div>
</div>
</Preview>
## Usage
<Callout title="Template macros available" icon="code" action={{ label: "Read more", href: "/cli#dropdown_menu" }}>
This component ships a `dropdown_menu()` macro for Jinja and Nunjucks.
</Callout>
Basecoat dropdown menus are inline-positioned relative to the `.dropdown-menu` wrapper. This differs from shadcn/ui's portalled Base UI implementation, but keeps the markup dependency-free and matches Basecoat's current popover/select positioning model.
<Steps>
<Step title="Include CSS">
Import Tailwind and one full Basecoat style bundle.
```css
@import "tailwindcss";
@import "basecoat-css/vega.css";
```
Or import only the base CSS, Dropdown Menu component CSS, and one style pack.
```css
@import "tailwindcss";
@import "basecoat-css/base.css";
@import "basecoat-css/components/dropdown-menu.css";
@import "basecoat-css/styles/vega.css";
```
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Include JavaScript">
Copy or serve the full Basecoat JavaScript bundle.
```html
<script src="/assets/js/all.min.js" defer></script>
```
Or copy or serve the Basecoat runtime and Dropdown Menu script.
```html
<script src="/assets/js/basecoat.min.js" defer></script>
<script src="/assets/js/dropdown-menu.min.js" defer></script>
```
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Add your dropdown menu HTML">
```html
<div id="demo-dropdown-menu" class="dropdown-menu">
<button type="button" id="demo-dropdown-menu-trigger" aria-haspopup="menu" aria-controls="demo-dropdown-menu-menu" aria-expanded="false" class="btn" data-variant="outline">Open</button>
<div id="demo-dropdown-menu-popover" data-popover aria-hidden="true" class="w-40">
<div role="menu" id="demo-dropdown-menu-menu" aria-labelledby="demo-dropdown-menu-trigger">
<div role="group" aria-labelledby="demo-dropdown-account">
<div role="heading" id="demo-dropdown-account">My Account</div>
<div role="menuitem">
<span>Profile</span>
<kbd>⇧⌘P</kbd>
</div>
<div role="menuitem">
<span>Billing</span>
<kbd>⌘B</kbd>
</div>
<div role="menuitem">
<span>Settings</span>
<kbd>⌘S</kbd>
</div>
</div>
<hr role="separator" />
<div role="group">
<div role="menuitem">Team</div>
<div role="menuitem">
<span>New Team</span>
<kbd>⌘+T</kbd>
</div>
</div>
<hr role="separator" />
<div role="group">
<div role="menuitem">GitHub</div>
<div role="menuitem">Support</div>
<div role="menuitem" aria-disabled="true">API</div>
</div>
<hr role="separator" />
<div role="group">
<div role="menuitem">
<span>Log out</span>
<kbd>⇧⌘Q</kbd>
</div>
</div>
</div>
</div>
</div>
```
</Step>
</Steps>
### HTML structure
<dl>
<dt><code>&lt;div class="dropdown-menu"&gt;</code></dt>
<dd>
Relative wrapper for the trigger and inline menu content.
<dl>
<dt><code>&lt;button aria-haspopup="menu" aria-expanded="false"&gt;</code></dt>
<dd>Trigger button. The script toggles <code>aria-expanded</code> and manages keyboard navigation.</dd>
<dt><code>&lt;div data-popover aria-hidden="true"&gt;</code></dt>
<dd>
Menu content popover. Set <code>data-side="top|right|bottom|left|inline-start|inline-end"</code> and <code>data-align="start|center|end"</code> to control placement.
<dl>
<dt><code>&lt;div role="menu"&gt;</code></dt>
<dd>
Container for menu items, groups, labels, and separators.
<dl>
<dt><code>&lt;div role="group" aria-labelledby="&#123; HEADING_ID &#125;"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Groups related menu items.</dd>
<dt><code>&lt;div role="heading" id="&#123; HEADING_ID &#125;"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Group heading/label.</dd>
<dt><code>&lt;div role="menuitem"&gt;</code></dt>
<dd>Standard action item. Use <code>aria-disabled="true"</code> for disabled items.</dd>
<dt><code>&lt;div role="menuitemcheckbox" aria-checked="true"&gt;</code></dt>
<dd>Checkbox-style item. Add a child with <code>data-indicator</code> for the checked icon.</dd>
<dt><code>&lt;div role="menuitemradio" aria-checked="true"&gt;</code></dt>
<dd>Radio-style item. Add a child with <code>data-indicator</code> for the selected icon.</dd>
<dt><code>&lt;kbd&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Shortcut hint aligned to the inline end of the item.</dd>
<dt><code>&lt;hr role="separator"&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Separator between groups or options.</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
### JavaScript API
| API | Type | Description |
| --- | --- | --- |
| `dropdown.open()` | Method | Opens the menu. |
| `dropdown.close()` | Method | Closes the menu. |
| `dropdown.toggle()` | Method | Toggles the menu. |
| `dropdown.refresh()` | Method | Rescans menu items after children change inside the existing `role="menu"` element. |
## Examples
### Basic
<Preview>
<div id="dropdown-basic" class="dropdown-menu">
<button type="button" id="dropdown-basic-trigger" aria-haspopup="menu" aria-controls="dropdown-basic-menu" aria-expanded="false" class="btn" data-variant="outline">Open</button>
<div id="dropdown-basic-popover" data-popover aria-hidden="true" class="min-w-32">
<div role="menu" id="dropdown-basic-menu" aria-labelledby="dropdown-basic-trigger">
<div role="group" aria-labelledby="dropdown-basic-account">
<div role="heading" id="dropdown-basic-account">My Account</div>
<div role="menuitem">Profile</div>
<div role="menuitem">Billing</div>
<div role="menuitem">Settings</div>
</div>
<hr role="separator" />
<div role="menuitem">GitHub</div>
<div role="menuitem">Support</div>
<div role="menuitem" aria-disabled="true">API</div>
</div>
</div>
</div>
</Preview>
### Shortcuts
<Preview>
<div id="dropdown-shortcuts" class="dropdown-menu">
<button type="button" id="dropdown-shortcuts-trigger" aria-haspopup="menu" aria-controls="dropdown-shortcuts-menu" aria-expanded="false" class="btn" data-variant="outline">Open</button>
<div id="dropdown-shortcuts-popover" data-popover aria-hidden="true" class="min-w-46">
<div role="menu" id="dropdown-shortcuts-menu" aria-labelledby="dropdown-shortcuts-trigger">
<div role="group" aria-labelledby="dropdown-shortcuts-account">
<div role="heading" id="dropdown-shortcuts-account">My Account</div>
<div role="menuitem">
<span>Profile</span>
<kbd>⇧⌘P</kbd>
</div>
<div role="menuitem">
<span>Billing</span>
<kbd>⌘B</kbd>
</div>
<div role="menuitem">
<span>Settings</span>
<kbd>⌘S</kbd>
</div>
</div>
<hr role="separator" />
<div role="menuitem">
<span>Log out</span>
<kbd>⇧⌘Q</kbd>
</div>
</div>
</div>
</div>
</Preview>
### Icons
<Preview>
<div id="dropdown-icons" class="dropdown-menu">
<button type="button" id="dropdown-icons-trigger" aria-haspopup="menu" aria-controls="dropdown-icons-menu" aria-expanded="false" class="btn" data-variant="outline">Open</button>
<div id="dropdown-icons-popover" data-popover aria-hidden="true" class="min-w-32">
<div role="menu" id="dropdown-icons-menu" aria-labelledby="dropdown-icons-trigger">
<div role="menuitem"><svg class="lucide lucide-user" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" /></svg>
Profile
</div>
<div role="menuitem"><svg class="lucide lucide-credit-card" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2" /><line x1="2" x2="22" y1="10" y2="10" /></svg>
Billing
</div>
<div role="menuitem"><svg class="lucide lucide-settings" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /><circle cx="12" cy="12" r="3" /></svg>
Settings
</div>
<hr role="separator" />
<div role="menuitem" data-variant="destructive"><svg class="lucide lucide-log-out" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 17 5-5-5-5" /><path d="M21 12H9" /><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /></svg>
Log out
</div>
</div>
</div>
</div>
</Preview>
### Checkboxes
<Preview>
<div id="dropdown-checkboxes" class="dropdown-menu">
<button type="button" id="dropdown-checkboxes-trigger" aria-haspopup="menu" aria-controls="dropdown-checkboxes-menu" aria-expanded="false" class="btn" data-variant="outline">Open</button>
<div id="dropdown-checkboxes-popover" data-popover aria-hidden="true" class="w-40">
<div role="menu" id="dropdown-checkboxes-menu" aria-labelledby="dropdown-checkboxes-trigger">
<div role="group" aria-labelledby="dropdown-checkboxes-label">
<div role="heading" id="dropdown-checkboxes-label">Appearance</div>
<div role="menuitemcheckbox" aria-checked="true">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
Status Bar
</div>
<div role="menuitemcheckbox" aria-checked="false" aria-disabled="true">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
Activity Bar
</div>
<div role="menuitemcheckbox" aria-checked="false">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
Panel
</div>
</div>
</div>
</div>
</div>
</Preview>
### Checkboxes Icons
<Preview>
<div id="dropdown-checkboxes-icons" class="dropdown-menu">
<button type="button" id="dropdown-checkboxes-icons-trigger" aria-haspopup="menu" aria-controls="dropdown-checkboxes-icons-menu" aria-expanded="false" class="btn" data-variant="outline">Notifications</button>
<div id="dropdown-checkboxes-icons-popover" data-popover aria-hidden="true" class="w-48">
<div role="menu" id="dropdown-checkboxes-icons-menu" aria-labelledby="dropdown-checkboxes-icons-trigger">
<div role="group" aria-labelledby="dropdown-checkboxes-icons-label">
<div role="heading" id="dropdown-checkboxes-icons-label">Notification Preferences</div>
<div role="menuitemcheckbox" aria-checked="true">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
<svg class="lucide lucide-mail" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" /><rect x="2" y="4" width="20" height="16" rx="2" /></svg>
Email notifications
</div>
<div role="menuitemcheckbox" aria-checked="false">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
<svg class="lucide lucide-message-square" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" /></svg>
SMS notifications
</div>
<div role="menuitemcheckbox" aria-checked="true">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
<svg class="lucide lucide-bell" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></svg>
Push notifications
</div>
</div>
</div>
</div>
</div>
</Preview>
### Radio group
<Preview>
<div id="dropdown-radio-group" class="dropdown-menu">
<button type="button" id="dropdown-radio-group-trigger" aria-haspopup="menu" aria-controls="dropdown-radio-group-menu" aria-expanded="false" class="btn" data-variant="outline">Open</button>
<div id="dropdown-radio-group-popover" data-popover aria-hidden="true" class="min-w-32">
<div role="menu" id="dropdown-radio-group-menu" aria-labelledby="dropdown-radio-group-trigger">
<div role="group" aria-labelledby="dropdown-radio-label">
<div role="heading" id="dropdown-radio-label">Panel Position</div>
<div role="menuitemradio" aria-checked="false">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
Top
</div>
<div role="menuitemradio" aria-checked="true">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
Bottom
</div>
<div role="menuitemradio" aria-checked="false">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
Right
</div>
</div>
</div>
</div>
</div>
</Preview>
### Radio Icons
<Preview>
<div id="dropdown-radio-icons" class="dropdown-menu">
<button type="button" id="dropdown-radio-icons-trigger" aria-haspopup="menu" aria-controls="dropdown-radio-icons-menu" aria-expanded="false" class="btn" data-variant="outline">Payment Method</button>
<div id="dropdown-radio-icons-popover" data-popover aria-hidden="true" class="min-w-56">
<div role="menu" id="dropdown-radio-icons-menu" aria-labelledby="dropdown-radio-icons-trigger">
<div role="group" aria-labelledby="dropdown-radio-icons-label">
<div role="heading" id="dropdown-radio-icons-label">Select Payment Method</div>
<div role="menuitemradio" aria-checked="true">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
<svg class="lucide lucide-credit-card" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2" /><line x1="2" x2="22" y1="10" y2="10" /></svg>
Credit Card
</div>
<div role="menuitemradio" aria-checked="false">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
<svg class="lucide lucide-wallet" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1" /><path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4" /></svg>
PayPal
</div>
<div role="menuitemradio" aria-checked="false">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
<svg class="lucide lucide-building-2" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z" /><path d="M6 12H4a2 2 0 0 0-2 2v8h20v-8a2 2 0 0 0-2-2h-2" /><path d="M10 6h4" /><path d="M10 10h4" /><path d="M10 14h4" /><path d="M10 18h4" /></svg>
Bank Transfer
</div>
</div>
</div>
</div>
</div>
</Preview>
### Destructive
<Preview>
<div id="dropdown-destructive" class="dropdown-menu">
<button type="button" id="dropdown-destructive-trigger" aria-haspopup="menu" aria-controls="dropdown-destructive-menu" aria-expanded="false" class="btn" data-variant="outline">Actions</button>
<div id="dropdown-destructive-popover" data-popover aria-hidden="true" class="min-w-32">
<div role="menu" id="dropdown-destructive-menu" aria-labelledby="dropdown-destructive-trigger">
<div role="group">
<div role="menuitem"><svg class="lucide lucide-pencil" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9" /><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z" /></svg>
Edit
</div>
<div role="menuitem"><svg class="lucide lucide-share" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" /><polyline points="16 6 12 2 8 6" /><line x1="12" x2="12" y1="2" y2="15" /></svg>
Share
</div>
</div>
<hr role="separator" />
<div role="group">
<div role="menuitem" data-variant="destructive"><svg class="lucide lucide-trash" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18" /><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /></svg>
Delete
</div>
</div>
</div>
</div>
</div>
</Preview>
### Avatar
<Preview>
<div id="dropdown-avatar" class="dropdown-menu">
<button type="button" id="dropdown-avatar-trigger" aria-haspopup="menu" aria-controls="dropdown-avatar-menu" aria-expanded="false" class="btn rounded-full" data-variant="ghost" data-size="icon">
<span class="avatar">
<img src="https://github.com/shadcn.png" alt="shadcn" />
<span>LR</span>
</span>
</button>
<div id="dropdown-avatar-popover" data-popover aria-hidden="true" data-align="end">
<div role="menu" id="dropdown-avatar-menu" aria-labelledby="dropdown-avatar-trigger">
<div role="group">
<div role="menuitem"><svg class="lucide lucide-badge-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z" /><path d="m9 12 2 2 4-4" /></svg>
Account
</div>
<div role="menuitem"><svg class="lucide lucide-credit-card" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2" /><line x1="2" x2="22" y1="10" y2="10" /></svg>
Billing
</div>
<div role="menuitem"><svg class="lucide lucide-bell" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></svg>
Notifications
</div>
</div>
<hr role="separator" />
<div role="menuitem"><svg class="lucide lucide-log-out" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 17 5-5-5-5" /><path d="M21 12H9" /><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /></svg>
Sign Out
</div>
</div>
</div>
</div>
</Preview>
### RTL
Dropdown menus support document direction. Set `dir="rtl"` on the dropdown root or a parent element. Use logical alignment data attributes where possible.
<Preview>
<div dir="rtl">
<div id="dropdown-rtl" class="dropdown-menu">
<button type="button" id="dropdown-rtl-trigger" aria-haspopup="menu" aria-controls="dropdown-rtl-menu" aria-expanded="false" class="btn" data-variant="outline">افتح القائمة</button>
<div id="dropdown-rtl-popover" data-popover aria-hidden="true" class="w-36" data-align="end">
<div role="menu" id="dropdown-rtl-menu" aria-labelledby="dropdown-rtl-trigger">
<div role="group" aria-labelledby="dropdown-rtl-team">
<div role="heading" id="dropdown-rtl-team">الفريق</div>
<div role="menuitem">الفريق</div>
<div role="menuitem">
<span>فريق جديد</span>
<kbd>⌘+T</kbd>
</div>
</div>
<hr role="separator" />
<div role="group" aria-labelledby="dropdown-rtl-view">
<div role="heading" id="dropdown-rtl-view">عرض</div>
<div role="menuitemcheckbox" aria-checked="true">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
شريط الحالة
</div>
<div role="menuitemcheckbox" aria-checked="false">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
شريط النشاط
</div>
<div role="menuitemcheckbox" aria-checked="false">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
اللوحة
</div>
</div>
<hr role="separator" />
<div role="group" aria-labelledby="dropdown-rtl-position">
<div role="heading" id="dropdown-rtl-position">الموضع</div>
<div role="menuitemradio" aria-checked="false">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
أعلى
</div>
<div role="menuitemradio" aria-checked="true">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
أسفل
</div>
<div role="menuitemradio" aria-checked="false">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
يمين
</div>
<div role="menuitemradio" aria-checked="false">
<span data-indicator><svg class="lucide lucide-check" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg></span>
يسار
</div>
</div>
<hr role="separator" />
<div role="group">
<div role="menuitem" data-variant="destructive">تسجيل الخروج</div>
</div>
</div>
</div>
</div>
</div>
</Preview>
-403
View File
@@ -1,403 +0,0 @@
---
layout: layouts/page.njk
title: Dropdown Menu
description: Displays a menu to the user — such as a set of actions or functions — triggered by a button.
toc:
- label: Usage
id: usage
children:
- label: HTML + JavaScript
id: usage-html-js
children:
- label: "Step 1: Include the JavaScript files"
id: usage-html-js-1
- label: "Step 2: Add your dropdown menu HTML"
id: usage-html-js-2
- label: HTML structure
id: usage-html-js-3
- label: JavaScript events
id: usage-html-js-4
- label: Jinja and Nunjucks
id: usage-macro
- label: Examples
id: examples
children:
- label: Checkboxes
id: example-checkboxes
- label: Radio Group
id: example-radio-group
---
{% from "macros/code_block.njk" import code_block %}
{% from "macros/code_preview.njk" import code_preview %}
{% from "dropdown-menu.njk" import dropdown_menu %}
{% set code_html %}
{% call dropdown_menu(
id="demo-dropdown-menu",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="account-options">
<div role="heading" id="account-options">My Account</div>
<div role="menuitem">
Profile
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
<div role="menuitem">
Billing
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</div>
<div role="menuitem">
Settings
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</div>
<div role="menuitem">
Keyboard shortcuts
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘K</span>
</div>
</div>
<hr role="separator">
<div role="menuitem">
GitHub
</div>
<div role="menuitem">
Support
</div>
<div role="menuitem" aria-disabled="true">
API
</div>
<hr role="separator">
<div role="menuitem">
Logout
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
{% endcall %}
{% endset %}
{{ code_preview("dropdown-menu", code_html | prettyHtml) }}
<h2 id="usage">Usage</h2>
<h3 id="usage-html-js"><a href="#usage-html-js">HTML + JavaScript</a></h3>
<h4 id="usage-html-js-1"><a href="#usage-html-js-1">Step 1: Include the JavaScript files</a></h4>
<section class="prose">
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/basecoat.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/dropdown-menu.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/js/dropdown-menu.js" target="_blank">
dropdown-menu.js
{% lucide "arrow-right" %}
</a>
</div>
<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add your dropdown menu HTML</a></h4>
{{ code_block(code_html | prettyHtml, "html") }}
<h4 id="usage-html-js-3"><a href="#usage-html-js-3">HTML structure</a></h4>
<section class="prose">
<dl>
<dt><code class="highlight language-html">&lt;div class="dropdown-menu"&gt;</code></dt>
<dd>Wraps around the entire component.
<dl>
<dt><code class="highlight language-html">&lt;button type="button" popovertarget="{ POPOVER_ID }"&gt;</code></dt>
<dd>
<p>The trigger to open the popover, can also have the following attributes:</p>
<ul>
<li><code>id="{BUTTON_ID}"</code>: linked to by the <code>aria-labelledby</code> attribute of the listbox.</li>
<li><code>aria-haspopup="menu"</code>: indicates that the button opens a menu.</li>
<li><code>aria-controls="{ MENU_ID }"</code>: points to the menu's id.</li>
<li><code>aria-expanded="false"</code>: tracks the popover's state.</li>
</ul>
</dd>
<dt><code class="highlight language-html">&lt;div popover class="popover" id="{ POPOVER_ID }"&gt;</code></dt>
<dd>As with the <a href="/components/popover">Popover</a> component, you can set up the side and alignment of the popover using the <code>data-side</code> and <code>data-align</code> attributes.
<dl>
<dt><code class="highlight language-html">&lt;div role="menu"&gt;</code></dt>
<dd>The menu containing the options. Should have the following attributes:
<ul>
<li><code>id="{ MENU_ID }"</code>: refered to by the <code>aria-controls</code> attribute of the trigger.</li>
<li><code>aria-labelledby="{ BUTTON_ID }"</code>: linked to by the button's <code>id</code> attribute.</li>
</ul>
<dl>
<dt><code class="highlight language-html">&lt;button role="menuitem"&gt;</code></dt>
<dd>Menu item.</dd>
<dt><code class="highlight language-html">&lt;button role="menuitemcheckbox"&gt;</code></dt>
<dd>Menu item with a checkbox.</dd>
<dt><code class="highlight language-html">&lt;button role="menuitemradio"&gt;</code></dt>
<dd>Menu item with a radio button.</dd>
<dt><code class="highlight language-html">&lt;hr role="separator"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Separator between groups/options.</dd>
<dt><code class="highlight language-html">&lt;div role="group"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Group of options, can have a <code>aria-labelledby</code> attribute to link to a heading.</dd>
<dt><code class="highlight language-html">&lt;span role="heading"&gt;</code></dt>
<dd>Group heading, must have an <code>id</code> attribute if you use the <code>aria-labelledby</code> attribute on the group.</dd>
</dl>
</dd>
</dl>
</dl>
</dd>
</dl>
</section>
<h4 id="usage-html-js-4"><a href="#usage-html-js-4">JavaScript events</a></h4>
<section class="prose">
<dl>
<dt><code>basecoat:initialized</code></dt>
<dd>Once the component is fully initialized, it dispatches a custom (non-bubbling) <code>basecoat:initialized</code> event on itself.</dd>
<dt><code>basecoat:popover</code></dt>
<dd>When the popover opens, the component dispatches a custom (non-bubbling) <code>basecoat:popover</code> event on <code>document</code>. Other popover components (Combobox, Dropdown Menu, Popover and Select) listen for this to close any open popovers.</dd>
</dl>
</section>
<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>
<div class="prose">
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">dropdown_menu()</code> Nunjucks or Jinja macro for this component.</p>
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/dropdown-menu.html.jinja" target="_blank">
Jinja macro
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/dropdown-menu.njk" target="_blank">
Nunjucks macro
{% lucide "arrow-right" %}
</a>
</div>
{% set raw_code %}{% raw %}{% call dropdown_menu(
id="dropdown-menu",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="account-options">
<div role="heading" id="account-options">My Account</div>
<div role="menuitem">
Profile
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
<div role="menuitem">
Billing
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</div>
<div role="menuitem">
Settings
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</div>
<div role="menuitem">
Keyboard shortcuts
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘K</span>
</div>
</div>
<hr role="separator">
<div role="menuitem">
GitHub
</div>
<div role="menuitem">
Support
</div>
<div role="menuitem" disabled>
API
</div>
<hr role="separator">
<div role="menuitem">
Logout
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
{% endcall %}{% endraw %}{% endset%}
{{ code_block(raw_code, "jinja") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-checkboxes"><a href="#example-checkboxes">Checkboxes</a></h3>
{% set code %}{% call dropdown_menu(
id="dropdown-menu-checkboxes",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="account-options">
<div role="heading" id="account-options">Account Options</div>
<div role="menuitem">
{% lucide "user" %}
Profile
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
<div role="menuitem">
{% lucide "credit-card" %}
Billing
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</div>
<div role="menuitem">
{% lucide "settings" %}
Settings
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</div>
</div>
<hr role="separator">
<div role="group" aria-labelledby="appearance-options">
<div role="heading" id="appearance-options">Appearance</div>
<div
role="menuitemcheckbox"
aria-checked="true"
class="group"
>
{% lucide "check", {
"class": "invisible group-aria-checked:visible",
"aria-hidden": "true",
"focusable": "false"
} %}
Status Bar
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
<div
role="menuitemcheckbox"
aria-checked="false"
class="group"
aria-disabled="true"
>
{% lucide "check", {
"class": "invisible group-aria-checked:visible",
"aria-hidden": "true",
"focusable": "false"
} %}
Activity Bar
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</div>
<div
role="menuitemcheckbox"
aria-checked="false"
class="group"
>
{% lucide "check", {
"class": "invisible group-aria-checked:visible",
"aria-hidden": "true",
"focusable": "false"
} %}
Panel
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</div>
</div>
<hr role="separator">
<div role="menuitem">
{% lucide "log-out" %}
Logout
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
{% endcall %}
<script>
(() => {
const dropdownMenu = document.querySelector('#dropdown-menu-checkboxes');
const checkboxes = dropdownMenu.querySelectorAll('div[role="menuitemcheckbox"]');
checkboxes.forEach(checkbox => {
checkbox.addEventListener('click', () => {
const isChecked = checkbox.getAttribute('aria-checked') === 'true';
checkbox.setAttribute('aria-checked', !isChecked);
});
});
})();
</script>
{% endset %}
{{ code_preview("dropdown-menu-checkboxes", code | prettyHtml) }}
<h3 id="example-radio-group"><a href="#example-radio-group">Radio Group</a></h3>
{% set code %}{% call dropdown_menu(
id="dropdown-menu-radio-group",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="position-options">
<span id="position-options" role="heading">Panel Position</span>
<hr role="separator">
<div
role="menuitemradio"
aria-checked="false"
class="group"
>
<div class="size-4 flex items-center justify-center">
<div
class="size-2 rounded-full bg-foreground invisible group-aria-checked:visible"
aria-hidden="true",
focusable="false"
></div>
</div>
Status Bar
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
<div
role="menuitemradio"
aria-checked="true"
class="group"
>
<div class="size-4 flex items-center justify-center">
<div
class="size-2 rounded-full bg-foreground invisible group-aria-checked:visible"
aria-hidden="true",
focusable="false"
></div>
</div>
Activity Bar
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</div>
<div
role="menuitemradio"
aria-checked="false"
class="group"
>
<div class="size-4 flex items-center justify-center">
<div
class="size-2 rounded-full bg-foreground invisible group-aria-checked:visible"
aria-hidden="true",
focusable="false"
></div>
</div>
Panel
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</div>
</div>
{% endcall %}
<script>
(() => {
const dropdownMenu = document.querySelector('#dropdown-menu-radio-group');
const radioButtons = dropdownMenu.querySelectorAll('div[role="menuitemradio"]');
radioButtons.forEach(radioButton => {
radioButton.addEventListener('click', () => {
radioButtons.forEach(radioButton => {
radioButton.setAttribute('aria-checked', 'false');
});
radioButton.setAttribute('aria-checked', 'true');
});
});
})();
</script>
{% endset %}
{{ code_preview("dropdown-menu", code | prettyHtml) }}
+210
View File
@@ -0,0 +1,210 @@
# Empty
<Preview>
<section class="empty">
<header>
<figure><svg class="lucide lucide-folder-code" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 10.5 8 13l2 2.5" /><path d="m14 10.5 2 2.5-2 2.5" /><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2z" /></svg></figure>
<h3>No Projects Yet</h3>
<p>You haven't created any projects yet. Get started by creating your first project.</p>
</header>
<footer>
<div class="flex gap-2">
<button class="btn">Create Project</button>
<button class="btn" data-variant="outline">Import Project</button>
</div>
<a href="#" class="btn text-muted-foreground" data-variant="link" data-size="sm">
Learn More
<svg class="lucide lucide-arrow-up-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h10v10" /><path d="M7 17 17 7" /></svg>
</a>
</footer>
</section>
</Preview>
## Usage
<Steps>
<Step title="Include CSS">
Import Tailwind and one full Basecoat style bundle.
```css
@import "tailwindcss";
@import "basecoat-css/vega.css";
```
Or import only the base CSS, Empty component CSS, and one style pack.
```css
@import "tailwindcss";
@import "basecoat-css/base.css";
@import "basecoat-css/components/empty.css";
@import "basecoat-css/styles/vega.css";
```
Using CDN or bundler imports? See the [Installation page](/installation).
</Step>
<Step title="Add your empty HTML">
Add `class="empty"` to a semantic container. The component styles common children like `header`, `figure`, headings, descriptions, and `footer`.
```html
<section class="empty">
<header>
<figure><svg class="lucide lucide-folder-code" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 10.5 8 13l2 2.5" /><path d="m14 10.5 2 2.5-2 2.5" /><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2z" /></svg></figure>
<h3>No Projects Yet</h3>
<p>You haven't created any projects yet. Get started by creating your first project.</p>
</header>
<footer>
<div class="flex gap-2">
<button class="btn">Create Project</button>
<button class="btn" data-variant="outline">Import Project</button>
</div>
<a href="#" class="btn text-muted-foreground" data-variant="link" data-size="sm">
Learn More
<svg class="lucide lucide-arrow-up-right" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h10v10" /><path d="M7 17 17 7" /></svg>
</a>
</footer>
</section>
```
</Step>
</Steps>
#### HTML structure
<dl>
<dt><code>&lt;section class="empty"&gt;</code></dt>
<dd>
The empty-state container.
<dl>
<dt><code>&lt;header&gt;</code></dt>
<dd>
Groups the media, title, and description.
<dl>
<dt><code>&lt;figure&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Icon, avatar, image, or avatar group. An icon-only figure is styled automatically with <code>:has(&gt; svg:only-child)</code>.</dd>
<dt><code>&lt;h2&gt;</code>, <code>&lt;h3&gt;</code>, or <code>&lt;h4&gt;</code></dt>
<dd>The empty-state title.</dd>
<dt><code>&lt;p&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>The empty-state description.</dd>
</dl>
</dd>
<dt><code>&lt;section&gt;</code> or <code>&lt;footer&gt;</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Actions and supporting content.</dd>
</dl>
</dd>
</dl>
## Examples
### Outline
<Preview class="w-full">
<section class="empty border border-dashed">
<header>
<figure><svg class="lucide lucide-cloud" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></svg></figure>
<h3>Cloud Storage Empty</h3>
<p>Upload files to your cloud storage to access them anywhere.</p>
</header>
<footer>
<button class="btn" data-variant="outline" data-size="sm">Upload Files</button>
</footer>
</section>
</Preview>
### Background
<Preview class="w-full">
<section class="empty h-full bg-muted/30">
<header>
<figure><svg class="lucide lucide-bell" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></svg></figure>
<h3>No Notifications</h3>
<p class="max-w-xs text-pretty">You're all caught up. New notifications will appear here.</p>
</header>
<footer>
<button class="btn" data-variant="outline"><svg class="lucide lucide-refresh-ccw" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" /><path d="M3 3v5h5" /><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" /><path d="M16 16h5v5" /></svg>
Refresh
</button>
</footer>
</section>
</Preview>
### Avatar
<Preview>
<section class="empty">
<header>
<figure class="size-12 rounded-full">
<img class="grayscale" alt="@shadcn" src="https://github.com/shadcn.png" />
</figure>
<h3>User Offline</h3>
<p>This user is currently offline. You can leave a message to notify them or try again later.</p>
</header>
<footer>
<button class="btn" data-size="sm">Leave Message</button>
</footer>
</section>
</Preview>
### Avatar group
<Preview>
<section class="empty">
<header>
<figure class="-space-x-2 [&_img]:ring-background [&_img]:ring-2 [&_img]:grayscale [&_img]:size-12 [&_img]:rounded-full">
<img alt="@shadcn" src="https://github.com/shadcn.png" />
<img alt="@maxleiter" src="https://github.com/maxleiter.png" />
<img alt="@evilrabbit" src="https://github.com/evilrabbit.png" />
</figure>
<h3>No Team Members</h3>
<p>Invite your team to collaborate on this project.</p>
</header>
<footer>
<button class="btn" data-size="sm"><svg class="lucide lucide-plus" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14" /><path d="M12 5v14" /></svg>
<span>Invite Members</span>
</button>
</footer>
</section>
</Preview>
### Input group
<Preview>
<section class="empty">
<header>
<h3>404 - Not Found</h3>
<p>The page you're looking for doesn't exist. Try searching for what you need below.</p>
</header>
<footer>
<div class="input-group sm:w-3/4">
<input type="search" placeholder="Try searching for pages..." />
<span data-align="start" aria-hidden="true"><svg class="lucide lucide-search" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34" /><circle cx="11" cy="11" r="8" /></svg></span>
<span data-align="end"><kbd class="kbd">/</kbd></span>
</div>
<p>Need help? <a href="#">Contact support</a></p>
</footer>
</section>
</Preview>
### RTL
<Preview>
<section class="empty" dir="rtl">
<header>
<figure><svg class="lucide lucide-folder-code" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 10.5 8 13l2 2.5" /><path d="m14 10.5 2 2.5-2 2.5" /><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2z" /></svg></figure>
<h3>لا توجد مشاريع بعد</h3>
<p>لم تقم بإنشاء أي مشاريع بعد. ابدأ بإنشاء مشروعك الأول.</p>
</header>
<footer>
<div class="flex flex-row justify-center gap-2">
<button class="btn">إنشاء مشروع</button>
<button class="btn" data-variant="outline">استيراد مشروع</button>
</div>
<a href="#" class="btn text-muted-foreground" data-variant="link" data-size="sm">
تعرف على المزيد
<svg class="lucide lucide-arrow-up-right rtl:rotate-270" data-icon="inline-end" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h10v10" /><path d="M7 17 17 7" /></svg>
</a>
</footer>
</section>
</Preview>

Some files were not shown because too many files have changed in this diff Show More