Migrate empty and item docs to Basecoat APIs

This commit is contained in:
Ronan Berder
2026-06-03 18:59:21 +08:00
parent 317c490e0c
commit 7e7d509e90
164 changed files with 15779 additions and 2940 deletions
+1 -1
View File
@@ -87,7 +87,7 @@ toc:
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
<a class="badge-outline" href="/cli#macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
+47 -2
View File
@@ -14,6 +14,12 @@ toc:
id: example-no-description
- label: No icon
id: example-no-icon
- label: Action
id: example-action
- label: Custom Colors
id: example-custom-colors
- label: RTL
id: example-rtl
---
{% from "macros/code_preview.njk" import code_preview %}
@@ -47,7 +53,7 @@ toc:
{% endset %}
{{ code_preview("alert", code | prettyHtml) }}
<h2 id="usage">Usage</h2>
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Use the <code>alert</code> or <code>alert-destructive</code>.</p>
@@ -74,6 +80,8 @@ toc:
<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>
<dt><code class="highlight language-html">&lt;div data-action&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>An action aligned to the inline end of the alert.</dd>
</dl>
</dd>
</dl>
@@ -105,4 +113,41 @@ toc:
<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) }}
{{ code_preview("alert-no-icon", code) }}
<h3 id="example-action"><a href="#example-action">Action</a></h3>
{% set code %}<div class="alert">
{% lucide "info" %}
<h2>Enable notifications</h2>
<section>Receive updates when there is activity in your workspace.</section>
<div data-action>
<button type="button" class="btn-xs-outline">Enable</button>
</div>
</div>{% endset %}
{{ code_preview("alert-action", code) }}
<h3 id="example-custom-colors"><a href="#example-custom-colors">Custom Colors</a></h3>
{% set code %}<div class="alert bg-amber-50 text-amber-900 dark:bg-amber-950 dark:text-amber-200">
{% lucide "triangle-alert" %}
<h2>Storage almost full</h2>
<section class="text-amber-800 dark:text-amber-300">Upgrade your plan or delete unused files.</section>
</div>{% endset %}
{{ code_preview("alert-custom-colors", code) }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Alerts support right-to-left layouts through logical spacing and native text flow. Add <code>dir="rtl"</code> to the alert or an ancestor.</p>
</section>
{% set code %}<div class="alert" dir="rtl">
{% lucide "info" %}
<h2>تم حفظ التغييرات</h2>
<section>تم تحديث إعداداتك بنجاح.</section>
<div data-action>
<button type="button" class="btn-xs-outline">عرض</button>
</div>
</div>{% endset %}
{{ code_preview("alert-rtl", code) }}
+29 -2
View File
@@ -20,6 +20,10 @@ toc:
id: example-with-icon
- label: Link
id: example-link
- label: Custom Colors
id: example-custom-colors
- label: RTL
id: example-rtl
---
{% from "macros/code_preview.njk" import code_preview %}
@@ -49,7 +53,8 @@ toc:
<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>.
<p>Use one of the badge classes: <code>badge</code>, <code>badge-primary</code>, <code>badge-secondary</code>, <code>badge-destructive</code>, <code>badge-outline</code>, <code>badge-ghost</code>, or <code>badge-link</code>.</p>
<p>When an icon needs directional spacing, add <code>data-icon="inline-start"</code> or <code>data-icon="inline-end"</code> to the icon.</p>
</section>
{{ code_block(code) }}
@@ -88,6 +93,28 @@ toc:
{% set code %}<a href="#" class="badge-outline">
Link
{% lucide "arrow-right" %}
{% lucide "arrow-right", { "data-icon": "inline-end", "class": "rtl:rotate-180" } %}
</a>{% endset %}
{{ code_preview("badge-link", code) }}
<h3 id="example-custom-colors"><a href="#example-custom-colors">Custom Colors</a></h3>
{% set code %}<span class="badge bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300">
Custom
</span>{% endset %}
{{ code_preview("badge-custom-colors", code) }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Badges support right-to-left layouts through native text flow and logical icon spacing. Directional icons should be flipped explicitly.</p>
</section>
{% set code %}<div dir="rtl" class="flex flex-wrap gap-2">
<span class="badge">نشط</span>
<a href="#" class="badge-outline">
التفاصيل
{% lucide "arrow-right", { "data-icon": "inline-end", "class": "rtl:rotate-180" } %}
</a>
</div>{% endset %}
{{ code_preview("badge-rtl", code) }}
+104 -8
View File
@@ -5,23 +5,35 @@ description: A container that groups related buttons together with consistent st
toc:
- label: Usage
id: usage
- label: Accessibility
id: accessibility
- label: Button Group vs Toggle Group
id: button-group-vs-toggle-group
- label: Examples
id: examples
children:
- label: Orientation
id: example-orientation
- label: Size
id: example-size
- label: Nested
id: example-nested
- label: Separator
id: example-separator
- label: Split
id: example-split
- label: Input
id: example-input
- label: Input Group
id: example-input-group
- label: Dropdown Menu
id: example-dropdown-menu
- label: Select
id: example-select
- label: Popover
id: example-popover
- label: RTL
id: example-rtl
---
{% from "dropdown-menu.njk" import dropdown_menu %}
@@ -77,22 +89,45 @@ toc:
</div>
{% endcall %}
</div>
</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>
<section class="prose">
<p>Wrap related buttons or inputs in a <code>&lt;div role="group" class="button-group"&gt;</code> element. Add <code>aria-label</code> or <code>aria-labelledby</code> when the group purpose is not already clear from nearby text.</p>
<p>Use <code>data-orientation="vertical"</code> to stack items vertically. Use <code>&lt;hr role="separator"&gt;</code> to visually divide non-outline buttons. Use <code>button-group-text</code> for static text inside a group.</p>
</section>
{% set code_simple %}
<div role="group" class="button-group">
<div role="group" aria-label="Message actions" 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="accessibility"><a href="#accessibility">Accessibility</a></h2>
<section class="prose">
<ul>
<li>Use <code>role="group"</code> on the <code>button-group</code> container.</li>
<li>Use <code>aria-label</code> or <code>aria-labelledby</code> to label groups whose purpose is not obvious.</li>
<li>Keyboard navigation remains native: users tab through each button, input, select, dropdown trigger, or popover trigger.</li>
</ul>
</section>
<h2 id="button-group-vs-toggle-group"><a href="#button-group-vs-toggle-group">Button Group vs Toggle Group</a></h2>
<section class="prose">
<ul>
<li>Use Button Group for related actions, such as archive/report/snooze.</li>
<li>Use Toggle Group when each item controls selected/on state.</li>
</ul>
</section>
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-orientation"><a href="#example-orientation">Orientation</a></h3>
@@ -144,7 +179,7 @@ toc:
<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" aria-label="Pagination controls" class="button-group">
<div role="group" class="button-group">
<button type="button" class="btn-sm-outline">1</button>
<button type="button" class="btn-sm-outline">2</button>
@@ -181,18 +216,55 @@ toc:
{{ code_preview("button-group-separator", code_separator | prettyHtml) }}
<h3 id="example-split"><a href="#example-split">Split</a></h3>
<section class="prose">
<p>Use a separator to create a split action with a primary action and a secondary menu trigger.</p>
</section>
{% set code_split %}
<div role="group" aria-label="Split save actions" class="button-group">
<button type="button" class="btn-secondary">Save</button>
<hr role="separator">
{% set trigger_split %}
{% lucide "chevron-down" %}
{% endset %}
{% call dropdown_menu(
trigger=trigger_split,
trigger_attrs={"class": "btn-icon-secondary", "aria-label": "More save options"},
popover_attrs={"data-align": "end"}
) %}
<div role="menuitem">Save as draft</div>
<div role="menuitem">Save and publish</div>
<div role="menuitem">Save as template</div>
{% endcall %}
</div>
{% endset %}
{{ code_preview("button-group-split", code_split | prettyHtml) }}
<h3 id="example-input"><a href="#example-input">Input</a></h3>
{% set code_input %}
<div class="button-group">
<div role="group" aria-label="Search" class="button-group w-full max-w-sm">
<input type="text" class="input" placeholder="Search...">
<button type="button" class="btn-icon-outline">{% lucide "search" %}</button>
<button type="button" class="btn-icon-outline" aria-label="Search">{% lucide "search" %}</button>
</div>
{% endset %}
{{ code_preview("button-group-input", code_input | prettyHtml) }}
<h3 id="example-input-group"><a href="#example-input-group">Input Group</a></h3>
{% set code_input_group %}
<div role="group" aria-label="Amount" class="button-group w-full max-w-sm">
<div class="button-group-text">$</div>
<input type="text" class="input" placeholder="0.00">
<div class="button-group-text">USD</div>
</div>
{% endset %}
{{ code_preview("button-group-input-group", code_input_group | prettyHtml) }}
<h3 id="example-dropdown-menu"><a href="#example-dropdown-menu">Dropdown Menu</a></h3>
@@ -246,7 +318,7 @@ toc:
{% set code_select %}
<div class="flex w-fit items-stretch gap-2">
<div class="button-group">
<div role="group" aria-label="Currency amount" 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>
@@ -269,14 +341,17 @@ toc:
{% lucide "bot" %}
Copilot
</button>
{% set trigger_popover %}
{% lucide "chevron-down" %}
{% endset %}
{% call popover(
trigger=trigger,
trigger=trigger_popover,
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>
<textarea class="textarea w-full mb-4" placeholder="Describe your task 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
@@ -288,3 +363,24 @@ toc:
{% endset %}
{{ code_preview("button-group-popover", code_popover | prettyHtml) }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Button groups support right-to-left layouts through logical border and radius rules. Add <code>dir="rtl"</code> to the group or an ancestor. Directional icons should be flipped explicitly.</p>
</section>
{% set code_rtl %}
<div dir="rtl">
<div role="group" aria-label="إجراءات الرسالة" class="button-group">
<button type="button" class="btn-outline">أرشفة</button>
<button type="button" class="btn-outline">تقرير</button>
<button type="button" class="btn-outline">
إرسال
{% lucide "arrow-right", { "class": "rtl:rotate-180" } %}
</button>
</div>
</div>
{% endset %}
{{ code_preview("button-group-rtl", code_rtl | prettyHtml) }}
+127 -66
View File
@@ -8,57 +8,55 @@ toc:
- label: Examples
id: examples
children:
- label: Primary
id: example-primary
- label: Secondary
id: example-secondary
- label: Destructive
id: example-destructive
- label: Size
id: example-size
- label: Default
id: example-default
- label: Outline
id: example-outline
- label: Secondary
id: example-secondary
- label: Ghost
id: example-ghost
- label: Destructive
id: example-destructive
- 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: Extra Small
id: example-extra-small
- label: Extra Small Icon
id: example-extra-small-icon
- label: Small
id: example-small
- label: Rounded
id: example-rounded
- label: Spinner
id: example-spinner
- label: Button Group
id: example-button-group
- label: RTL
id: example-rtl
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<button class="btn">Button</button>{% endset %}
{% set code %}<button type="button" class="btn">Button</button>{% endset %}
{{ code_preview("button", code) }}
<h2 id="usage">Usage</h2>
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>You can use one of the <code>btn</code> classes to style any element as a button:</p>
<p>You can use one of the <code>btn</code> classes to style a button element:</p>
<ul>
<li>
<b>Variants</b>:
<ul>
<li><code>btn</code> or <code>btn-primary</code> for primary
buttons,</li>
<li><code>btn</code> or <code>btn-primary</code> for default/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>
@@ -67,94 +65,157 @@ toc:
<li><code>btn-xs</code> for extra small buttons.</li>
<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>, <code>btn-xs-outline</code> or <code>btn-sm-icon-outline</code>.</li>
<li><code>btn-icon</code> for icon buttons. This can be combined with sizes and variants, for example <code>btn-sm-icon-outline</code> or <code>btn-xs-icon-destructive</code>.</li>
</ul>
</li>
</ul>
<p>Button classes are combined into a single class name. Use <code>btn-{variant}</code>, <code>btn-{size}</code>, <code>btn-{size}-{variant}</code>, <code>btn-icon-{variant}</code>, or <code>btn-{size}-icon-{variant}</code>. Do not stack classes like <code>btn-sm btn-outline</code>; use <code>btn-sm-outline</code> instead.</p>
<p>Basecoat follows Tailwind v4's default button cursor behavior and does not force <code>cursor: pointer</code>.</p>
</section>
{{ code_block(code) }}
{% set code_composition %}<button type="button" class="btn">Default</button>
<button type="button" class="btn-outline">Outline</button>
<button type="button" class="btn-sm">Small</button>
<button type="button" class="btn-sm-outline">Small outline</button>
<button type="button" class="btn-icon-outline" aria-label="Settings">
{% lucide "settings" %}
</button>
<button type="button" class="btn-sm-icon-outline" aria-label="Settings">
{% lucide "settings" %}
</button>{% endset %}
{{ code_block(code_composition) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-primary"><a href="#example-primary">Primary</a></h3>
<h3 id="example-size"><a href="#example-size">Size</a></h3>
{% set code %}<button class="btn">Primary</button>{% endset %}
{{ code_preview("button-primary", code) }}
<section class="prose">
<p>Use size-specific classes to change the size of the button.</p>
</section>
<h3 id="example-secondary"><a href="#example-secondary">Secondary</a></h3>
{% set code_size %}<div class="flex flex-wrap items-center gap-2">
<button type="button" class="btn-xs">Extra Small</button>
<button type="button" class="btn-sm">Small</button>
<button type="button" class="btn">Default</button>
<button type="button" class="btn-lg">Large</button>
</div>{% endset %}
{{ code_preview("button-size", code_size) }}
{% set code %}<button class="btn-secondary">Button</button>{% endset %}
{{ code_preview("button-secondary", code) }}
<h3 id="example-default"><a href="#example-default">Default</a></h3>
<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) }}
{% set code %}<button type="button" class="btn">Button</button>{% endset %}
{{ code_preview("button-default", code) }}
<h3 id="example-outline"><a href="#example-outline">Outline</a></h3>
{% set code %}<button class="btn-outline">Outline</button>{% endset %}
{% set code %}<button type="button" class="btn-outline">Outline</button>{% endset %}
{{ code_preview("button-outline", code) }}
<h3 id="example-secondary"><a href="#example-secondary">Secondary</a></h3>
{% set code %}<button type="button" class="btn-secondary">Secondary</button>{% endset %}
{{ code_preview("button-secondary", code) }}
<h3 id="example-ghost"><a href="#example-ghost">Ghost</a></h3>
{% set code %}<button class="btn-ghost">Ghost</button>{% endset %}
{% set code %}<button type="button" class="btn-ghost">Ghost</button>{% endset %}
{{ code_preview("button-ghost", code) }}
<h3 id="example-destructive"><a href="#example-destructive">Destructive</a></h3>
{% set code %}<button type="button" class="btn-destructive">Destructive</button>{% endset %}
{{ code_preview("button-destructive", code) }}
<h3 id="example-link"><a href="#example-link">Link</a></h3>
{% set code %}<button class="btn-link">Link</button>{% endset %}
{% set code %}<button type="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">
{% set code %}<button type="button" class="btn-icon-outline" aria-label="Open">
{% 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
<section class="prose">
<p>Mark inline icons with <code>data-icon="inline-start"</code> or <code>data-icon="inline-end"</code> when the button should use icon-aware spacing.</p>
</section>
{% set code %}<button type="button" class="btn">
{% lucide "git-branch", { "data-icon": "inline-start" } %}
New Branch
</button>{% endset %}
{{ code_preview("button-with-icon", code) }}
<h3 id="example-loading"><a href="#example-loading">Loading</a></h3>
<h3 id="example-rounded"><a href="#example-rounded">Rounded</a></h3>
{% set code %}<button class="btn-outline" disabled>
{% lucide "loader", { "class": "animate-spin" } %}
Loading
<section class="prose">
<p>Use standard Tailwind classes to override the button radius.</p>
</section>
{% set code %}<button type="button" class="btn rounded-full">
{% lucide "arrow-up" %}
Rounded
</button>{% endset %}
{{ code_preview("button-loading", code) }}
{{ code_preview("button-rounded", code) }}
<h3 id="example-large"><a href="#example-large">Large</a></h3>
<h3 id="example-spinner"><a href="#example-spinner">Spinner</a></h3>
{% set code %}<button class="btn-lg">
{% lucide "shopping-cart" %}
Buy
</button>{% endset %}
{{ code_preview("button-large", code) }}
{% set code_spinner %}<div class="flex flex-wrap items-center gap-2">
<button type="button" class="btn" disabled>
{% lucide "loader", { "class": "animate-spin" } %}
Generating
</button>
<button type="button" class="btn-outline">
Downloading
{% lucide "loader", { "class": "animate-spin" } %}
</button>
</div>{% endset %}
{{ code_preview("button-spinner", code_spinner) }}
<h3 id="example-extra-small"><a href="#example-extra-small">Extra Small</a></h3>
<h3 id="example-button-group"><a href="#example-button-group">Button Group</a></h3>
{% set code %}<button class="btn-xs-outline">
XS outline
</button>{% endset %}
{{ code_preview("button-extra-small", code) }}
<section class="prose">
<p>To group related buttons, use the <a href="/components/button-group/">Button Group</a> component.</p>
</section>
<h3 id="example-extra-small-icon"><a href="#example-extra-small-icon">Extra Small Icon</a></h3>
{% set code_button_group %}<div class="flex w-fit items-stretch gap-2">
<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>
<button type="button" class="btn-icon-outline" aria-label="More options">
{% lucide "ellipsis" %}
</button>
</div>
</div>{% endset %}
{{ code_preview("button-group-demo", code_button_group) }}
{% set code %}<button class="btn-xs-icon-outline">
{% lucide "plus" %}
</button>{% endset %}
{{ code_preview("button-extra-small-icon", code) }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<h3 id="example-small"><a href="#example-small">Small</a></h3>
<section class="prose">
<p>Buttons support right-to-left layouts through logical spacing. Add <code>dir="rtl"</code> to the button or an ancestor. Directional icons should be flipped explicitly.</p>
</section>
{% set code %}<button class="btn-sm-icon-destructive">
{% lucide "trash" %}
</button>{% endset %}
{{ code_preview("button-small", code) }}
{% set code_rtl %}<div dir="rtl" class="flex flex-wrap items-center gap-2">
<button type="button" class="btn-outline">زر</button>
<button type="button" class="btn-destructive">حذف</button>
<button type="button" class="btn-outline">
إرسال
{% lucide "arrow-right", { "data-icon": "inline-end", "class": "rtl:rotate-180" } %}
</button>
<button type="button" class="btn-secondary" disabled>
{% lucide "loader", { "class": "animate-spin" } %}
جاري التحميل
</button>
</div>{% endset %}
{{ code_preview("button-rtl", code_rtl) }}
+47 -10
View File
@@ -8,10 +8,14 @@ toc:
- label: Examples
id: examples
children:
- label: Size
id: example-size
- label: With content and footer
id: example-with-content-and-footer
- label: With image
id: example-with-image
- label: RTL
id: example-rtl
---
{% from "macros/code_preview.njk" import code_preview %}
@@ -23,17 +27,17 @@ toc:
<p>Enter your details below to login to your account</p>
</header>
<section>
<form class="form grid gap-6">
<form class="grid gap-6">
<div class="grid gap-2">
<label for="demo-card-form-email">Email</label>
<input type="email" id="demo-card-form-email">
<label class="label" for="demo-card-form-email">Email</label>
<input class="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>
<label class="label" for="demo-card-form-password">Password</label>
<a href="#" class="ms-auto inline-block text-sm underline-offset-4 hover:underline">Forgot your password?</a>
</div>
<input type="password" id="demo-card-form-password">
<input class="input" type="password" id="demo-card-form-password">
</div>
</form>
</section>
@@ -45,7 +49,7 @@ toc:
</div>{% endset %}
{{ code_preview("card", code, "w-full max-w-sm") }}
<h2 id="usage">Usage</h2>
<h2 id="usage"><a href="#usage">Usage</a></h2>
{% set code %}<div class="card">
<header>
@@ -87,6 +91,19 @@ toc:
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-size"><a href="#example-size">Size</a></h3>
{% set code %}<div class="card" data-size="sm">
<header>
<h2>Small Card</h2>
<p>A compact card with reduced spacing.</p>
</header>
<section>
<p class="text-sm">Use <code>data-size="sm"</code> for denser layouts.</p>
</section>
</div>{% endset %}
{{ code_preview("card-size", code) }}
<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">
@@ -96,7 +113,7 @@ toc:
</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">
<ol class="mt-4 flex list-decimal flex-col gap-2 ps-6">
<li>New analytics widgets for daily/weekly metrics</li>
<li>Simplified navigation menu</li>
<li>Dark mode support</li>
@@ -148,9 +165,29 @@ toc:
{% lucide "land-plot" %}
350m²
</span>
<span class="ml-auto font-medium tabular-nums">
<span class="ms-auto font-medium tabular-nums">
$135,000
</span>
</footer>
</div>{% endset %}
{{ code_preview("card-with-image", code) }}
{{ code_preview("card-with-image", code) }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Cards support right-to-left layouts through native text flow. Add <code>dir="rtl"</code> to the card or an ancestor.</p>
</section>
{% set code %}<div class="card" dir="rtl">
<header>
<h2>إعدادات الحساب</h2>
<p>راجع إعدادات الأمان والإشعارات.</p>
</header>
<section>
<p class="text-sm">يمكنك تعديل هذه الإعدادات في أي وقت.</p>
</section>
<footer>
<button type="button" class="btn">حفظ</button>
</footer>
</div>{% endset %}
{{ code_preview("card-rtl", code) }}
+131 -66
View File
@@ -5,97 +5,162 @@ description: A control that allows the user to toggle between checked and not ch
toc:
- label: Usage
id: usage
- label: Checked state
id: checked-state
- label: Invalid state
id: invalid-state
- label: Examples
id: examples
children:
- label: Basic
id: example-basic
- label: Description
id: example-description
- label: Disabled
id: example-disabled
- label: With text
id: example-with-text
- label: Form
id: example-form
- label: Group
id: example-group
- label: Table
id: example-table
- label: RTL
id: example-rtl
---
{% 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 %}
{% set code %}<div role="group" class="field flex-row items-center gap-3">
<input type="checkbox" id="terms" class="input">
<label for="terms">Accept terms and conditions</label>
</div>{% endset %}
{{ code_preview("checkbox", code) }}
<h2 id="usage">Usage</h2>
<h2 id="usage"><a href="#usage">Usage</a></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>
<p>Add the <code>input</code> class to your <code>&lt;input type="checkbox"&gt;</code> elements. Pair it with <a href="/components/field">Field</a> and a native <code>&lt;label&gt;</code> for proper layout and labeling.</p>
</section>
{% set code %}<input type="checkbox" class="input">{% endset %}
{{ code_block(code) }}
<h2 id="checked-state"><a href="#checked-state">Checked state</a></h2>
<section class="prose">
<p>Use native HTML attributes for state. Add <code>checked</code> for an initially checked checkbox, or read and update the element's <code>checked</code> property in JavaScript.</p>
</section>
{% set code %}<input type="checkbox" class="input" checked>{% endset %}
{{ code_block(code) }}
<h2 id="invalid-state"><a href="#invalid-state">Invalid state</a></h2>
<section class="prose">
<p>Set <code>aria-invalid="true"</code> on the checkbox and, when using <code>field</code>, set <code>data-invalid="true"</code> on the field wrapper.</p>
</section>
{% set code %}<div role="group" class="field flex-row items-start gap-3" data-invalid="true">
<input type="checkbox" id="invalid-checkbox" class="input" aria-invalid="true">
<div class="grid gap-1.5">
<label for="invalid-checkbox">Accept terms and conditions</label>
<p class="field-error">You must accept the terms to continue.</p>
</div>
</div>{% endset %}
{{ code_preview("checkbox-invalid", code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-basic"><a href="#example-basic">Basic</a></h3>
{% set code %}<div role="group" class="field flex-row items-center gap-3">
<input type="checkbox" id="checkbox-basic" class="input">
<label for="checkbox-basic">Accept terms and conditions</label>
</div>{% endset %}
{{ code_preview("checkbox-basic", code) }}
<h3 id="example-description"><a href="#example-description">Description</a></h3>
{% set code %}<div role="group" class="field flex-row items-start gap-3">
<input type="checkbox" id="checkbox-description" class="input" aria-describedby="checkbox-description-text">
<div class="grid gap-1.5">
<label for="checkbox-description">Accept terms and conditions</label>
<p id="checkbox-description-text" class="field-description">By clicking this checkbox, you agree to the terms and conditions.</p>
</div>
</div>{% endset %}
{{ code_preview("checkbox-description", code) }}
<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 %}
{% set code %}<div role="group" class="field flex-row items-center gap-3" data-disabled="true">
<input type="checkbox" id="checkbox-disabled" class="input" disabled>
<label for="checkbox-disabled">Accept terms and conditions</label>
</div>{% endset %}
{{ code_preview("checkbox-disabled", code) }}
<h3 id="example-with-text"><a href="#example-with-text">With text</a></h3>
<h3 id="example-group"><a href="#example-group">Group</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>
{% set code %}<fieldset class="fieldset">
<legend>Sidebar</legend>
<p>Select the items you want to display in the sidebar.</p>
<div role="group" aria-label="Sidebar items" data-slot="checkbox-group">
<div role="group" class="field flex-row items-center gap-3">
<input class="input" type="checkbox" id="checkbox-recents" checked>
<label for="checkbox-recents">Recents</label>
</div>
<div role="group" class="field flex-row items-center gap-3">
<input class="input" type="checkbox" id="checkbox-home" checked>
<label for="checkbox-home">Home</label>
</div>
<div role="group" class="field flex-row items-center gap-3">
<input class="input" type="checkbox" id="checkbox-applications">
<label for="checkbox-applications">Applications</label>
</div>
<div role="group" class="field flex-row items-center gap-3">
<input class="input" type="checkbox" id="checkbox-desktop">
<label for="checkbox-desktop">Desktop</label>
</div>
</div>
</fieldset>{% endset %}
{{ code_preview("checkbox-group", code) }}
<h3 id="example-table"><a href="#example-table">Table</a></h3>
{% set code %}<div class="table-container">
<table class="table">
<thead>
<tr>
<th><input type="checkbox" class="input" aria-label="Select all"></th>
<th>Status</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="checkbox" class="input" aria-label="Select row 1" checked></td>
<td>Active</td>
<td>olivia@example.com</td>
</tr>
<tr>
<td><input type="checkbox" class="input" aria-label="Select row 2"></td>
<td>Inactive</td>
<td>noah@example.com</td>
</tr>
</tbody>
</table>
</div>{% endset %}
{{ code_preview("checkbox-table", code) }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Checkbox layout uses document direction and logical spacing. Set <code>dir="rtl"</code> on a parent to render right-to-left.</p>
</section>
{% set code %}<div dir="rtl" class="field flex-row items-start gap-3">
<input type="checkbox" id="checkbox-rtl" class="input" aria-describedby="checkbox-rtl-description" checked>
<div class="grid gap-1.5">
<label for="checkbox-rtl">قبول الشروط والأحكام</label>
<p id="checkbox-rtl-description" class="field-description">يمكنك تغيير هذا الخيار في أي وقت.</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) }}
{{ code_preview("checkbox-rtl", code) }}
+207 -162
View File
@@ -1,7 +1,7 @@
---
layout: layouts/page.njk
title: Combobox
description: Autocomplete input and command palette with a list of suggestions.
description: Autocomplete input with a list of suggestions.
toc:
- label: Usage
id: usage
@@ -24,26 +24,39 @@ toc:
- label: Examples
id: examples
children:
- label: Scrollable
id: example-scrollable
- label: Top side
- label: Multiple
id: example-multiple
- label: Groups
id: example-groups
- label: Custom Items
id: example-custom
- label: Invalid
id: example-invalid
- label: Disabled
id: example-disabled
- label: Auto Highlight
id: example-auto-highlight
- label: Top Side
id: example-top
- label: RTL
id: example-rtl
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "select.njk" import select %}
{% from "combobox.njk" import combobox %}
{% set options_frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"] %}
{% set 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..."
{% call combobox(
id="framework-combobox",
name="framework",
placeholder="Select a framework",
input_attrs={"class": "w-[240px]"},
listbox_attrs={"data-empty": "No framework found."}
) %}
{% for framework in options_frameworks %}
<div role="option" data-value="{{ framework }}">{{ framework }}</div>
{% for framework in frameworks %}
<div role="option" data-value="{{ framework }}">{{ framework }}</div>
{% endfor %}
{% endcall %}
{% endset %}
@@ -57,11 +70,11 @@ toc:
<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>
<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:</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 %}
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/combobox.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
@@ -69,12 +82,12 @@ toc:
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
<a class="badge-outline" href="/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
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/js/combobox.js" target="_blank">
combobox.js
{% lucide "arrow-right" %}
</a>
</div>
@@ -82,7 +95,7 @@ toc:
<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>
<p>Combobox is input-first. The visible input filters the list; the hidden input stores the submitted value.</p>
</section>
{{ code_block(code_html | prettyHtml, "html") }}
@@ -91,54 +104,20 @@ toc:
<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>
<dt><code class="highlight language-html">&lt;div class="combobox"&gt;</code></dt>
<dd>Root element. Add <code>data-multiple="true"</code> for multiple selection and <code>data-auto-highlight="true"</code> to highlight the first match while filtering.</dd>
<dt><code class="highlight language-html">&lt;input type="text" role="combobox"&gt;</code></dt>
<dd>The editable control. It should include <code>aria-expanded</code>, <code>aria-controls</code>, and <code>aria-autocomplete="list"</code>.</dd>
<dt><code class="highlight language-html">&lt;div class="combobox-chips"&gt;</code> <span class="badge-secondary">Multiple only</span></dt>
<dd>Wraps selected chips and the editable input in multiple mode.</dd>
<dt><code class="highlight language-html">&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 class="highlight language-html">&lt;div role="listbox"&gt;</code></dt>
<dd>The suggestions list. Add <code>aria-multiselectable="true"</code> in multiple mode and <code>data-empty</code> for the empty message.</dd>
<dt><code class="highlight language-html">&lt;div role="option" data-value="..."&gt;</code></dt>
<dd>Selectable option. Options can contain custom HTML; use <code>data-label</code> when the visible input should use different text than the rendered content.</dd>
<dt><code class="highlight language-html">&lt;input type="hidden"&gt;</code></dt>
<dd>Submitted value. Single mode stores a string; multiple mode stores a JSON array.</dd>
</dl>
</section>
@@ -147,147 +126,213 @@ toc:
<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>
<dd>Dispatched on the component after initialization.</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>
<dd>Dispatched on <code>document</code> when the popup opens so other popup components can close.</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>
<dd>Dispatched on selection changes with <code>event.detail.value</code>.</dd>
</dl>
</section>
<h4 id="usage-html-js-5"><a href="#usage-html-js-4">JavaScript methods</a></h4>
<h4 id="usage-html-js-5"><a href="#usage-html-js-5">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>
<dt><code>value</code></dt>
<dd>Gets or sets the selected value. Multiple mode uses an array.</dd>
<dt><code>select(value)</code></dt>
<dd>Selects an option by value.</dd>
<dt><code>deselect(value)</code> <span class="badge-secondary">Multiple only</span></dt>
<dd>Removes a selected value.</dd>
<dt><code>toggle(value)</code> <span class="badge-secondary">Multiple only</span></dt>
<dd>Toggles a selected value.</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>
<section class="prose">
<p>Use the <code>combobox()</code> macro for Nunjucks or Jinja.</p>
</section>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
<a class="badge-outline" href="/cli#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">
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/combobox.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">
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/combobox.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
{% set raw_code %}{% raw %}{% call combobox(
placeholder="Select a framework",
listbox_attrs={"data-empty": "No framework found."}
) %}
<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>
<h3 id="example-multiple"><a href="#example-multiple">Multiple</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..."
{% call combobox(
id="frameworks-combobox",
name="frameworks",
multiple=true,
selected=["Next.js"],
placeholder="Add framework",
input_attrs={"class": "w-[320px]"},
listbox_attrs={"data-empty": "No framework found."},
auto_highlight=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 }}">
<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 %}
{% for framework in frameworks %}
<div role="option" data-value="{{ framework }}">{{ framework }}</div>
{% endfor %}
{% endcall %}
{% endset %}
{{ code_preview("combobox-multiple", code | prettyHtml) }}
<h3 id="example-groups"><a href="#example-groups">Groups</a></h3>
{% set code %}
{% call combobox(
id="timezone-combobox",
placeholder="Search timezone",
input_attrs={"class": "w-[280px]"},
listbox_attrs={"data-empty": "No timezone found."}
) %}
<div role="group" aria-labelledby="timezone-americas">
<div role="heading" id="timezone-americas">Americas</div>
<div role="option" data-value="America/New_York">(GMT-5) New York</div>
<div role="option" data-value="America/Los_Angeles">(GMT-8) Los Angeles</div>
<div role="option" data-value="America/Sao_Paulo">(GMT-3) São Paulo</div>
</div>
<hr role="separator">
<div role="option">
{% lucide "circle-plus" %}
Create timezone
<div role="group" aria-labelledby="timezone-europe">
<div role="heading" id="timezone-europe">Europe</div>
<div role="option" data-value="Europe/London">(GMT+0) London</div>
<div role="option" data-value="Europe/Paris">(GMT+1) Paris</div>
<div role="option" data-value="Europe/Berlin">(GMT+1) Berlin</div>
</div>
{% endcall %}
{% endset %}
{{ code_preview("combobox-with-groups", code | prettyHtml) }}
{{ code_preview("combobox-groups", code | prettyHtml) }}
<h3 id="example-top"><a href="#example-top">Top side</a></h3>
<h3 id="example-custom"><a href="#example-custom">Custom Items</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...",
{% call combobox(
id="country-combobox",
placeholder="Search countries",
input_attrs={"class": "w-[280px]"},
listbox_attrs={"data-empty": "No country 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 (ar)</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 (jp)</span>
</span>
</div>
<div role="option" data-value="united-states" data-label="United States" data-filter="United States North America USA">
<span class="flex flex-col">
<span>United States</span>
<span class="text-muted-foreground text-xs">North America (us)</span>
</span>
</div>
{% endcall %}
{% endset %}
{{ code_preview("combobox-custom", code | prettyHtml) }}
<h3 id="example-invalid"><a href="#example-invalid">Invalid</a></h3>
{% set code %}
<div role="group" class="field" data-invalid="true">
<label for="invalid-combobox-input">Framework</label>
{% call combobox(
id="invalid-combobox",
placeholder="Select a framework",
input_attrs={"id": "invalid-combobox-input", "aria-invalid": "true", "class": "w-[240px]"},
listbox_attrs={"data-empty": "No framework found."}
) %}
{% for framework in frameworks %}
<div role="option" data-value="{{ framework }}">{{ framework }}</div>
{% endfor %}
{% endcall %}
<p class="field-error" role="alert">Select a framework to continue.</p>
</div>
{% endset %}
{{ code_preview("combobox-invalid", code | prettyHtml, "w-full max-w-sm") }}
<h3 id="example-disabled"><a href="#example-disabled">Disabled</a></h3>
{% set code %}
{% call combobox(
placeholder="Select a framework",
input_attrs={"disabled": "disabled", "class": "w-[240px]"}
) %}
<div role="option" data-value="nextjs">Next.js</div>
{% endcall %}
{% endset %}
{{ code_preview("combobox-disabled", code | prettyHtml) }}
<h3 id="example-auto-highlight"><a href="#example-auto-highlight">Auto Highlight</a></h3>
{% set code %}
{% call combobox(
placeholder="Select a framework",
input_attrs={"class": "w-[240px]"},
auto_highlight=true
) %}
{% for framework in frameworks %}
<div role="option" data-value="{{ framework }}">{{ framework }}</div>
{% endfor %}
{% endcall %}
{% endset %}
{{ code_preview("combobox-auto-highlight", code | prettyHtml) }}
<h3 id="example-top"><a href="#example-top">Top Side</a></h3>
{% set code %}
{% call combobox(
placeholder="Select a framework",
input_attrs={"class": "w-[240px]"},
popover_attrs={"data-side": "top"}
) %}
{% for framework in options_frameworks %}
<div role="option" data-value="{{ framework }}">{{ framework }}</div>
{% for framework in frameworks %}
<div role="option" data-value="{{ framework }}">{{ framework }}</div>
{% endfor %}
{% endcall %}{% endset %}
{{ code_preview("combobox", code | prettyHtml) }}
{% endcall %}
{% endset %}
{{ code_preview("combobox-top", code | prettyHtml) }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
{% set code %}
<div dir="rtl">
{% call combobox(
placeholder="اختر إطار عمل",
input_attrs={"class": "w-[240px]"}
) %}
<div role="option" data-value="nextjs">Next.js</div>
<div role="option" data-value="sveltekit">SvelteKit</div>
<div role="option" data-value="astro">Astro</div>
{% endcall %}
</div>
{% endset %}
{{ code_preview("combobox-rtl", code | prettyHtml) }}
+156 -109
View File
@@ -1,8 +1,10 @@
---
layout: layouts/page.njk
title: Command
description: Fast, composable command menu for quick navigation and actions.
description: Command menu for search and quick actions.
toc:
- label: About
id: about
- label: Usage
id: usage
children:
@@ -22,61 +24,76 @@ toc:
- label: Examples
id: examples
children:
- label: Dialog
id: example-dialog
- label: Basic
id: example-basic
- label: Shortcuts
id: example-shortcuts
- label: Groups
id: example-groups
- label: Scrollable
id: example-scrollable
- label: RTL
id: example-rtl
- label: API Reference
id: api-reference
---
{% 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") %}
{% macro command_items(suggestions_id="suggestions", settings_id="settings", shortcuts=false) %}
<div role="group" aria-labelledby="{{ suggestions_id }}">
<span role="heading" id="{{ suggestions_id }}">Suggestions</span>
<div role="menuitem" onclick="console.log('Open calendar')">
<div role="menuitem" data-filter="Calendar" data-keywords="date event schedule">
{% lucide "calendar" %}
<span>Calendar</span>
{% if shortcuts %}<span data-shortcut>⌘K</span>{% endif %}
</div>
<div role="menuitem" onclick="console.log('Search emoji')">
<div role="menuitem" data-filter="Search Emoji" data-keywords="emoji smile reaction">
{% lucide "smile" %}
<span>Search Emoji</span>
{% if shortcuts %}<span data-shortcut>⌘E</span>{% endif %}
</div>
<div role="menuitem" aria-disabled="true">
{% lucide "calculator" %}
<span>Calculator</span>
</div>
</div>
<hr role="separator" />
<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">
<div role="menuitem" data-filter="Profile" data-keywords="user account">
{% lucide "user" %}
<span>Profile</span>
<kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘P</kbd>
{% if shortcuts %}<span data-shortcut>⌘P</span>{% endif %}
</div>
<div role="menuitem" onclick="console.log('Open billing')" data-filter="Billing">
<div role="menuitem" data-filter="Billing" data-keywords="invoice payment">
{% lucide "credit-card" %}
<span>Billing</span>
<kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘B</kbd>
{% if shortcuts %}<span data-shortcut>⌘B</span>{% endif %}
</div>
<div role="menuitem" onclick="console.log('Open settings')" data-filter="Settings">
<div role="menuitem" data-filter="Settings" data-keywords="config preferences">
{% lucide "settings" %}
<span>Settings</span>
<kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘S</kbd>
{% if shortcuts %}<span data-shortcut>⌘S</span>{% endif %}
</div>
</div>
{% endmacro %}
{% set code_standalone %}
{% call command(
id="demo-command-standalone",
main_attrs={ "class": "rounded-lg border shadow-md" }
) %}
{% call command(id="demo-command-standalone") %}
{{ command_items("suggestions", "settings") }}
{% endcall %}
{% endset %}
{{ code_preview("command-standalone", code_standalone | prettyHtml, class=" w-full max-w-[450px]") }}
{{ code_preview("command-standalone", code_standalone | prettyHtml, class="w-full max-w-sm") }}
<h2 id="about"><a href="#about">About</a></h2>
<section class="prose">
<p>shadcn/ui's Command component is built on <code>cmdk</code>. Basecoat implements the same command-menu pattern with vanilla HTML, CSS, and a small JavaScript controller for filtering, active item navigation, and dialog closing.</p>
</section>
<h2 id="usage"><a href="#usage">Usage</a></h2>
@@ -97,7 +114,7 @@ toc:
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
<a class="badge-outline" href="/cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
@@ -109,68 +126,53 @@ toc:
<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>
<p>Use <code>&lt;dialog class="command-dialog"&gt;</code> for the command palette variant.</p>
</section>
{% set code_dialog %}
<dialog class="command-dialog" aria-label="Command menu">
{% set code_dialog_usage %}<button class="btn-outline" onclick="document.getElementById('command-palette').showModal()">Open</button>
<dialog id="command-palette" class="command-dialog" aria-label="Command menu">
<div class="command">
<!-- Regular command content: header, menu, etc. -->
<!-- Command content -->
</div>
</dialog>
{% endset %}
{{ code_block(code_dialog | prettyHtml, "html") }}
</dialog>{% endset %}
{{ code_block(code_dialog_usage | prettyHtml, "html") }}
<h4 id="usage-html-js-3"><a href="#usage-html-js-3">HTML structure</a></h4>
<section class="prose">
<p>The command menu can be used standalone or inside a native dialog. The structure is:</p>
<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.
<dd>Dialog variant. Wraps one <code>&lt;div class="command"&gt;</code>. Add <code>aria-label</code> or <code>aria-labelledby</code> for an accessible name.
<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.
<dd>Root command menu. Can also be used standalone without the dialog wrapper.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dd>Contains the search input to filter command options.
<dd>Search input wrapper. The macro includes a search icon and input.
<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>
<dt><code class="highlight language-html">&lt;input type="text" role="combobox"&gt;</code></dt>
<dd>Filter input. Use <code>aria-expanded="true"</code> and <code>aria-controls="{ MENU_ID }"</code> to point to the command list.</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.
<dt><code class="highlight language-html">&lt;div role="menu" id="{ MENU_ID }"&gt;</code></dt>
<dd>Command list. Set <code>data-empty</code> to customize the empty state.
<dl>
<dt><code class="highlight language-html">&lt;div role="group" aria-labelledby="{ HEADING_ID }"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Groups related command items. Hidden automatically when every item in the group is filtered out.</dd>
<dt><code class="highlight language-html">&lt;span role="heading" id="{ HEADING_ID }"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Group heading. Use an <code>id</code> when the parent group uses <code>aria-labelledby</code>.</dd>
<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>
<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>, and <code>aria-disabled="true"</code>.</dd>
<dt><code class="highlight language-html">&lt;span data-shortcut&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Shortcut hint aligned to the inline end of the item.</dd>
<dt><code class="highlight language-html">&lt;span data-indicator&gt;</code> <span class="badge-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 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>
<dd>Separator between groups/items. Hidden while filtering.</dd>
</dl>
</dd>
</dl>
@@ -185,7 +187,7 @@ toc:
<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>
<dd>Once the component is initialized, it dispatches a custom non-bubbling <code>basecoat:initialized</code> event on itself.</dd>
</dl>
</section>
@@ -196,7 +198,7 @@ toc:
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
<a class="badge-outline" href="/cli#macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
@@ -211,7 +213,7 @@ toc:
</div>
{% set raw_code %}{% raw %}{% call command() %}
<div role="menuitem">
<div role="menuitem" data-keywords="date event">
<svg>...</svg>
<span>Calendar</span>
</div>
@@ -222,67 +224,112 @@ toc:
{% 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>
<h3 id="example-basic"><a href="#example-basic">Basic</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>
<p>A simple command menu in a dialog. The button also shows the keyboard shortcut used in this example.</p>
</section>
{% set code_dialog %}
<button
type="button"
class="btn-outline"
onclick="document.getElementById('demo-command-dialog').showModal()"
>
{% set code_basic %}
<button type="button" class="btn-outline" onclick="document.getElementById('command-basic').showModal()">
Open command menu
<kbd class="kbd">⌘J</kbd>
</button>
{% call command_dialog(id="demo-command-dialog") %}
{{ command_items("cmd-suggestions", "cmd-settings") }}
{% call command_dialog(id="command-basic") %}
{{ command_items("command-basic-suggestions", "command-basic-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();
}
document.addEventListener('keydown', (event) => {
if ((event.metaKey || event.ctrlKey) && event.key === 'j') {
event.preventDefault();
const dialog = document.getElementById('command-basic');
dialog.open ? dialog.close() : dialog.showModal();
dialog.querySelector('header input')?.focus();
}
});
</script>
{% endset %}
{{ code_preview("command-basic", code_basic | prettyHtml) }}
{{ code_preview("command-dialog", code_dialog | prettyHtml) }}
<h3 id="example-shortcuts"><a href="#example-shortcuts">Shortcuts</a></h3>
{% set code_shortcuts %}
{% call command(id="command-shortcuts") %}
{{ command_items("command-shortcuts-suggestions", "command-shortcuts-settings", true) }}
{% endcall %}
{% endset %}
{{ code_preview("command-shortcuts", code_shortcuts | prettyHtml, class="w-full max-w-sm") }}
<h3 id="example-groups"><a href="#example-groups">Groups</a></h3>
{% set code_groups %}
{% call command(id="command-groups") %}
{{ command_items("command-groups-suggestions", "command-groups-settings") }}
{% endcall %}
{% endset %}
{{ code_preview("command-groups", code_groups | prettyHtml, class="w-full max-w-sm") }}
<h3 id="example-scrollable"><a href="#example-scrollable">Scrollable</a></h3>
{% set code_scrollable %}
{% call command(id="command-scrollable") %}
<div role="group" aria-labelledby="command-scrollable-heading">
<span role="heading" id="command-scrollable-heading">Pages</span>
{% for i in range(0, 18) %}
<div role="menuitem" data-filter="Page {{ i + 1 }}">
{% lucide "file" %}
<span>Page {{ i + 1 }}</span>
</div>
{% endfor %}
</div>
{% endcall %}
{% endset %}
{{ code_preview("command-scrollable", code_scrollable | prettyHtml, class="w-full max-w-sm") }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Command supports document direction. Set <code>dir="rtl"</code> on the command root or a parent element.</p>
</section>
{% set code_rtl %}
<div dir="rtl">
{% call command(id="command-rtl", placeholder="اكتب أمراً أو ابحث...") %}
<div role="group" aria-labelledby="command-rtl-suggestions">
<span role="heading" id="command-rtl-suggestions">اقتراحات</span>
<div role="menuitem" data-filter="Calendar">
{% lucide "calendar" %}
<span>التقويم</span>
<span data-shortcut>⌘K</span>
</div>
<div role="menuitem" data-filter="Settings">
{% lucide "settings" %}
<span>الإعدادات</span>
<span data-shortcut>⌘S</span>
</div>
</div>
{% endcall %}
</div>
{% endset %}
{{ code_preview("command-rtl", code_rtl | prettyHtml, class="w-full max-w-sm") }}
<h2 id="api-reference"><a href="#api-reference">API Reference</a></h2>
<section class="prose">
<dl>
<dt><code class="highlight language-html">.command</code></dt>
<dd>Root command menu.</dd>
<dt><code class="highlight language-html">.command-dialog</code></dt>
<dd>Native dialog variant. Closes automatically when an item is clicked unless that item has <code>data-keep-command-open</code>.</dd>
<dt><code class="highlight language-html">data-filter</code></dt>
<dd>Overrides item text used by the filter.</dd>
<dt><code class="highlight language-html">data-keywords</code></dt>
<dd>Additional whitespace- or comma-separated search terms.</dd>
<dt><code class="highlight language-html">data-force</code></dt>
<dd>Always keeps an item visible while filtering.</dd>
<dt><code class="highlight language-html">data-empty</code></dt>
<dd>Custom empty-state text on the menu element.</dd>
</dl>
</section>
+168 -58
View File
@@ -8,13 +8,25 @@ toc:
children:
- label: HTML
id: usage-html
- label: HTML structure
id: usage-html-structure
- label: Jinja and Nunjucks
id: usage-macro
- label: Examples
id: examples
children:
- label: Scrollable content
- label: Custom Close Button
id: example-custom-close-button
- label: No Close Button
id: example-no-close-button
- label: Sticky Footer
id: example-sticky-footer
- label: Scrollable Content
id: example-scrollable-content
- label: RTL
id: example-rtl
- label: API Reference
id: api-reference
---
{% from "macros/code_block.njk" import code_block %}
@@ -32,17 +44,16 @@ toc:
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">
<form class="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/>
<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 for="demo-dialog-edit-profile-username">Username</label>
<input type="text" value="@peduarte" id="demo-dialog-edit-profile-username" />
<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>
{% endcall %}
@@ -52,41 +63,42 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Basecoat uses the native <code>&lt;dialog&gt;</code> element and <code>showModal()</code>. This differs from shadcn/ui's portalled Base UI implementation, but preserves native modality, focus handling, and inert page content without component JavaScript.</p>
</section>
<h3 id="usage-html"><a href="#usage-html">HTML</a></h3>
{{ code_block(code_html | prettyHtml, class="") }}
<h3 id="usage-html-structure"><a href="#usage-html-structure">HTML structure</a></h3>
<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;button type="button" onclick="dialog.showModal()"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Trigger button. Basecoat intentionally uses the native <code>HTMLDialogElement.showModal()</code> method.</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>
<dd>Native modal dialog. Add <code>aria-labelledby="{ TITLE_ID }"</code> and <code>aria-describedby="{ DESCRIPTION_ID }"</code> when title and description are present. The macro also adds backdrop-click close handling.
<dl>
<dt><code class="highlight language-html">&lt;div&gt;</code></dt>
<dd>Wraps the content of the dialog.
<dd>Dialog content surface.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dd>Contains the header for the dialog:
<dt><code class="highlight language-html">&lt;header&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Dialog header.
<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>
<dt><code class="highlight language-html">&lt;h2 id="{ TITLE_ID }"&gt;</code></dt>
<dd>Dialog title. Reference it from <code>aria-labelledby</code>.</dd>
<dt><code class="highlight language-html">&lt;p id="{ DESCRIPTION_ID }"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Dialog description. Reference it from <code>aria-describedby</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>
<dd>Dialog body/content area. Add overflow utilities when the body should scroll.</dd>
<dt><code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Usually contains actions.</dd>
<dd>Action area. It stacks actions on small screens and aligns them to the end on larger screens.</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>
<dd>Close button. You can also wrap a button in <code>&lt;form method="dialog"&gt;</code>.</dd>
</dl>
</dd>
</dl>
@@ -101,7 +113,7 @@ toc:
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
<a class="badge-outline" href="/cli#macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
@@ -115,63 +127,161 @@ toc:
</a>
</div>
{% set raw_code %}{% raw %}{% set code_html %}
{% set footer %}
{% set raw_code %}{% raw %}{% 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",
id="dialog-edit-profile",
title="Edit profile",
description="Make changes to your profile here. Click save when you're done.",
description="Make changes to your profile here.",
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%}
<form class="grid gap-4">...</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>
<h3 id="example-custom-close-button"><a href="#example-custom-close-button">Custom Close Button</a></h3>
{% set code %}
{% set footer %}
<button class="btn-outline" onclick="this.closest('dialog').close()">Close</button>
<button class="btn" onclick="this.closest('dialog').close()">Continue</button>
{% endset %}
{% call dialog(
id="dialog-custom-close",
title="Custom close button",
description="The default close icon is hidden and the footer provides the close action.",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
close_button=false,
footer=footer
) %}
<p class="text-sm text-muted-foreground">Use <code>close_button=false</code> and add a close control where it fits your layout.</p>
{% endcall %}
{% endset %}
{{ code_preview("dialog-custom-close", code | prettyHtml) }}
<h3 id="example-no-close-button"><a href="#example-no-close-button">No Close Button</a></h3>
{% set code %}
{% set footer %}
<button class="btn" onclick="this.closest('dialog').close()">I understand</button>
{% endset %}
{% call dialog(
id="dialog-no-close",
title="No close button",
description="The dialog can still be closed with Escape or a custom action.",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
close_button=false,
footer=footer
) %}
<p class="text-sm text-muted-foreground">Native dialogs remain keyboard-accessible even without the default close icon.</p>
{% endcall %}
{% endset %}
{{ code_preview("dialog-no-close", code | prettyHtml) }}
<h3 id="example-sticky-footer"><a href="#example-sticky-footer">Sticky Footer</a></h3>
{% set code %}
{% set footer %}
<button class="btn-outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Save</button>
{% endset %}
{% call dialog(
id="dialog-sticky-footer",
title="Sticky footer",
description="Keep actions visible while the body scrolls.",
trigger="Sticky Footer",
trigger_attrs={"class": "btn-outline"},
body_attrs={"class": "overflow-y-auto scrollbar"},
footer_attrs={"class": "sticky bottom-0"},
footer=footer
) %}
<div class="space-y-4 text-sm text-muted-foreground">
{% for i in range(0, 8) %}
<p>Dialog content row {{ i + 1 }}. Add enough content to make the body scroll while the footer remains available.</p>
{% endfor %}
</div>
{% endcall %}
{% endset %}
{{ code_preview("dialog-sticky-footer", code | prettyHtml) }}
<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",
id="dialog-scrollable",
title="Scrollable Content",
description="This is a dialog with scrollable content.",
description="Long content can scroll inside the dialog body.",
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 class="space-y-4 text-sm text-muted-foreground">
{% for i in range(0, 10) %}
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer non nibh sit amet augue commodo ultrices. Row {{ i + 1 }}.</p>
{% endfor %}
</div>
{% endcall %}
{% endset %}
{{ code_preview("dialog-scrollable-content", code | prettyHtml) }}
{{ code_preview("dialog-scrollable-content", code | prettyHtml) }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Dialog positioning and close button placement use logical properties. Set <code>dir="rtl"</code> on the dialog or a parent element.</p>
</section>
{% set code %}
<div dir="rtl">
{% set footer %}
<button class="btn-outline" onclick="this.closest('dialog').close()">إلغاء</button>
<button class="btn" onclick="this.closest('dialog').close()">حفظ</button>
{% endset %}
{% call dialog(
id="dialog-rtl",
title="تعديل الملف الشخصي",
description="قم بتحديث بياناتك ثم احفظ التغييرات.",
trigger="فتح",
trigger_attrs={"class": "btn-outline"},
footer=footer
) %}
<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" value="ليلى">
</div>
</form>
{% endcall %}
</div>
{% endset %}
{{ code_preview("dialog-rtl", code | prettyHtml) }}
<h2 id="api-reference"><a href="#api-reference">API Reference</a></h2>
<section class="prose">
<dl>
<dt><code class="highlight language-html">.dialog</code></dt>
<dd>Native dialog root. It owns the backdrop through <code>::backdrop</code> and the open state through the native <code>open</code> attribute.</dd>
<dt><code class="highlight language-html">.dialog &gt; div</code></dt>
<dd>Dialog content surface. This maps to shadcn/ui's <code>DialogContent</code>.</dd>
<dt><code class="highlight language-html">.dialog &gt; div &gt; header</code></dt>
<dd>Header area. Direct <code>h2</code> and <code>p</code> children are styled as title and description.</dd>
<dt><code class="highlight language-html">.dialog &gt; div &gt; section</code></dt>
<dd>Body/content area. Add utilities such as <code>overflow-y-auto</code> when you need scrolling.</dd>
<dt><code class="highlight language-html">.dialog &gt; div &gt; footer</code></dt>
<dd>Action area. Use Basecoat button classes for actions.</dd>
<dt><code class="highlight language-html">close_button=false</code></dt>
<dd>Macro option to hide the generated close icon.</dd>
</dl>
</section>
+232 -221
View File
@@ -9,7 +9,7 @@ toc:
- label: HTML + JavaScript
id: usage-html-js
children:
- label: "Step 1: Include the JavaScript files"
- label: "Step 1: Include the JavaScript file"
id: usage-html-js-1
- label: "Step 2: Add your dropdown menu HTML"
id: usage-html-js-2
@@ -22,10 +22,22 @@ toc:
- label: Examples
id: examples
children:
- label: Basic
id: example-basic
- label: Shortcuts
id: example-shortcuts
- label: Icons
id: example-icons
- label: Checkboxes
id: example-checkboxes
- label: Radio Group
id: example-radio-group
- label: Destructive
id: example-destructive
- label: RTL
id: example-rtl
- label: API Reference
id: api-reference
---
{% from "macros/code_block.njk" import code_block %}
@@ -39,50 +51,39 @@ toc:
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="group" aria-labelledby="demo-dropdown-account">
<div role="heading" id="demo-dropdown-account">My Account</div>
<div role="menuitem">
Profile
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
<span data-shortcut>⇧⌘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>
<span data-shortcut>⌘B</span>
</div>
<div role="menuitem">Settings</div>
<div role="menuitem">Keyboard shortcuts</div>
</div>
<hr role="separator">
<div role="menuitem">
GitHub
</div>
<div role="menuitem">
Support
</div>
<div role="menuitem" aria-disabled="true">
API
</div>
<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>
<div role="menuitem">Log out</div>
{% endcall %}
{% endset %}
{{ code_preview("dropdown-menu", code_html | prettyHtml) }}
<h2 id="usage">Usage</h2>
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Basecoat dropdown menus are inline-positioned relative to the <code>.dropdown-menu</code> 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.</p>
</section>
<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>
<h4 id="usage-html-js-1"><a href="#usage-html-js-1">Step 1: Include the JavaScript file</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>
@@ -97,7 +98,7 @@ toc:
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
<a class="badge-outline" href="/cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
@@ -116,43 +117,34 @@ toc:
<section class="prose">
<dl>
<dt><code class="highlight language-html">&lt;div class="dropdown-menu"&gt;</code></dt>
<dd>Wraps around the entire component.
<dd>Relative wrapper for the trigger and inline menu content.
<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>
<dt><code class="highlight language-html">&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 class="highlight language-html">&lt;div data-popover aria-hidden="true"&gt;</code></dt>
<dd>Menu content popover. Set <code>data-side="top|right|bottom|left"</code> and <code>data-align="start|center|end"</code> to control placement.
<dl>
<dt><code class="highlight language-html">&lt;div role="menu"&gt;</code></dt>
<dd>Container for menu items, groups, labels, and separators.
<dl>
<dt><code class="highlight language-html">&lt;div role="group" aria-labelledby="{ HEADING_ID }"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Groups related menu items.</dd>
<dt><code class="highlight language-html">&lt;div role="heading" id="{ HEADING_ID }"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Group heading/label.</dd>
<dt><code class="highlight language-html">&lt;div role="menuitem"&gt;</code></dt>
<dd>Standard action item. Use <code>aria-disabled="true"</code> for disabled items.</dd>
<dt><code class="highlight language-html">&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 class="highlight language-html">&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 class="highlight language-html">&lt;span data-shortcut&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Shortcut hint aligned to the inline end of the item.</dd>
<dt><code class="highlight language-html">&lt;hr role="separator"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Separator between groups or options.</dd>
</dl>
</dd>
</dl>
</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>
@@ -163,9 +155,9 @@ toc:
<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>
<dd>Once the component is 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>
<dd>When the menu opens, the component dispatches a custom event on <code>document</code>. Other popover-based components listen for this to close any open popovers.</dd>
</dl>
</section>
@@ -176,7 +168,7 @@ toc:
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
<a class="badge-outline" href="/cli#macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
@@ -198,128 +190,111 @@ toc:
) %}
<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 role="menuitem">Profile</div>
<div role="menuitem">Billing</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%}
<div role="menuitem">Team</div>
<div role="menuitem">Subscription</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>
<h3 id="example-basic"><a href="#example-basic">Basic</a></h3>
{% set code %}{% call dropdown_menu(
id="dropdown-menu-checkboxes",
{% set code %}
{% call dropdown_menu(
id="dropdown-basic",
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 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="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"
} %}
<div role="menuitem">Team</div>
<div role="menuitem" aria-disabled="true">API</div>
{% endcall %}
{% endset %}
{{ code_preview("dropdown-menu-basic", code | prettyHtml) }}
<h3 id="example-shortcuts"><a href="#example-shortcuts">Shortcuts</a></h3>
{% set code %}
{% call dropdown_menu(
id="dropdown-shortcuts",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56"}
) %}
<div role="menuitem">New Tab <span data-shortcut>⌘T</span></div>
<div role="menuitem">New Window <span data-shortcut>⌘N</span></div>
<div role="menuitem">New Private Window <span data-shortcut>⇧⌘N</span></div>
<hr role="separator">
<div role="menuitem">Print <span data-shortcut>⌘P</span></div>
{% endcall %}
{% endset %}
{{ code_preview("dropdown-menu-shortcuts", code | prettyHtml) }}
<h3 id="example-icons"><a href="#example-icons">Icons</a></h3>
{% set code %}
{% call dropdown_menu(
id="dropdown-icons",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56"}
) %}
<div role="menuitem">{% lucide "user" %} Profile</div>
<div role="menuitem">{% lucide "credit-card" %} Billing</div>
<div role="menuitem">{% lucide "settings" %} Settings</div>
<hr role="separator">
<div role="menuitem">{% lucide "log-out" %} Log out</div>
{% endcall %}
{% endset %}
{{ code_preview("dropdown-menu-icons", code | prettyHtml) }}
<h3 id="example-checkboxes"><a href="#example-checkboxes">Checkboxes</a></h3>
{% set code %}
{% call dropdown_menu(
id="dropdown-checkboxes",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56"}
) %}
<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>{% lucide "check" %}</span>
Status Bar
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
<span data-shortcut>⌘S</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"
} %}
<div role="menuitemcheckbox" aria-checked="false">
<span data-indicator>{% lucide "check" %}</span>
Activity Bar
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
<span data-shortcut>⌘A</span>
</div>
<div
role="menuitemcheckbox"
aria-checked="false"
class="group"
>
{% lucide "check", {
"class": "invisible group-aria-checked:visible",
"aria-hidden": "true",
"focusable": "false"
} %}
<div role="menuitemcheckbox" aria-checked="false" aria-disabled="true">
<span data-indicator>{% lucide "check" %}</span>
Panel
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
<span data-shortcut>⌘P</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 => {
const dropdownMenu = document.querySelector('#dropdown-checkboxes');
const checkboxes = dropdownMenu.querySelectorAll('[role="menuitemcheckbox"]');
checkboxes.forEach((checkbox) => {
checkbox.addEventListener('click', () => {
const isChecked = checkbox.getAttribute('aria-checked') === 'true';
checkbox.setAttribute('aria-checked', !isChecked);
if (checkbox.getAttribute('aria-disabled') === 'true') return;
checkbox.setAttribute('aria-checked', checkbox.getAttribute('aria-checked') !== 'true');
});
});
})();
@@ -329,75 +304,111 @@ toc:
<h3 id="example-radio-group"><a href="#example-radio-group">Radio Group</a></h3>
{% set code %}{% call dropdown_menu(
id="dropdown-menu-radio-group",
{% set code %}
{% call dropdown_menu(
id="dropdown-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 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>{% lucide "check" %}</span>
Top
</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 role="menuitemradio" aria-checked="true">
<span data-indicator>{% lucide "check" %}</span>
Bottom
</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 role="menuitemradio" aria-checked="false">
<span data-indicator>{% lucide "check" %}</span>
Right
</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');
const dropdownMenu = document.querySelector('#dropdown-radio-group');
const radios = dropdownMenu.querySelectorAll('[role="menuitemradio"]');
radios.forEach((radio) => {
radio.addEventListener('click', () => {
radios.forEach((item) => item.setAttribute('aria-checked', 'false'));
radio.setAttribute('aria-checked', 'true');
});
});
})();
</script>
{% endset %}
{{ code_preview("dropdown-menu", code | prettyHtml) }}
{{ code_preview("dropdown-menu-radio-group", code | prettyHtml) }}
<h3 id="example-destructive"><a href="#example-destructive">Destructive</a></h3>
{% set code %}
{% call dropdown_menu(
id="dropdown-destructive",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56"}
) %}
<div role="menuitem">Edit</div>
<div role="menuitem">Duplicate</div>
<hr role="separator">
<div role="menuitem" data-variant="destructive">
{% lucide "trash-2" %}
Delete
</div>
{% endcall %}
{% endset %}
{{ code_preview("dropdown-menu-destructive", code | prettyHtml) }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Dropdown menus support document direction. Set <code>dir="rtl"</code> on the dropdown root or a parent element. Use logical alignment data attributes where possible.</p>
</section>
{% set code %}
<div dir="rtl">
{% call dropdown_menu(
id="dropdown-rtl",
trigger="فتح",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56", "data-align": "end"}
) %}
<div role="heading" id="dropdown-rtl-label">الحساب</div>
<div role="menuitem">الملف الشخصي <span data-shortcut>⌘P</span></div>
<div role="menuitem">الإعدادات <span data-shortcut>⌘S</span></div>
<hr role="separator">
<div role="menuitem" data-variant="destructive">حذف</div>
{% endcall %}
</div>
{% endset %}
{{ code_preview("dropdown-menu-rtl", code | prettyHtml) }}
<h2 id="api-reference"><a href="#api-reference">API Reference</a></h2>
<section class="prose">
<dl>
<dt><code class="highlight language-html">.dropdown-menu</code></dt>
<dd>Root component class. Must wrap the trigger and the <code>[data-popover]</code> content.</dd>
<dt><code class="highlight language-html">[data-popover]</code></dt>
<dd>Inline menu popover. Supports <code>data-side</code> and <code>data-align</code> through the shared popover positioning rules.</dd>
<dt><code class="highlight language-html">[role="menu"]</code></dt>
<dd>Menu container referenced by the trigger's <code>aria-controls</code>.</dd>
<dt><code class="highlight language-html">[role="menuitem"]</code></dt>
<dd>Action item. Supports <code>aria-disabled="true"</code>, <code>data-inset</code>, and <code>data-variant="destructive"</code>.</dd>
<dt><code class="highlight language-html">[role="menuitemcheckbox"]</code></dt>
<dd>Checkbox item. Use <code>aria-checked="true|false"</code> and an optional <code>[data-indicator]</code> child.</dd>
<dt><code class="highlight language-html">[role="menuitemradio"]</code></dt>
<dd>Radio item. Use <code>aria-checked="true|false"</code> and an optional <code>[data-indicator]</code> child.</dd>
<dt><code class="highlight language-html">[role="heading"]</code></dt>
<dd>Group label. Supports <code>data-inset</code>.</dd>
<dt><code class="highlight language-html">[role="separator"]</code></dt>
<dd>Visual separator between groups or actions.</dd>
<dt><code class="highlight language-html">[data-shortcut]</code></dt>
<dd>Shortcut hint aligned to the inline end of a menu item.</dd>
<dt><code class="highlight language-html">[data-indicator]</code></dt>
<dd>Indicator slot for checkbox and radio menu items. It becomes visible when the item has <code>aria-checked="true"</code>.</dd>
</dl>
</section>
+115 -79
View File
@@ -5,6 +5,11 @@ description: Display empty states with icons, titles, descriptions, and actions.
toc:
- label: Usage
id: usage
children:
- label: HTML
id: usage-html
- label: HTML structure
id: usage-html-structure
- label: Examples
id: examples
children:
@@ -16,38 +21,33 @@ toc:
id: example-avatar-group
- label: Input Group
id: example-input-group
- label: RTL
id: example-rtl
---
{% 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 Empty component in Basecoat.</h2>
</div>
{% set code_default %}
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12 text-neutral-800 dark:text-neutral-300">
<header class="flex max-w-sm flex-col items-center gap-2 text-center">
<div class="mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6">
<section class="empty">
<header>
<figure>
{% lucide "folder-code" %}
</div>
<h3 class="text-lg font-medium tracking-tight">No Projects Yet</h3>
<p class="text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4">
You haven't created any projects yet. Get started by creating your first project.
</p>
</figure>
<h3>No Projects Yet</h3>
<p>You haven't created any projects yet. Get started by creating your first project.</p>
</header>
<section class="flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance">
<footer>
<div class="flex gap-2">
<button class="btn">Create Project</button>
<button class="btn-outline">Import Project</button>
</div>
</section>
<a href="#" class="inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive underline-offset-4 hover:underline h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5 text-muted-foreground">
Learn More
{% lucide "arrow-up-right" %}
</a>
</div>
<a href="#" class="btn-link text-muted-foreground">
Learn More
{% lucide "arrow-up-right" %}
</a>
</footer>
</section>
{% endset %}
{{ code_preview("empty-default", code_default) }}
@@ -55,8 +55,37 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Empty states are pure HTML composition using Tailwind utility classes. Use flex layout utilities to center content, and compose with semantic elements like <code>&lt;header&gt;</code>, <code>&lt;h3&gt;</code>, <code>&lt;p&gt;</code>, and <code>&lt;button&gt;</code>.</p>
<p>Browse the examples below and copy the pattern that fits your needs.</p>
<p>Use <code>.empty</code> on a semantic container. The component styles common children like <code>&lt;header&gt;</code>, <code>&lt;figure&gt;</code>, headings, descriptions, and <code>&lt;footer&gt;</code>.</p>
</section>
<h3 id="usage-html"><a href="#usage-html">HTML</a></h3>
{{ code_block(code_default) }}
<h3 id="usage-html-structure"><a href="#usage-html-structure">HTML structure</a></h3>
<section class="prose">
<p>The component has the following HTML structure:</p>
<dl>
<dt><code class="highlight language-html">&lt;section class="empty"&gt;</code></dt>
<dd>The empty-state container.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dd>Groups the media, title, and description.
<dl>
<dt><code class="highlight language-html">&lt;figure&gt;</code> <span class="badge-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 class="highlight language-html">&lt;h2&gt;</code>, <code class="highlight language-html">&lt;h3&gt;</code>, or <code class="highlight language-html">&lt;h4&gt;</code></dt>
<dd>The empty-state title.</dd>
<dt><code class="highlight language-html">&lt;p&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The empty-state description.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code> or <code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Actions and supporting content.</dd>
</dl>
</dd>
</dl>
</section>
<h2 id="examples"><a href="#examples">Examples</a></h2>
@@ -64,20 +93,18 @@ toc:
<h3 id="example-outline"><a href="#example-outline">Outline</a></h3>
{% set code_outline %}
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12 text-neutral-800 dark:text-neutral-300 border">
<header class="flex max-w-sm flex-col items-center gap-3 text-center">
<div class="mb-2 bg-muted text-foreground flex size-12 shrink-0 items-center justify-center rounded-lg [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-6">
<section class="empty border">
<header>
<figure>
{% lucide "cloud" %}
</div>
<h3 class="text-lg font-semibold tracking-tight">Cloud Storage Empty</h3>
<p class="text-muted-foreground text-sm/relaxed">
Upload files to your cloud storage to access them anywhere.
</p>
</figure>
<h3>Cloud Storage Empty</h3>
<p>Upload files to your cloud storage to access them anywhere.</p>
</header>
<section class="flex w-full max-w-sm min-w-0 flex-col items-center gap-3">
<footer>
<button class="btn-sm-outline">Upload Files</button>
</section>
</div>
</footer>
</section>
{% endset %}
{{ code_preview("empty-outline", code_outline) }}
@@ -85,20 +112,18 @@ toc:
<h3 id="example-avatar"><a href="#example-avatar">Avatar</a></h3>
{% set code_avatar %}
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12 text-neutral-800 dark:text-neutral-300">
<header class="flex max-w-sm flex-col items-center gap-3 text-center">
<div class="mb-2 flex items-center justify-center">
<img alt="@shadcn" src="https://github.com/shadcn.png" class="grayscale size-12 shrink-0 object-cover rounded-full">
</div>
<h3 class="text-lg font-semibold tracking-tight">User Offline</h3>
<p class="text-muted-foreground text-sm/relaxed">
This user is currently offline. You can leave a message to notify them or try again later.
</p>
<section class="empty">
<header>
<figure class="size-12 rounded-full">
<img alt="@shadcn" src="https://github.com/shadcn.png">
</figure>
<h3>User Offline</h3>
<p>This user is currently offline. You can leave a message or try again later.</p>
</header>
<section class="flex w-full max-w-sm min-w-0 flex-col items-center gap-3">
<footer>
<button class="btn-sm">Leave Message</button>
</section>
</div>
</footer>
</section>
{% endset %}
{{ code_preview("empty-avatar", code_avatar) }}
@@ -106,27 +131,23 @@ toc:
<h3 id="example-avatar-group"><a href="#example-avatar-group">Avatar Group</a></h3>
{% set code_avatar_group %}
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12 text-neutral-800 dark:text-neutral-300">
<header class="flex max-w-sm flex-col items-center gap-3 text-center">
<div class="mb-2 flex items-center justify-center">
<div class="flex -space-x-2 [&amp;_img]:ring-background [&amp;_img]:ring-2 [&amp;_img]:grayscale [&amp;_img]:size-12 [&amp;_img]:shrink-0 [&amp;_img]:object-cover [&amp;_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>
<h3 class="text-lg font-semibold tracking-tight">No Team Members</h3>
<p class="text-muted-foreground text-sm/relaxed">
Invite your team to collaborate on this project.
</p>
<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="@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">
</figure>
<h3>No Team Members</h3>
<p>Invite your team to collaborate on this project.</p>
</header>
<section class="flex w-full max-w-sm min-w-0 flex-col items-center gap-3">
<button class="btn-sm inline-flex items-center gap-2">
<footer>
<button class="btn-sm">
{% lucide "plus" %}
Invite Members
</button>
</section>
</div>
</footer>
</section>
{% endset %}
{{ code_preview("empty-avatar-group", code_avatar_group) }}
@@ -134,24 +155,39 @@ toc:
<h3 id="example-input-group"><a href="#example-input-group">Input Group</a></h3>
{% set code_input_group %}
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg p-6 text-center text-balance md:p-12 text-neutral-800 dark:text-neutral-300">
<header class="flex max-w-sm flex-col items-center gap-3 text-center">
<h3 class="text-lg font-semibold tracking-tight">404 - Not Found</h3>
<p class="text-muted-foreground text-sm/relaxed">
The page you're looking for doesn't exist. Try searching for what you need below.
</p>
<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>
<section class="flex w-full max-w-md min-w-0 flex-col items-center gap-3">
<div class="relative w-3/4">
<input type="text" class="input pl-8.5 pr-10" placeholder="Try searching for pages...">
{% lucide "search" , { "class": "absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground pointer-events-none shrink-0 size-4" } %}
<kbd class="kbd absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">/</kbd>
<footer>
<div class="input-group w-full">
{% lucide "search" %}
<input type="search" placeholder="Try searching for pages...">
<kbd class="kbd">/</kbd>
</div>
<p class="text-muted-foreground text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary">
Need help? <a href="#">Contact support</a>
</p>
</section>
</div>
<p>Need help? <a href="#">Contact support</a></p>
</footer>
</section>
{% endset %}
{{ code_preview("empty-input-group", code_input_group) }}
{{ code_preview("empty-input-group", code_input_group) }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
{% set code_rtl %}
<section class="empty" dir="rtl">
<header>
<figure>
{% lucide "folder-code" %}
</figure>
<h3>لا توجد مشاريع بعد</h3>
<p>ابدأ بإنشاء مشروعك الأول.</p>
</header>
<footer>
<button class="btn-sm">إنشاء مشروع</button>
</footer>
</section>
{% endset %}
{{ code_preview("empty-rtl", code_rtl) }}
+68 -9
View File
@@ -14,6 +14,8 @@ toc:
id: example-textarea
- label: Select
id: example-select
- label: Slider
id: example-slider
- label: Fieldset
id: example-fieldset
- label: Checkbox
@@ -22,8 +24,14 @@ toc:
id: example-radio
- label: Switch
id: example-switch
- label: Choice Card
id: example-choice-card
- label: Field Group
id: example-field-group
- label: Responsive Layout
id: example-responsive
- label: RTL
id: example-rtl
---
{% from "macros/code_preview.njk" import code_preview %}
@@ -292,25 +300,25 @@ toc:
<p>Select the items you want to show on the desktop.</p>
<div class="flex flex-col gap-3">
<div class="field">
<div role="group" class="field">
<label class="gap-3">
<input type="checkbox">
Hard disks
</label>
</div>
<div class="field">
<div role="group" class="field">
<label class="gap-3">
<input type="checkbox">
External disks
</label>
</div>
<div class="field">
<div role="group" class="field">
<label class="gap-3">
<input type="checkbox">
CDs, DVDs and iPods
</label>
</div>
<div class="field">
<div role="group" class="field">
<label class="gap-3">
<input type="checkbox">
Connected servers
@@ -320,7 +328,7 @@ toc:
<hr role="separator">
<div class="field" data-orientation="horizontal">
<div role="group" class="field" data-orientation="horizontal">
{# <div class="flex w-full items-center gap-3"> #}
<input id="sync-desktop-documents" type="checkbox" checked class="input self-start">
<section>
@@ -342,19 +350,19 @@ toc:
<p>Yearly and lifetime plans offer significant savings.</p>
<div role="radiogroup" class="grid gap-3">
<div class="field">
<div role="group" class="field">
<label class="gap-3">
<input type="radio" name="subscription-plan" checked>
Monthly ($9.99/month)
</label>
</div>
<div class="field">
<div role="group" class="field">
<label class="gap-3">
<input type="radio" name="subscription-plan">
Yearly ($99.99/year)
</label>
</div>
<div class="field">
<div role="group" class="field">
<label class="gap-3">
<input type="radio" name="subscription-plan">
Lifetime ($299.99)
@@ -414,6 +422,34 @@ toc:
{{ code_preview("field-choice-card", code_choice_card, class="w-full max-w-md") }}
<h3 id="example-field-group"><a href="#example-field-group">Field Group</a></h3>
{% set code_field_group %}
<fieldset class="fieldset">
<legend>Account</legend>
<p>Use a field group to stack related fields with consistent spacing.</p>
<div role="group" aria-label="Field group">
<div role="group" class="field">
<label for="field-group-name">Name</label>
<input id="field-group-name" type="text" placeholder="Evil Rabbit">
</div>
<div class="field-separator">
<hr role="separator">
<span>Optional</span>
</div>
<div role="group" class="field">
<label for="field-group-bio">Bio</label>
<textarea id="field-group-bio" rows="3" placeholder="Tell us about yourself"></textarea>
</div>
</div>
</fieldset>
{% endset %}
{{ code_preview("field-group", code_field_group, class="w-full max-w-md") }}
<h3 id="example-responsive"><a href="#example-responsive">Responsive Layout</a></h3>
<section class="prose">
@@ -456,4 +492,27 @@ toc:
</fieldset>
{% endset %}
{{ code_preview("field-responsive", code_responsive, class="w-full max-w-3xl") }}
{{ code_preview("field-responsive", code_responsive, class="w-full max-w-3xl") }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Fields support right-to-left layouts through native form direction and logical spacing. Add <code>dir="rtl"</code> to the field, fieldset, or an ancestor.</p>
</section>
{% set code_rtl %}
<fieldset class="fieldset" dir="rtl">
<legend>الملف الشخصي</legend>
<p>تظهر هذه المعلومات في حسابك.</p>
<div role="group" aria-label="Field group">
<div role="group" class="field">
<label for="field-rtl-name">الاسم الكامل</label>
<input id="field-rtl-name" type="text" placeholder="أرنب شرير">
<p>اكتب اسمك الأول والأخير.</p>
</div>
</div>
</fieldset>
{% endset %}
{{ code_preview("field-rtl", code_rtl, class="w-full max-w-md") }}
-79
View File
@@ -1,79 +0,0 @@
---
layout: layouts/page.njk
title: Form
description: Building forms with Basecoat components.
toc:
- label: Usage
id: usage
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<form class="form grid 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-textarea">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="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>
<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" checked>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>
<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">Security emails</label>
<p class="text-muted-foreground text-sm">Receive emails about your account security.</p>
</div>
<input type="checkbox" id="demo-form-switch-disabled" role="switch" disabled>
</div>
</section>
<button type="submit" class="btn">Submit</button>
</form>{% endset %}
{{ code_preview("input", code, "w-full max-w-sm") }}
<h2 id="usage">Usage</h2>
<section class="prose">
<p>Adding the <code>form</code> class to the parent of any form element (<code>&lt;label&gt;</code>, <code>&lt;input&gt;</code>, <code>&lt;textarea&gt;</code>, <code>&lt;select&gt;</code>) will automatically apply the style you would get from adding <code>label</code>, <code>input</code>, <code>textarea</code>, or <code>select</code> classes. Read more about <a href="/components/label">Label</a>, <a href="/components/input">Input</a>, <a href="/components/textarea">Textarea</a>, and <a href="/components/select">Select</a>.</p>
</section>
{{ code_block(code) }}
+368 -241
View File
@@ -1,329 +1,364 @@
---
layout: layouts/page.njk
title: Input Group
description: Display additional information or actions to an input or textarea.
description: Add icons, text, buttons, and helper content to inputs and textareas.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Align
id: example-align
children:
- label: inline-start
id: align-inline-start
- label: inline-end
id: align-inline-end
- label: block-start
id: align-block-start
- label: block-end
id: align-block-end
- label: Icon
id: example-icon
- label: Text
id: example-text
- label: Button
id: example-button
- label: Tooltip
id: example-tooltip
- label: Textarea
id: example-textarea
- label: Spinner
id: example-spinner
- label: Kbd
id: example-kbd
- label: Dropdown
id: example-dropdown
- label: Group
id: example-group
- label: Spinner
id: example-spinner
- label: Textarea
id: example-textarea
- label: Custom Input
id: example-custom-input
- label: RTL
id: example-rtl
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "macros/code_preview.njk" import code_preview %}
{% from "dropdown-menu.njk" import dropdown_menu %}
{% from "popover.njk" import popover %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated Input Group component in Basecoat.</h2>
</div>
{% set spinner_icon %}{% lucide "loader-circle", { "role": "status", "aria-label": "Loading", "class": "animate-spin" } %}{% endset %}
{% set code_default %}
<div class="grid gap-6">
<div class="relative">
<input type="text" class="input pl-9 pr-20" placeholder="Search...">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4">
{% lucide "search" %}
</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">12 results</div>
<div class="input-group">
<input type="text" placeholder="Search...">
<span data-align="start" aria-hidden="true">{% lucide "search" %}</span>
<span data-align="end">12 results</span>
</div>
<div class="relative">
<input type="text" class="input pl-15 pr-9" placeholder="example.com">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">https://</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground [&>svg]:size-4" data-tooltip="This is content in a tooltip.">{% lucide "info" %}</div>
<div class="input-group">
<input type="text" placeholder="example.com">
<span data-align="start">https://</span>
<button type="button" class="btn-xs-icon-ghost rounded-full" data-align="end" aria-label="Info">
{% lucide "info" %}
</button>
</div>
<div class="relative">
<textarea class="textarea pr-10 min-h-27 pb-12" placeholder="Ask, Search or Chat..."></textarea>
<footer role="group" class="absolute bottom-0 px-3 pb-3 pt-1.5 flex items-center w-full gap-2">
<button type="button" class="btn-icon-outline rounded-full size-6 text-muted-foreground hover:text-accent-foreground">
{% lucide "plus" %}
</button>
<div class="input-group min-h-32" data-orientation="vertical">
<textarea placeholder="Ask, Search or Chat..."></textarea>
<footer data-align="end" role="group" aria-label="Message actions">
<button type="button" class="btn-xs-icon-outline rounded-full" aria-label="Add attachment">{% lucide "plus" %}</button>
{% set trigger %}Auto{% endset %}
{{ dropdown_menu(
id="demo-dropdown-menu",
trigger="Auto",
trigger_attrs={"class": "btn-sm-ghost text-muted-foreground hover:text-accent-foreground h-6 p-2"},
popover_attrs={"data-side": "top", "class": "min-w-32"},
main_attrs={"class": "min-w-0"},
trigger=trigger,
trigger_attrs={"class": "btn-xs-ghost"},
popover_attrs={"data-side": "top", "data-align": "start", "class": "min-w-28 [--radius:0.95rem]"},
items=[
{ type: "item", label: "Auto" },
{ type: "item", label: "Agent" },
{ type: "item", label: "Manual" }
]
) }}
<div class="text-muted-foreground text-sm ml-auto">52% used</div>
<hr class="w-0 h-4 border-r border-border shrink-0 m-0">
<button type="button" class="btn-icon rounded-full size-6 bg-muted-foreground hover:bg-foreground" disabled>
{% lucide "arrow-up" %}
</button>
<span class="ms-auto">52% used</span>
<hr role="separator" class="h-4">
<button type="button" class="btn-xs-icon rounded-full" disabled aria-label="Send">{% lucide "arrow-up" %}</button>
</footer>
</div>
<div class="relative">
<input type="text" class="input pr-9" placeholder="@shadcn">
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none bg-primary text-primary-foreground flex size-4 items-center justify-center rounded-full [&>svg]:size-3">{% lucide "check" %}</div>
<div class="input-group">
<input type="text" placeholder="@shadcn">
<span data-align="end" aria-hidden="true">
<span class="flex size-4 items-center justify-center rounded-full bg-primary text-primary-foreground">
{% lucide "check", { "class": "size-3" } %}
</span>
</span>
</div>
</div>
{% endset %}
{{ code_preview("input-group-default", code_default, class="w-full max-w-sm") }}
{{ code_preview("input-group-default", code_default | prettyHtml, class="w-full max-w-sm") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
{% set code_usage %}
<div class="input-group">
<input type="text" placeholder="Search...">
<span data-align="start" aria-hidden="true">
<!-- icon -->
</span>
<span data-align="end">12 results</span>
</div>
{% endset %}
{{ code_block(code_usage | prettyHtml, "html") }}
<section class="prose">
<p>Input groups are pure HTML composition using Tailwind utility classes. There is no dedicated component: use <code>relative</code> positioning on a wrapper, add your content with <code>absolute</code> positioning, and adjust input padding to make room.</p>
<p>Browse the examples below and copy the pattern that fits your needs.</p>
<p>Use <code>.input-group</code> on the root. The group owns the visible shell. Native controls stay semantic and become borderless children inside that shell.</p>
<p>The component has the following HTML structure:</p>
<dl>
<dt><code class="highlight language-html">&lt;div class="input-group"&gt;</code></dt>
<dd>The root shell. Add <code>role="group"</code> and an accessible label only when the group itself needs to be named.
<dl>
<dt><code class="highlight language-html">&lt;input&gt;</code>, <code class="highlight language-html">&lt;textarea&gt;</code>, <code class="highlight language-html">&lt;select&gt;</code></dt>
<dd>The native control. Use <code>aria-invalid="true"</code> for invalid state and <code>disabled</code> for disabled state.</dd>
<dt><code class="highlight language-html">&lt;span&gt;</code>, <code class="highlight language-html">&lt;svg&gt;</code>, <code class="highlight language-html">&lt;kbd&gt;</code></dt>
<dd>Decorative or helper content. Use <code>aria-hidden="true"</code> for decorative icons.</dd>
<dt><code class="highlight language-html">&lt;button&gt;</code>, <code class="highlight language-html">&lt;div role="group"&gt;</code></dt>
<dd>Interactive actions. Use <code>role="group"</code> with an accessible label when multiple related actions share one side of the input.</dd>
<dt><code class="highlight language-html">&lt;header&gt;</code>, <code class="highlight language-html">&lt;footer&gt;</code></dt>
<dd>Header or footer addon rows for vertically oriented input groups.</dd>
<dt><code class="highlight language-html">data-align="start|end"</code></dt>
<dd>Optional visual placement. Values use logical direction, so start/end automatically flip in RTL. Use <code>data-orientation="vertical"</code> on the root for header/footer layouts.</dd>
<dt><code class="highlight language-html">[data-control]</code></dt>
<dd>Escape hatch for custom controls that are not native <code>input</code>, <code>textarea</code>, or <code>select</code>.</dd>
</dl>
</dd>
</dl>
</section>
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-align"><a href="#example-align">Align</a></h3>
<section class="prose">
<p>Use <code>data-align="start|end"</code> on addon wrappers to position them inline. Use <code>data-orientation="vertical"</code> with <code>header</code> or <code>footer</code> for block placement.</p>
</section>
<h4 id="align-inline-start"><a href="#align-inline-start">inline-start</a></h4>
{% set code_align_inline_start %}
<div class="field max-w-sm">
<label for="inline-start-input">Input</label>
<div class="input-group">
<input id="inline-start-input" type="text" placeholder="Search...">
<span role="group" data-align="start" aria-hidden="true">
{% lucide "search", { "class": "text-muted-foreground" } %}
</span>
</div>
<p>Icon positioned at the start.</p>
</div>
{% endset %}
{{ code_preview("input-group-inline-start", code_align_inline_start | prettyHtml, class="w-full max-w-sm") }}
<h4 id="align-inline-end"><a href="#align-inline-end">inline-end</a></h4>
{% set code_align_inline_end %}
<div class="field max-w-sm">
<label for="inline-end-input">Input</label>
<div class="input-group">
<input id="inline-end-input" type="password" placeholder="Enter password">
<span role="group" data-align="end" aria-hidden="true">
{% lucide "eye-off" %}
</span>
</div>
<p>Icon positioned at the end.</p>
</div>
{% endset %}
{{ code_preview("input-group-inline-end", code_align_inline_end | prettyHtml, class="w-full max-w-sm") }}
<h4 id="align-block-start"><a href="#align-block-start">block-start</a></h4>
{% set code_align_block_start %}
<div role="group" aria-label="Block start input group examples" class="fieldset max-w-sm">
<div class="field">
<label for="block-start-input">Input</label>
<div class="input-group" data-orientation="vertical">
<input id="block-start-input" type="text" placeholder="Enter your name">
<header data-align="start">
<span>Full Name</span>
</header>
</div>
<p>Header positioned above the input.</p>
</div>
<div class="field">
<label for="block-start-textarea">Textarea</label>
<div class="input-group" data-orientation="vertical">
<textarea id="block-start-textarea" class="font-mono text-sm" placeholder="console.log('Hello, world!');"></textarea>
<header data-align="start">
{% lucide "file-code", { "class": "text-muted-foreground" } %}
<span class="font-mono">script.js</span>
<button type="button" class="btn-xs-icon-ghost ml-auto" aria-label="Copy">
{% lucide "copy" %}
</button>
</header>
</div>
<p>Header positioned above the textarea.</p>
</div>
</div>
{% endset %}
{{ code_preview("input-group-block-start", code_align_block_start | prettyHtml, class="w-full max-w-sm") }}
<h4 id="align-block-end"><a href="#align-block-end">block-end</a></h4>
{% set code_align_block_end %}
<div role="group" aria-label="Block end input group examples" class="fieldset max-w-sm">
<div class="field">
<label for="block-end-input">Input</label>
<div class="input-group" data-orientation="vertical">
<input id="block-end-input" type="text" placeholder="Enter amount">
<footer data-align="end">
<span>USD</span>
</footer>
</div>
<p>Footer positioned below the input.</p>
</div>
<div class="field">
<label for="block-end-textarea">Textarea</label>
<div class="input-group" data-orientation="vertical">
<textarea id="block-end-textarea" placeholder="Write a comment..."></textarea>
<footer data-align="end">
<span>0/280</span>
<button type="button" class="btn-sm ml-auto">Post</button>
</footer>
</div>
<p>Footer positioned below the textarea.</p>
</div>
</div>
{% endset %}
{{ code_preview("input-group-block-end", code_align_block_end | prettyHtml, class="w-full max-w-sm") }}
<h3 id="example-icon"><a href="#example-icon">Icon</a></h3>
{% set code_icon %}
<div class="grid gap-6">
<div class="relative">
<input type="text" class="input pl-9" placeholder="Search...">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4">
{% lucide "search" %}
</div>
<div class="input-group">
<input type="text" placeholder="Search...">
<span data-align="start" aria-hidden="true">{% lucide "search" %}</span>
</div>
<div class="relative">
<input type="email" class="input pl-9" placeholder="Enter your email">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4">
{% lucide "mail" %}
</div>
<div class="input-group">
<input type="email" placeholder="Enter your email">
<span data-align="start" aria-hidden="true">{% lucide "mail" %}</span>
</div>
<div class="relative">
<input type="text" class="input px-9" placeholder="Card number">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4">
{% lucide "credit-card" %}
</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4">
{% lucide "check" %}
</div>
<div class="input-group">
<input type="text" placeholder="Card number">
<span data-align="start" aria-hidden="true">{% lucide "credit-card" %}</span>
<span data-align="end" aria-hidden="true">{% lucide "check" %}</span>
</div>
<div class="relative">
<input type="text" class="input pr-16" placeholder="Card number">
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4 flex items-center gap-2">
<div class="input-group">
<input type="text" placeholder="Card number">
<span data-align="end" aria-hidden="true">
{% lucide "star" %}
{% lucide "info" %}
</div>
</span>
</div>
</div>
{% endset %}
{{ code_preview("input-group-icon-prefix", code_icon, class="w-full max-w-sm") }}
{{ code_preview("input-group-icon", code_icon | prettyHtml, class="w-full max-w-sm") }}
<h3 id="example-text"><a href="#example-text">Text</a></h3>
{% set code_text %}
<div class="grid gap-6">
<div class="relative">
<input type="text" class="input pl-7 pr-12" placeholder="0.00">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">$</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">USD</div>
<div class="input-group">
<input type="text" placeholder="0.00">
<span data-align="start">$</span>
<span data-align="end">USD</span>
</div>
<div class="relative">
<input type="text" class="input pl-15 pr-12" placeholder="example.com">
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">https://</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">.com</div>
<div class="input-group">
<input type="text" placeholder="example.com">
<span data-align="start">https://</span>
<span data-align="end">.com</span>
</div>
<div class="relative">
<input type="text" class="input pr-30" placeholder="Enter your username">
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground text-sm">@company.com</div>
<div class="input-group">
<input type="text" placeholder="Enter your username">
<span data-align="end">@company.com</span>
</div>
<div class="relative">
<textarea class="textarea pr-10 min-h-25 pb-12" placeholder="Enter your message"></textarea>
<footer role="group" class="absolute bottom-0 px-3 pb-3 pt-1.5 flex items-center w-full gap-2">
<div class="text-muted-foreground text-sm">120 characters left</div>
<div class="input-group" data-orientation="vertical">
<textarea placeholder="Enter your message"></textarea>
<footer data-align="end">
<span class="text-xs text-muted-foreground">120 characters left</span>
</footer>
</div>
</div>
{% endset %}
{{ code_preview("input-group-text", code_text, class="w-full max-w-sm") }}
{{ code_preview("input-group-text", code_text | prettyHtml, class="w-full max-w-sm") }}
<h3 id="example-button"><a href="#example-button">Button</a></h3>
{% set code_button %}
<div class="grid gap-6">
<div class="relative">
<input type="text" readonly class="input pr-9" value="https://x.com/hunvreus">
<button
data-copied="false"
onclick="
this.dataset.copied = 'true';
setTimeout(() => this.dataset.copied = 'false', 2000);
"
class="group absolute right-1.5 top-1/2 -translate-y-1/2 btn-icon-ghost text-muted-foreground hover:text-accent-foreground size-6"
>
{% lucide "copy", { "class": "group-data-[copied=true]:hidden" } %}
{% lucide "check", { "class": "hidden group-data-[copied=true]:block" } %}
</button>
</div>
<div class="relative">
<input type="text" class="input pl-21 pr-9 rounded-full">
<button
aria-pressed="false"
onclick="this.ariaPressed = this.ariaPressed === 'true' ? 'false' : 'true'"
class="absolute right-1.5 top-1/2 -translate-y-1/2 btn-icon-ghost text-muted-foreground hover:text-accent-foreground size-6 rounded-full aria-pressed:bg-transparent aria-pressed:hover:bg-accent dark:aria-pressed:hover:bg-accent/50 aria-pressed:text-blue-600 aria-pressed:[&>svg]:fill-blue-600"
>{% lucide "star" %}</button>
<div class="absolute left-1.5 top-1/2 -translate-y-1/2 flex items-center gap-1 z-10">
{% set trigger %}
{% lucide "info" %}
{% endset %}
{% call popover(
trigger=trigger,
trigger_attrs={"class": "btn-sm-icon-ghost rounded-full size-6"},
popover_attrs={"class": "max-w-72"}
) %}
<h3 class="font-medium mb-1">Your connection is not secure.</h3>
<p>You should not enter any sensitive information on this site.</p>
{% endcall %}
<div class="text-muted-foreground text-sm pointer-events-none">https://</div>
</div>
</div>
<div class="relative">
<input type="text" readonly class="input pr-9" placeholder="Type to search...">
<button type="button" class="absolute right-1.5 top-1/2 -translate-y-1/2 btn-secondary h-6 p-2 rounded">Search</button>
</div>
</div>
{% endset %}
{{ code_preview("input-group-button", code_button, class="w-full max-w-sm") }}
<h3 id="example-tooltip"><a href="#example-tooltip">Tooltip</a></h3>
{% set code_tooltip %}
<div class="grid gap-6">
<div class="relative">
<input type="password" class="input pr-9" placeholder="Enter password">
<div class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground [&>svg]:size-4" data-tooltip="Password must be at least 8 characters long.">{% lucide "info" %}</div>
</div>
<div class="relative">
<input type="email" class="input pr-9" placeholder="Your email address">
<div class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground [&>svg]:size-4" data-tooltip="We'll use this to send your notifications.">{% lucide "circle-help" %}</div>
</div>
<div class="relative">
<input type="email" class="input pl-9" placeholder="Your email address">
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground [&>svg]:size-4" data-tooltip="Click for help with API keys." data-side="left">{% lucide "circle-help" %}</div>
</div>
</div>
{% endset %}
{{ code_preview("input-group-tooltip", code_tooltip, class="w-full max-w-sm") }}
<h3 id="example-textarea"><a href="#example-textarea">Textarea</a></h3>
{% set code_textarea %}
<div class="relative">
<textarea class="textarea pt-15 pb-17 min-h-77" placeholder="console.log('Hello, world!')."></textarea>
<header class="absolute top-0 flex items-center w-full gap-2 p-3 border-b">
<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="tabler-icon tabler-icon-brand-javascript size-4 text-muted-foreground"><path d="M20 4l-2 14.5l-6 2l-6 -2l-2 -14.5z"></path><path d="M7.5 8h3v8l-2 -1"></path><path d="M16.5 8h-2.5a.5 .5 0 0 0 -.5 .5v3a.5 .5 0 0 0 .5 .5h1.423a.5 .5 0 0 1 .495 .57l-.418 2.93l-2 .5"></path></svg>
<span class="font-mono text-sm text-muted-foreground mr-auto">script.js</span>
<button class="btn-sm-icon-ghost text-muted-foreground hover:text-accent-foreground size-6">
{% lucide "refresh-ccw" %}
</button>
<button class="btn-sm-icon-ghost text-muted-foreground hover:text-accent-foreground size-6">
<div class="input-group">
<input type="text" readonly placeholder="https://x.com/shadcn">
<button type="button" class="btn-xs-icon-ghost" data-align="end" aria-label="Copy" title="Copy">
{% lucide "copy" %}
</button>
</header>
<footer class="absolute bottom-0 flex items-center w-full gap-2 p-3 border-t">
<span class="text-sm text-muted-foreground mr-auto">Line 1, Column 1</span>
<button type="button" class="btn-sm">
Run
{% lucide "corner-down-left" %}
</div>
<div class="input-group [--radius:9999px]">
<button type="button" class="btn-xs-icon-secondary" data-align="start" aria-label="Connection information">
{% lucide "info" %}
</button>
<span data-align="start" class="text-muted-foreground">https://</span>
<input type="text" id="input-secure-19">
<button type="button" class="btn-xs-icon-ghost" data-align="end" aria-label="Favorite">
{% lucide "star" %}
</button>
</footer>
</div>
{% endset %}
{{ code_preview("input-group-textarea", code_textarea, class="w-full max-w-md") }}
<h3 id="example-spinner"><a href="#example-spinner">Spinner</a></h3>
{% set code_spinner %}
<div class="grid gap-6">
<div class="relative">
<input type="text" class="input pr-9" placeholder="Searching..." disabled>
<div class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4 opacity-50">
{% lucide "loader-circle", { "class": "animate-spin" } %}
</div>
</div>
<div class="relative">
<input type="text" class="input pl-9" placeholder="Processing..." disabled>
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4 opacity-50">
{% lucide "loader-circle", { "class": "animate-spin" } %}
</div>
</div>
<div class="relative">
<input type="text" class="input pr-24" placeholder="Saving changes..." disabled>
<div class="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2 text-sm text-muted-foreground opacity-50">
<div>Saving...</div>
<div class="[&>svg]:size-4">
{% lucide "loader-circle", { "class": "animate-spin" } %}
</div>
</div>
</div>
<div class="relative">
<input type="text" class="input pl-9 pr-26" placeholder="Refreshing data..." disabled>
<div class="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground [&>svg]:size-4 opacity-50">
{% lucide "loader", { "class": "animate-spin" } %}
</div>
<div class="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2 text-sm text-muted-foreground opacity-50">
Please wait...
</div>
<div class="input-group">
<input type="text" placeholder="Type to search...">
<button type="button" class="btn-secondary" data-align="end">Search</button>
</div>
</div>
{% endset %}
{{ code_preview("input-group-spinner", code_spinner, class="w-full max-w-md") }}
{{ code_preview("input-group-button", code_button | prettyHtml, class="w-full max-w-sm") }}
<h3 id="example-kbd"><a href="#example-kbd">Kbd</a></h3>
{% set code_kbd %}
<div class="input-group">
<input type="text" placeholder="Search...">
<span data-align="start" aria-hidden="true">{% lucide "search" %}</span>
<kbd data-align="end">⌘K</kbd>
</div>
{% endset %}
{{ code_preview("input-group-kbd", code_kbd | prettyHtml, class="w-full max-w-sm") }}
<h3 id="example-dropdown"><a href="#example-dropdown">Dropdown</a></h3>
{% set code_dropdown %}
<div class="grid gap-6">
<div class="relative">
<input type="text" class="input pr-9" placeholder="Enter file name">
{% set trigger %}
{% lucide "ellipsis" %}
{% endset %}
<div class="input-group">
<input type="text" placeholder="Enter file name">
{% set trigger %}{% lucide "ellipsis" %}{% endset %}
{{ dropdown_menu(
main_attrs={"class": "absolute right-1.5 top-1/2 -translate-y-1/2 z-10"},
main_attrs={"data-align": "end"},
trigger=trigger,
trigger_attrs={"class": "btn-sm-icon-ghost text-muted-foreground hover:text-accent-foreground size-6"},
trigger_attrs={"class": "btn-xs-icon-ghost", "aria-label": "Open file actions"},
popover_attrs={"data-align": "end", "class": "min-w-32"},
items=[
{ type: "item", label: "Settings" },
@@ -333,17 +368,14 @@ toc:
) }}
</div>
<div class="relative">
<input type="text" class="input pr-30" placeholder="Enter search query">
{% set trigger %}
Search in...
{% lucide "chevron-down", { "class": "size-3" } %}
{% endset %}
<div class="input-group [--radius:1rem]">
<input type="text" placeholder="Enter search query">
{% set trigger %}Search In... {% lucide "chevron-down", { "class": "size-3" } %}{% endset %}
{{ dropdown_menu(
main_attrs={"class": "absolute right-1.5 top-1/2 -translate-y-1/2"},
main_attrs={"data-align": "end"},
trigger=trigger,
trigger_attrs={"class": "btn-sm-ghost text-muted-foreground hover:text-accent-foreground p-2 h-6"},
popover_attrs={"data-align": "end", "class": "min-w-32"},
trigger_attrs={"class": "btn-xs-ghost pr-1.5! text-xs"},
popover_attrs={"data-align": "end", "class": "min-w-32 [--radius:0.95rem]"},
items=[
{ type: "item", label: "Documentation" },
{ type: "item", label: "Blog Posts" },
@@ -354,19 +386,114 @@ toc:
</div>
{% endset %}
{{ code_preview("input-group-dropdown", code_dropdown, class="w-full max-w-sm") }}
{{ code_preview("input-group-dropdown", code_dropdown | prettyHtml, class="w-full max-w-sm") }}
<h3 id="example-group"><a href="#example-group">Group</a></h3>
<h3 id="example-spinner"><a href="#example-spinner">Spinner</a></h3>
{% set code_group %}
<div class="flex w-fit items-stretch">
<label for="url" class="text-sm flex items-center bg-muted border border-r-0 px-4 rounded-l-md shadow-xs">https://</label>
<div class="relative">
<input type="text" class="input pr-9 rounded-none">
<div class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground [&>svg]:size-4">{% lucide "link-2" %}</div>
{% set code_spinner %}
<div class="grid gap-6">
<div class="input-group">
<input type="text" placeholder="Searching...">
<span data-align="end">{{ spinner_icon | safe }}</span>
</div>
<div class="input-group">
<input type="text" placeholder="Processing...">
<span data-align="start">{{ spinner_icon | safe }}</span>
</div>
<div class="input-group">
<input type="text" placeholder="Saving changes...">
<span data-align="end">
Saving...
{{ spinner_icon | safe }}
</span>
</div>
<div class="input-group">
<input type="text" placeholder="Refreshing data...">
<span data-align="start" aria-hidden="true">{% lucide "loader", { "class": "animate-spin" } %}</span>
<span data-align="end" class="text-muted-foreground">Please wait...</span>
</div>
<div class="text-sm flex items-center bg-muted border border-l-0 px-4 rounded-r-md shadow-xs">.com</div>
</div>
{% endset %}
{{ code_preview("input-group-group", code_group, class="w-full max-w-sm") }}
{{ code_preview("input-group-spinner", code_spinner | prettyHtml, class="w-full max-w-sm") }}
<h3 id="example-textarea"><a href="#example-textarea">Textarea</a></h3>
{% set code_textarea %}
<div class="input-group min-h-[200px]" data-orientation="vertical">
<textarea placeholder="console.log('Hello, world!');"></textarea>
<footer data-align="end" class="border-t">
<span>Line 1, Column 1</span>
<button type="button" class="btn-sm ml-auto">Run {% lucide "corner-down-left" %}</button>
</footer>
<header data-align="start" class="border-b">
{% lucide "file-code", { "class": "text-muted-foreground" } %}
<span>script.js</span>
<div role="group" aria-label="File actions" class="ml-auto flex gap-1">
<button type="button" class="btn-xs-icon-ghost">{% lucide "refresh-ccw" %}</button>
<button type="button" class="btn-xs-icon-ghost">{% lucide "copy" %}</button>
</div>
</header>
</div>
{% endset %}
{{ code_preview("input-group-textarea", code_textarea | prettyHtml, class="w-full max-w-md") }}
<h3 id="example-custom-input"><a href="#example-custom-input">Custom Input</a></h3>
{% set code_custom %}
<div class="input-group" data-orientation="vertical">
<textarea
data-control
placeholder="Autoresize textarea..."
class="field-sizing-content min-h-16 resize-none"
></textarea>
<footer data-align="end">
<button type="button" class="btn-sm ml-auto">Submit</button>
</footer>
</div>
{% endset %}
{{ code_preview("input-group-custom", code_custom | prettyHtml, class="w-full max-w-sm") }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
{% set code_rtl %}
<div dir="rtl" class="grid gap-6">
<div class="input-group">
<input type="text" placeholder="بحث...">
<span role="group" data-align="start" aria-hidden="true">{% lucide "search" %}</span>
<span role="group" data-align="end">١٢ نتيجة</span>
</div>
<div class="input-group">
<input type="text" placeholder="جاري البحث...">
<span role="group" data-align="end">{{ spinner_icon | safe }}</span>
</div>
<div class="input-group">
<input type="text" placeholder="جاري حفظ التغييرات...">
<span role="group" data-align="end">
جاري الحفظ...
{{ spinner_icon | safe }}
</span>
</div>
<div class="field">
<label for="rtl-textarea">منطقة النص</label>
<div class="input-group" data-orientation="vertical">
<textarea id="rtl-textarea" placeholder="اكتب تعليقًا..."></textarea>
<footer data-align="end">
<span>٠/٢٨٠</span>
<button type="button" class="btn-sm ms-auto">نشر</button>
</footer>
</div>
<p>تذييل موضع أسفل منطقة النص.</p>
</div>
</div>
{% endset %}
{{ code_preview("input-group-rtl", code_rtl | prettyHtml, class="w-full max-w-sm") }}
+152 -42
View File
@@ -1,90 +1,200 @@
---
layout: layouts/page.njk
title: Input
description: Displays a form input field or a component that looks like an input field.
description: A text input component for forms and user data entry with built-in styling and accessibility features.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Default
id: example-default
- label: Invalid
id: example-invalid
- label: Basic
id: example-basic
- label: Field
id: example-field
- label: Field Group
id: example-field-group
- label: Disabled
id: example-disabled
- label: With label
id: example-with-label
- label: With text
id: example-with-text
- label: With button
id: example-with-button
- label: Invalid
id: example-invalid
- label: File
id: example-file
- label: Inline
id: example-inline
- label: Grid
id: example-grid
- label: Required
id: example-required
- label: Badge
id: example-badge
- label: Input Group
id: example-input-group
- label: Button Group
id: example-button-group
- label: Form
id: example-form
- label: RTL
id: example-rtl
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<input class="input" type="email" placeholder="Email">{% endset %}
{{ code_preview("input", code) }}
{{ code_preview("input", code, class="w-full max-w-xs") }}
<h2 id="usage">Usage</h2>
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Simply add the <code>input</code> class to your <code>&lt;input&gt;</code> element 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>
<p>Add the <code>input</code> class to supported <code>&lt;input&gt;</code> elements. Inputs inside a <code>form</code> or <code>field</code> container also receive the same styling automatically for common text-like input types.</p>
<p>Use native HTML attributes for behavior: <code>disabled</code> for disabled inputs, <code>required</code> for required fields, and <code>aria-invalid="true"</code> for invalid state styling.</p>
</section>
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
{{ code_preview("input-default", code) }}
<h3 id="example-basic"><a href="#example-basic">Basic</a></h3>
<h3 id="example-invalid"><a href="#example-invalid">Invalid</a></h3>
{% set code_basic %}<input class="input" type="email" placeholder="Email">{% endset %}
{{ code_preview("input-basic", code_basic, class="w-full max-w-xs") }}
{% set code %}<input class="input" type="email" placeholder="Email" aria-invalid="true">{% endset %}
{{ code_preview("input-invalid", code) }}
<h3 id="example-field"><a href="#example-field">Field</a></h3>
<section class="prose">
<p>Use <code>field</code> with a label and description to create an accessible field.</p>
</section>
{% set code_field %}<div role="group" class="field w-full max-w-xs">
<label for="input-field">Email</label>
<input id="input-field" type="email" placeholder="m@example.com">
<p>Enter your email address.</p>
</div>{% endset %}
{{ code_preview("input-field", code_field, class="w-full max-w-xs") }}
<h3 id="example-field-group"><a href="#example-field-group">Field Group</a></h3>
{% set code_field_group %}<div class="grid w-full max-w-xs gap-4">
<div role="group" class="field">
<label for="input-first-name">First name</label>
<input id="input-first-name" type="text" placeholder="Jane">
</div>
<div role="group" class="field">
<label for="input-last-name">Last name</label>
<input id="input-last-name" type="text" placeholder="Doe">
</div>
</div>{% endset %}
{{ code_preview("input-field-group", code_field_group, class="w-full max-w-xs") }}
<h3 id="example-disabled"><a href="#example-disabled">Disabled</a></h3>
{% set code %}<input class="input" type="email" placeholder="Email" disabled>{% endset %}
{{ code_preview("input-disabled", code) }}
{% set code_disabled %}<input class="input" type="email" placeholder="Email" disabled>{% endset %}
{{ code_preview("input-disabled", code_disabled, class="w-full max-w-xs") }}
<h3 id="example-with-label"><a href="#example-with-label">With label</a></h3>
<h3 id="example-invalid"><a href="#example-invalid">Invalid</a></h3>
{% set code %}<div class="grid gap-3">
<label for="input-with-label" class="label">Label</label>
<input class="input" id="input-with-label" type="email" placeholder="Email">
{% set code_invalid %}<div role="group" class="field w-full max-w-xs">
<label for="input-invalid">Email</label>
<input class="input" id="input-invalid" type="email" placeholder="Email" aria-invalid="true">
<p>Please enter a valid email address.</p>
</div>{% endset %}
{{ code_preview("input-with-label", code) }}
{{ code_preview("input-invalid", code_invalid, class="w-full max-w-xs") }}
<h3 id="example-with-text"><a href="#example-with-text">With text</a></h3>
<h3 id="example-file"><a href="#example-file">File</a></h3>
{% set code %}<div class="grid gap-3">
<label for="input-with-text" class="label">Label</label>
<input class="input" id="input-with-text" type="email" placeholder="Email">
<p class="text-muted-foreground text-sm">Fill in your email address.</p>
{% set code_file %}<input class="input" type="file">{% endset %}
{{ code_preview("input-file", code_file, class="w-full max-w-xs") }}
<h3 id="example-inline"><a href="#example-inline">Inline</a></h3>
{% set code_inline %}<div class="flex w-full max-w-xs items-center gap-2">
<input class="input" type="search" placeholder="Search...">
<button type="submit" class="btn">Search</button>
</div>{% endset %}
{{ code_preview("input-with-text", code) }}
{{ code_preview("input-inline", code_inline, class="w-full max-w-xs") }}
<h3 id="example-with-button"><a href="#example-with-button">With button</a></h3>
<h3 id="example-grid"><a href="#example-grid">Grid</a></h3>
{% set code %}<div class="flex items-center space-x-2">
<input class="input" type="email" placeholder="Email">
<button type="submit" class="btn">Submit</button>
{% set code_grid %}<div class="grid w-full gap-4 md:grid-cols-2">
<input class="input" type="text" placeholder="First name">
<input class="input" type="text" placeholder="Last name">
<input class="input md:col-span-2" type="email" placeholder="Email">
</div>{% endset %}
{{ code_preview("textarea-form", code) }}
{{ code_preview("input-grid", code_grid, class="w-full max-w-xl") }}
<h3 id="example-required"><a href="#example-required">Required</a></h3>
{% set code_required %}<div role="group" class="field w-full max-w-xs">
<label for="input-required">Username <span class="text-destructive">*</span></label>
<input id="input-required" type="text" placeholder="hunvreus" required>
</div>{% endset %}
{{ code_preview("input-required", code_required, class="w-full max-w-xs") }}
<h3 id="example-badge"><a href="#example-badge">Badge</a></h3>
{% set code_badge %}<div role="group" class="field w-full max-w-xs">
<label for="input-badge" class="flex items-center gap-2">
Email
<span class="badge-secondary">Recommended</span>
</label>
<input id="input-badge" type="email" placeholder="m@example.com">
</div>{% endset %}
{{ code_preview("input-badge", code_badge, class="w-full max-w-xs") }}
<h3 id="example-input-group"><a href="#example-input-group">Input Group</a></h3>
<section class="prose">
<p>Use the <a href="/components/input-group/">Input Group</a> patterns to add icons, text, or buttons inside an input.</p>
</section>
{% set code_input_group %}<div class="relative w-full max-w-xs">
<input class="input ps-9" type="search" placeholder="Search...">
<div class="pointer-events-none absolute start-3 top-1/2 -translate-y-1/2 text-muted-foreground [&>svg]:size-4">
{% lucide "search" %}
</div>
</div>{% endset %}
{{ code_preview("input-input-group", code_input_group, class="w-full max-w-xs") }}
<h3 id="example-button-group"><a href="#example-button-group">Button Group</a></h3>
<section class="prose">
<p>Use <a href="/components/button-group/">Button Group</a> when an input should be grouped with adjacent buttons.</p>
</section>
{% set code_button_group %}<div role="group" aria-label="Search" class="button-group w-full max-w-xs">
<input class="input" type="search" placeholder="Search...">
<button type="submit" class="btn-icon-outline" aria-label="Search">
{% lucide "search" %}
</button>
</div>{% endset %}
{{ code_preview("input-button-group", code_button_group, class="w-full max-w-xs") }}
<h3 id="example-form"><a href="#example-form">Form</a></h3>
{% set code %}<form class="form space-y-6 w-full">
<div class="grid gap-3">
<label for="input-form" class="label">Username</label>
<input class="input" id="input-form" type="text" placeholder="hunvreus">
<p class="text-muted-foreground text-sm">This is your public display name.</p>
{% set code_form %}<form class="w-full max-w-sm space-y-6">
<div role="group" class="field">
<label for="input-form-username">Username</label>
<input id="input-form-username" type="text" placeholder="hunvreus" required>
<p>This is your public display name.</p>
</div>
<div role="group" class="field">
<label for="input-form-email">Email</label>
<input id="input-form-email" type="email" placeholder="m@example.com" required>
</div>
<button type="submit" class="btn">Submit</button>
</form>{% endset %}
{{ code_preview("input-form", code, "w-full max-w-sm") }}
{{ code_preview("input-form", code_form, class="w-full max-w-sm") }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Inputs support right-to-left layouts through native browser direction handling. Add <code>dir="rtl"</code> to the input, field, or an ancestor.</p>
</section>
{% set code_rtl %}<div role="group" class="field w-full max-w-xs" dir="rtl">
<label for="input-rtl-api-key">مفتاح API</label>
<input id="input-rtl-api-key" type="password" placeholder="sk-...">
<p>مفتاح API الخاص بك مشفر ومخزن بأمان.</p>
</div>{% endset %}
{{ code_preview("input-rtl", code_rtl, class="w-full max-w-xs") }}
+166 -222
View File
@@ -5,6 +5,11 @@ description: A versatile component that you can use to display any content.
toc:
- label: Usage
id: usage
children:
- label: HTML
id: usage-html
- label: HTML structure
id: usage-html-structure
- label: Examples
id: examples
children:
@@ -24,47 +29,65 @@ toc:
id: example-header
- label: Link
id: example-link
- label: RTL
id: example-rtl
---
{% from "macros/code_block.njk" import code_block %}
{% from "macros/code_preview.njk" import code_preview %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated Item component in Basecoat.</h2>
</div>
{% set code_basic %}
<div class="flex flex-col gap-6">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Basic Item</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">A simple item with title and description.</p>
</div>
<article class="item" data-variant="outline">
<section>
<h3>Basic Item</h3>
<p>A simple item with title and description.</p>
</section>
<aside>
<button class="btn-sm-outline">Action</button>
</article>
<a href="#" class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border py-3 px-4 gap-2.5">
<div class="flex shrink-0 items-center justify-center gap-2 [&_svg]:pointer-events-none bg-transparent [&_svg]:size-5">
{% lucide "badge-check" %}
</div>
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Your profile has been verified.</h3>
</div>
<div class="flex items-center gap-2 [&_svg]:size-4">
{% lucide "chevron-right" %}
</div>
</a>
</div>
</aside>
</article>
{% endset %}
{{ code_preview("item-basic", code_basic | prettyHtml, class="w-full max-w-md") }}
{{ code_preview("item-basic", code_basic, class="w-full max-w-md") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Items are pure HTML composition using Tailwind utility classes. Use semantic elements like <code>&lt;article&gt;</code> or <code>&lt;a&gt;</code>, apply flex layout, and compose with standard HTML elements like <code>&lt;h3&gt;</code>, <code>&lt;p&gt;</code>, <code>&lt;img&gt;</code>, or <code>&lt;button&gt;</code>.</p>
<p>Browse the examples below and copy the pattern that fits your needs.</p>
<p>Use <code>.item</code> on a semantic element such as <code>&lt;article&gt;</code> or <code>&lt;a&gt;</code>. Use <code>.item-group</code> when listing multiple items.</p>
</section>
<h3 id="usage-html"><a href="#usage-html">HTML</a></h3>
{{ code_block(code_basic) }}
<h3 id="usage-html-structure"><a href="#usage-html-structure">HTML structure</a></h3>
<section class="prose">
<p>The component has the following HTML structure:</p>
<dl>
<dt><code class="highlight language-html">&lt;article class="item"&gt;</code> or <code class="highlight language-html">&lt;a class="item"&gt;</code></dt>
<dd>The item root. Use <code>data-variant="outline|muted"</code> and <code>data-size="sm|xs"</code> when needed.
<dl>
<dt><code class="highlight language-html">&lt;figure&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Media: icon, avatar, or image.</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code></dt>
<dd>Primary content. Headings and paragraphs are styled automatically.
<dl>
<dt><code class="highlight language-html">&lt;h2&gt;</code>, <code class="highlight language-html">&lt;h3&gt;</code>, or <code class="highlight language-html">&lt;h4&gt;</code></dt>
<dd>The item title.</dd>
<dt><code class="highlight language-html">&lt;p&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The item description.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;aside&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Actions or trailing metadata.</dd>
<dt><code class="highlight language-html">&lt;header&gt;</code>, <code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Full-width item regions.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;div class="item-group" role="list"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Groups related items. Use <code>role="listitem"</code> on child items when they are not native list items.</dd>
</dl>
</section>
<h2 id="examples"><a href="#examples">Examples</a></h2>
@@ -72,249 +95,170 @@ toc:
<h3 id="example-variants"><a href="#example-variants">Variants</a></h3>
{% set code_variants %}
<div class="flex flex-col gap-6">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent p-4 gap-4">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Default Variant</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Standard styling with subtle background and borders.</p>
</div>
<button class="btn-sm-outline">Open</button>
<div class="item-group" role="list">
<article class="item" role="listitem">
<section>
<h3>Default Variant</h3>
<p>Transparent border for quiet item rows.</p>
</section>
<aside><button class="btn-sm-outline">Open</button></aside>
</article>
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Outline Variant</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Outlined style with clear borders and transparent background.</p>
</div>
<button class="btn-sm-outline">Open</button>
<article class="item" data-variant="outline" role="listitem">
<section>
<h3>Outline Variant</h3>
<p>Outlined style with a visible border.</p>
</section>
<aside><button class="btn-sm-outline">Open</button></aside>
</article>
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent bg-muted/50 p-4 gap-4">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Muted Variant</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Subdued appearance with muted colors for secondary content.</p>
</div>
<button class="btn-sm-outline">Open</button>
<article class="item" data-variant="muted" role="listitem">
<section>
<h3>Muted Variant</h3>
<p>Subdued appearance for secondary content.</p>
</section>
<aside><button class="btn-sm-outline">Open</button></aside>
</article>
</div>
{% endset %}
{{ code_preview("item-variants", code_variants) }}
{{ code_preview("item-variants", code_variants, class="w-full max-w-md") }}
<h3 id="example-size"><a href="#example-size">Size</a></h3>
<section class="prose">
<p>Use different padding and gap sizes for compact or spacious layouts.</p>
</section>
{% set code_size %}
<div class="item-group" role="list">
<article class="item" data-variant="outline" role="listitem">
<section><h3>Default</h3><p>Default item size.</p></section>
</article>
<article class="item" data-variant="outline" data-size="sm" role="listitem">
<section><h3>Small</h3><p>Small item size.</p></section>
</article>
<article class="item" data-variant="outline" data-size="xs" role="listitem">
<section><h3>Extra Small</h3><p>Extra small item size.</p></section>
</article>
</div>
{% endset %}
{{ code_preview("item-size", code_basic) }}
{{ code_preview("item-size", code_size, class="w-full max-w-md") }}
<h3 id="example-icon"><a href="#example-icon">Icon</a></h3>
{% set code_icon %}
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<div class="flex shrink-0 items-center justify-center gap-2 self-start [&_svg]:pointer-events-none size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4">
<article class="item" data-variant="outline">
<figure>
{% lucide "shield-alert" %}
</div>
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Security Alert</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">New login detected from unknown device.</p>
</div>
<button class="btn-sm-outline">Review</button>
</figure>
<section>
<h3>Security Alert</h3>
<p>New login detected from an unknown device.</p>
</section>
<aside><button class="btn-sm-outline">Review</button></aside>
</article>
{% endset %}
{{ code_preview("item-icon", code_icon, class="max-w-lg") }}
{{ code_preview("item-icon", code_icon, class="w-full max-w-lg") }}
<h3 id="example-avatar"><a href="#example-avatar">Avatar</a></h3>
{% set code_avatar %}
<div class="flex flex-col gap-6">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img src="https://github.com/hunvreus.png" alt="hunvreus" class="size-10 rounded-full object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">hunvreus</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Last seen 5 months ago</p>
</div>
<button class="btn-icon-outline rounded-full">
{% lucide "plus" %}
</button>
</article>
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<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 self-start">
<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 flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">No Team Members</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Invite your team to collaborate on this project.</p>
</div>
<button class="btn-outline">
Invite
</button>
</article>
</div>
<article class="item" data-variant="outline">
<figure>
<img src="https://github.com/hunvreus.png" alt="hunvreus">
</figure>
<section>
<h3>hunvreus</h3>
<p>Last seen 5 months ago.</p>
</section>
<aside>
<button class="btn-icon-outline rounded-full">{% lucide "plus" %}</button>
</aside>
</article>
{% endset %}
{{ code_preview("item-avatar", code_avatar, class="w-full max-w-lg") }}
{{ code_preview("item-avatar", code_avatar, class="w-full max-w-md") }}
<h3 id="example-image"><a href="#example-image">Image</a></h3>
{% set code_image %}
<div class="flex flex-col gap-6 w-full max-w-md">
<a href="#" class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img src="https://avatar.vercel.sh/example_1" alt="Midnight City Lights" class="grayscale size-10 rounded-sm object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="w-fit items-center gap-2 text-sm leading-snug font-medium line-clamp-1">Midnight City Lights - <span class="text-muted-foreground">Electric Nights</span></h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Neon Dreams</p>
</div>
<div class="flex flex-col gap-1 flex-none text-center">
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">3:45</p>
</div>
</a>
<a href="#" class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img src="https://avatar.vercel.sh/example_2" alt="Midnight City Lights" class="grayscale size-10 rounded-sm object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="w-fit items-center gap-2 text-sm leading-snug font-medium line-clamp-1">Coffee Shop Conversations - <span class="text-muted-foreground">Urban Stories</span></h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">The Morning Brew</p>
</div>
<div class="flex flex-col gap-1 flex-none text-center">
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">4:05</p>
</div>
</a>
<a href="#" class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img src="https://avatar.vercel.sh/example_3" alt="Midnight City Lights" class="grayscale size-10 rounded-sm object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="w-fit items-center gap-2 text-sm leading-snug font-medium line-clamp-1">Digital Rain - <span class="text-muted-foreground">Binary Beats</span></h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Cyber Symphony</p>
</div>
<div class="flex flex-col gap-1 flex-none text-center">
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">3:30</p>
</div>
</a>
</div>
<a href="#" class="item" data-variant="outline">
<figure>
<img src="https://avatar.vercel.sh/example_1" alt="Midnight City Lights">
</figure>
<section>
<h3>Midnight City Lights</h3>
<p>Neon Dreams</p>
</section>
<section>
<p>3:45</p>
</section>
</a>
{% endset %}
{{ code_preview("item-image", code_image) }}
{{ code_preview("item-image", code_image, class="w-full max-w-md") }}
<h3 id="example-group"><a href="#example-group">Group</a></h3>
{% set code_group %}
<div class="flex flex-col">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent p-4 gap-4">
<img src="https://github.com/hunvreus.png" alt="Ronan Berder" class="grayscale size-10 rounded-full object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">hunvreus</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">hunvreus@gmail.com</p>
</div>
<button class="btn-icon-outline rounded-full">
{% lucide "plus" %}
</button>
<div class="item-group" role="list">
<article class="item" role="listitem">
<figure><img src="https://github.com/hunvreus.png" alt="hunvreus"></figure>
<section><h3>hunvreus</h3><p>hunvreus@gmail.com</p></section>
<aside><button class="btn-icon-outline rounded-full">{% lucide "plus" %}</button></aside>
</article>
<hr role="separator" class="border-border">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent p-4 gap-4">
<img src="https://github.com/shadcn.png" alt="shadcn" class="grayscale size-10 rounded-full object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">shadcn</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">shadcn@vercel.com</p>
</div>
<button class="btn-icon-outline rounded-full">
{% lucide "plus" %}
</button>
</article>
<hr role="separator" class="border-border">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent p-4 gap-4">
<img src="https://github.com/adamwathan.png" alt="Adam Wathan" class="grayscale size-10 rounded-full object-cover">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">adamwathan</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">adam.wathan@gmail.com</p>
</div>
<button class="btn-icon-outline rounded-full">
{% lucide "plus" %}
</button>
<hr role="separator">
<article class="item" role="listitem">
<figure><img src="https://github.com/shadcn.png" alt="shadcn"></figure>
<section><h3>shadcn</h3><p>shadcn@vercel.com</p></section>
<aside><button class="btn-icon-outline rounded-full">{% lucide "plus" %}</button></aside>
</article>
</div>
{% endset %}
{{ code_preview("item-group", code_group, class="w-full max-w-md") }}
<h3 id="example-header"><a href="#example-header">Header</a></h3>
{% set code_header %}
<div class="flex-col grid grid-cols-3 gap-4">
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img alt="v0-1.5-sm" loading="lazy" class="grayscale aspect-square w-full rounded-sm object-cover flex-basis" src="https://avatar.vercel.sh/v0-1.5-sm">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">v0-1.5-sm</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Everyday tasks and UI generation.</p>
</div>
</article>
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img alt="v0-1.5-lg" loading="lazy" class="grayscale aspect-square w-full rounded-sm object-cover flex-basis" src="https://avatar.vercel.sh/v0-1.5-lg">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">v0-1.5-lg</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Advanced thinking or reasoning.</p>
</div>
</article>
<article class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<img alt="v0-2.0-mini" loading="lazy" class="grayscale aspect-square w-full rounded-sm object-cover flex-basis" src="https://avatar.vercel.sh/v0-2.0-mini">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">v0-2.0-mini</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Open Source model for everyone.</p>
</div>
</article>
</div>
<article class="item" data-variant="outline">
<header>
<h3>v0-1.5-sm</h3>
<span class="badge-outline">Model</span>
</header>
<figure>
<img src="https://avatar.vercel.sh/v0-1.5-sm" alt="v0-1.5-sm">
</figure>
<section>
<p>Everyday tasks and UI generation.</p>
</section>
</article>
{% endset %}
{{ code_preview("item-header", code_header) }}
{{ code_preview("item-header", code_header, class="w-full max-w-md") }}
<h3 id="example-link"><a href="#example-link">Link</a></h3>
<section class="prose">
<p>Items can be rendered as links by using an <code>&lt;a&gt;</code> element. Hover and focus states are applied automatically.</p>
</section>
{% set code_link %}
<div class="flex flex-col gap-6">
<a href="#" class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-transparent p-4 gap-4">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">Visit our documentation</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Learn how to get started with our components.</p>
</div>
<div class="flex items-center gap-2 [&_svg]:size-4">
{% lucide "chevron-right" %}
</div>
</a>
<a href="#" class="group/item flex items-center border text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] border-border p-4 gap-4">
<div class="flex flex-1 flex-col gap-1">
<h3 class="flex w-fit items-center gap-2 text-sm leading-snug font-medium">External resource</h3>
<p class="text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4">Opens in a new tab with security attributes.</p>
</div>
<div class="flex items-center gap-2 [&_svg]:size-4">
{% lucide "external-link" %}
</div>
</a>
</div>
<a href="#" class="item">
<section>
<h3>Visit our documentation</h3>
<p>Learn how to get started with our components.</p>
</section>
<aside>{% lucide "chevron-right" %}</aside>
</a>
{% endset %}
{{ code_preview("item-link", code_link, class="w-full max-w-md") }}
{{ code_preview("item-link", code_link, class="w-full max-w-md") }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
{% set code_rtl %}
<article class="item" data-variant="outline" dir="rtl">
<figure>{% lucide "shield-alert" %}</figure>
<section>
<h3>تنبيه أمني</h3>
<p>تم اكتشاف تسجيل دخول من جهاز غير معروف.</p>
</section>
<aside><button class="btn-sm-outline">مراجعة</button></aside>
</article>
{% endset %}
{{ code_preview("item-rtl", code_rtl, class="w-full max-w-lg") }}
+42 -7
View File
@@ -12,6 +12,10 @@ toc:
id: example-group
- label: Button
id: example-button
- label: Input Group
id: example-input-group
- label: RTL
id: example-rtl
---
{% from "macros/code_preview.njk" import code_preview %}
@@ -19,13 +23,13 @@ toc:
{% set code_html %}
<div class="flex flex-col items-center gap-4">
<span class="inline-flex items-center gap-1">
<span class="kbd-group">
<kbd class="kbd">⌘</kbd>
<kbd class="kbd">⇧</kbd>
<kbd class="kbd">⌥</kbd>
<kbd class="kbd">⌃</kbd>
</span>
<span class="inline-flex items-center gap-1">
<span class="kbd-group">
<kbd class="kbd">Ctrl</kbd>
<span>+</span>
<kbd class="kbd">B</kbd>
@@ -38,7 +42,7 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Use the <code>&lt;kbd&gt;</code> element with the <code>kbd</code> class:</p>
<p>Use the <code>&lt;kbd&gt;</code> element with the <code>kbd</code> class. Use <code>kbd-group</code> to group multiple keys.</p>
</section>
{% set code_simple %}
@@ -54,7 +58,7 @@ toc:
{% set group_html %}
<p class="text-muted-foreground text-sm">
Use
<span class="inline-flex items-center gap-1">
<span class="kbd-group">
<kbd class="kbd">Ctrl + B</kbd>
<kbd class="kbd">Ctrl + K</kbd>
</span>
@@ -68,9 +72,40 @@ toc:
{% set button_html %}
<div class="flex flex-wrap items-center gap-x-4">
<button class="btn-sm-outline pr-2">Accept <kbd class="kbd">⏎</kbd></button>
<button class="btn-sm-outline pr-2">Cancel <kbd class="kbd">Esc</kbd></button>
<button class="btn-sm-outline pe-2">Accept <kbd class="kbd">⏎</kbd></button>
<button class="btn-sm-outline pe-2">Cancel <kbd class="kbd">Esc</kbd></button>
</div>
{% endset %}
{{ code_preview("kbd-button", button_html | prettyHtml) }}
{{ code_preview("kbd-button", button_html | prettyHtml) }}
<h3 id="example-input-group"><a href="#example-input-group">Input Group</a></h3>
{% set input_group_html %}
<div class="relative w-full max-w-sm">
<input type="search" class="input ps-9 pe-12" placeholder="Search...">
{% lucide "search", { "class": "absolute start-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" } %}
<kbd class="kbd absolute end-3 top-1/2 -translate-y-1/2">/</kbd>
</div>
{% endset %}
{{ code_preview("kbd-input-group", input_group_html | prettyHtml) }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Keyboard labels are direction-neutral. Surrounding spacing should use logical utilities when position matters.</p>
</section>
{% set rtl_html %}
<p dir="rtl" class="text-muted-foreground text-sm">
استخدم
<span class="kbd-group">
<kbd class="kbd">Ctrl</kbd>
<kbd class="kbd">K</kbd>
</span>
لفتح البحث
</p>
{% endset %}
{{ code_preview("kbd-rtl", rtl_html | prettyHtml) }}
+17 -3
View File
@@ -10,6 +10,8 @@ toc:
children:
- label: Disabled
id: example-disabled
- label: RTL
id: example-rtl
---
{% from "macros/code_preview.njk" import code_preview %}
@@ -21,10 +23,10 @@ toc:
</label>{% endset %}
{{ code_preview("label", code) }}
<h2 id="usage">Usage</h2>
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Simply add the <code>label</code> class to your <code>&lt;label&gt;</code> element or have a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>). Labels' style may be affected by the presence of a disabled peer or child element.</p>
<p>Add the <code>label</code> class to your <code>&lt;label&gt;</code> element. Labels' style may be affected by the presence of a disabled peer or child element.</p>
</section>
{% set code %}<label class="label" for="email">Your email address</label>{% endset %}
@@ -38,4 +40,16 @@ toc:
<label class="label" for="email">Email</label>
<input class="input" id="email" type="email" placeholder="Email" disabled>
</div>{% endset %}
{{ code_preview("label-disabled", code) }}
{{ code_preview("label-disabled", code) }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Labels support right-to-left layouts through native text flow. Add <code>dir="rtl"</code> to the label or an ancestor.</p>
</section>
{% set code %}<div class="grid gap-3" dir="rtl">
<label class="label" for="email-rtl">البريد الإلكتروني</label>
<input class="input" id="email-rtl" type="email" placeholder="name@example.com">
</div>{% endset %}
{{ code_preview("label-rtl", code) }}
+127
View File
@@ -0,0 +1,127 @@
---
layout: layouts/page.njk
title: Native Select
description: A styled native HTML select element with consistent design system integration.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Groups
id: example-groups
- label: Disabled
id: example-disabled
- label: Invalid
id: example-invalid
- label: Size
id: example-size
- label: RTL
id: example-rtl
- label: Native Select vs Select
id: native-select-vs-select
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<select class="select w-[180px]" aria-label="Fruit">
<option value="">Select a fruit</option>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
<option value="blueberry">Blueberry</option>
<option value="pineapple">Pineapple</option>
</select>{% endset %}
{{ code_preview("native-select", code) }}
<section class="prose mt-6">
<p>For a custom listbox with richer styling and JavaScript behavior, see the <a href="/components/select">Select</a> component.</p>
</section>
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Add the <code>select</code> class to a native <code>&lt;select&gt;</code>. Use standard <code>&lt;option&gt;</code> and <code>&lt;optgroup&gt;</code> elements.</p>
</section>
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-groups"><a href="#example-groups">Groups</a></h3>
{% set code %}<select class="select w-[220px]" aria-label="Food">
<option value="">Select food</option>
<optgroup label="Fruits">
<option value="apple">Apple</option>
<option value="banana">Banana</option>
<option value="blueberry">Blueberry</option>
</optgroup>
<optgroup label="Vegetables">
<option value="carrot">Carrot</option>
<option value="potato">Potato</option>
<option value="tomato">Tomato</option>
</optgroup>
</select>{% endset %}
{{ code_preview("native-select-groups", code) }}
<h3 id="example-disabled"><a href="#example-disabled">Disabled</a></h3>
{% set code %}<select class="select w-[220px]" aria-label="Fruit" disabled>
<option value="">Select a fruit</option>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
</select>{% endset %}
{{ code_preview("native-select-disabled", code) }}
<h3 id="example-invalid"><a href="#example-invalid">Invalid</a></h3>
{% set code %}<div role="group" class="field" data-invalid="true">
<label for="native-select-invalid">Fruit</label>
<select id="native-select-invalid" class="select w-full" aria-invalid="true" aria-describedby="native-select-invalid-error">
<option value="">Select a fruit</option>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
</select>
<p id="native-select-invalid-error" class="field-error">Select a fruit to continue.</p>
</div>{% endset %}
{{ code_preview("native-select-invalid", code, "w-full max-w-sm") }}
<h3 id="example-size"><a href="#example-size">Size</a></h3>
{% set code %}<div class="flex flex-wrap items-center gap-3">
<select class="select w-[180px]" aria-label="Default size">
<option>Default</option>
<option>Apple</option>
<option>Banana</option>
</select>
<select class="select w-[180px]" data-size="sm" aria-label="Small size">
<option>Small</option>
<option>Apple</option>
<option>Banana</option>
</select>
</div>{% endset %}
{{ code_preview("native-select-size", code) }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Native select supports document direction. Set <code>dir="rtl"</code> on the select or a parent element.</p>
</section>
{% set code %}<select dir="rtl" class="select w-[220px]" aria-label="الفاكهة">
<option value="">اختر فاكهة</option>
<option value="apple">تفاح</option>
<option value="banana">موز</option>
<option value="blueberry">توت</option>
</select>{% endset %}
{{ code_preview("native-select-rtl", code) }}
<h2 id="native-select-vs-select"><a href="#native-select-vs-select">Native Select vs Select</a></h2>
<section class="prose">
<ul>
<li>Use <strong>Native Select</strong> for native browser behavior, better performance, form integration, and mobile-optimized pickers.</li>
<li>Use <strong>Select</strong> for a custom popover, richer option content, multi-select behavior, or custom JavaScript events.</li>
</ul>
</section>
+175 -66
View File
@@ -1,7 +1,7 @@
---
layout: layouts/page.njk
title: Popover
description: Displays rich content in a portal, triggered by a button.
description: Displays rich content triggered by a button.
toc:
- label: Usage
id: usage
@@ -9,7 +9,7 @@ toc:
- label: HTML + JavaScript
id: usage-html-js
children:
- label: "Step 1 (Optional): Include the JavaScript file"
- label: "Step 1: Include the JavaScript file"
id: usage-html-js-1
- label: "Step 2: Add your popover HTML"
id: usage-html-js-2
@@ -19,6 +19,19 @@ toc:
id: usage-html-js-4
- label: Jinja and Nunjucks
id: usage-macro
- label: Examples
id: examples
children:
- label: Basic
id: example-basic
- label: Align
id: example-align
- label: With Form
id: example-form
- label: RTL
id: example-rtl
- label: API Reference
id: api-reference
---
{% from "macros/code_preview.njk" import code_preview %}
@@ -30,39 +43,74 @@ toc:
id="demo-popover",
trigger="Open popover",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "w-80"}
popover_attrs={"class": "w-72"}
) %}
<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" autofocus/>
</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>
<header>
<h4>Dimensions</h4>
<p>Set the dimensions for the layer.</p>
</header>
<form class="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="input col-span-2 h-8" autofocus>
</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="input 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="input 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="input col-span-2 h-8">
</div>
</form>
{% endcall %}
{% endset %}
{{ code_preview("popover", code_html | prettyHtml) }}
{% set code_form %}
{% call popover(
id="popover-form",
trigger="Open popover",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "w-72"}
) %}
<header>
<h4>Dimensions</h4>
<p>Set the dimensions for the layer.</p>
</header>
<form class="grid gap-2">
<div class="grid grid-cols-3 items-center gap-4">
<label for="popover-form-width">Width</label>
<input type="text" id="popover-form-width" value="100%" class="input col-span-2 h-8" autofocus>
</div>
<div class="grid grid-cols-3 items-center gap-4">
<label for="popover-form-max-width">Max. width</label>
<input type="text" id="popover-form-max-width" value="300px" class="input col-span-2 h-8">
</div>
<div class="grid grid-cols-3 items-center gap-4">
<label for="popover-form-height">Height</label>
<input type="text" id="popover-form-height" value="25px" class="input col-span-2 h-8">
</div>
<div class="grid grid-cols-3 items-center gap-4">
<label for="popover-form-max-height">Max. height</label>
<input type="text" id="popover-form-max-height" value="none" class="input col-span-2 h-8">
</div>
</form>
{% endcall %}
{% endset %}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Basecoat popovers are inline-positioned relative to the <code>.popover</code> wrapper. This differs from shadcn/ui's portalled Base UI implementation, but keeps the markup dependency-free and works with Basecoat's existing dropdown/select positioning model.</p>
</section>
<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 file</a></h4>
@@ -80,10 +128,14 @@ toc:
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
<a class="badge-outline" href="/cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/js/popover.js" target="_blank">
popover.js
{% lucide "arrow-right" %}
</a>
</div>
<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add your popover HTML</a></h4>
@@ -94,17 +146,20 @@ toc:
<section class="prose">
<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-expanded="false"</code>: tracks the popover's state.</li>
<li><code>onclick</code>: a simple function to update <code>aria-expanded</code>.</li>
</ul>
<dt><code class="highlight language-html">&lt;div class="popover"&gt;</code></dt>
<dd>Relative wrapper for the trigger and inline popover content.
<dl>
<dt><code class="highlight language-html">&lt;button type="button" aria-expanded="false" aria-controls="{ POPOVER_ID }"&gt;</code></dt>
<dd>Trigger button. The script toggles <code>aria-expanded</code> and opens/closes the content.</dd>
<dt><code class="highlight language-html">&lt;div data-popover id="{ POPOVER_ID }" aria-hidden="true"&gt;</code></dt>
<dd>Popover content. Set <code>data-side="top|right|bottom|left"</code> and <code>data-align="start|center|end"</code> to control placement.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Header area. A heading inside the header is styled as the popover title, and a paragraph is styled as the description.</dd>
</dl>
</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;div data-popover class="popover" id="{ POPOVER_ID }"&gt;</code></dt>
<dd>You can set up the side and alignment of the popover using the <code>data-side</code> and <code>data-align</code> attributes.</dd>
</dl>
</section>
@@ -113,9 +168,9 @@ toc:
<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>
<dd>Once the component is 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>
<dd>When the popover opens, the component dispatches a custom event on <code>document</code>. Other popover-based components listen for this to close any open popovers.</dd>
</dl>
</section>
@@ -126,7 +181,7 @@ toc:
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
<a class="badge-outline" href="/cli#macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
@@ -144,31 +199,85 @@ toc:
id="demo-popover",
trigger="Open popover",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "w-80 p-4"}
popover_attrs={"class": "w-72", "data-align": "center"}
) %}
<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>
<header>
<h4>Title</h4>
<p>Description text here.</p>
</header>
{% endcall %}{% endraw %}{% endset %}
{{ code_block(raw_code, "jinja") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-basic"><a href="#example-basic">Basic</a></h3>
{% set code %}
{% call popover(
id="popover-basic",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "w-72"}
) %}
<header>
<h4>Title</h4>
<p>Description text here.</p>
</header>
{% endcall %}
{% endset %}
{{ code_preview("popover-basic", code | prettyHtml) }}
<h3 id="example-align"><a href="#example-align">Align</a></h3>
{% set code %}<div class="flex flex-wrap gap-3">
{% call popover(id="popover-align-start", trigger="Start", trigger_attrs={"class": "btn-outline"}, popover_attrs={"class": "w-56", "data-align": "start"}) %}
<p>Aligned to start.</p>
{% endcall %}
{% call popover(id="popover-align-center", trigger="Center", trigger_attrs={"class": "btn-outline"}, popover_attrs={"class": "w-56", "data-align": "center"}) %}
<p>Aligned to center.</p>
{% endcall %}
{% call popover(id="popover-align-end", trigger="End", trigger_attrs={"class": "btn-outline"}, popover_attrs={"class": "w-56", "data-align": "end"}) %}
<p>Aligned to end.</p>
{% endcall %}
</div>{% endset %}
{{ code_preview("popover-align", code | prettyHtml) }}
<h3 id="example-form"><a href="#example-form">With Form</a></h3>
{{ code_preview("popover-form", code_form | prettyHtml) }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Popover alignment uses logical start and end offsets. Set <code>dir="rtl"</code> on a parent to flip start/end alignment.</p>
</section>
{% set code %}<div dir="rtl">
{% call popover(
id="popover-rtl",
trigger="فتح",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "w-72", "data-align": "start"}
) %}
<header>
<h4>العنوان</h4>
<p>نص الوصف هنا.</p>
</header>
{% endcall %}
</div>{% endset %}
{{ code_preview("popover-rtl", code | prettyHtml) }}
<h2 id="api-reference"><a href="#api-reference">API Reference</a></h2>
<section class="prose">
<dl>
<dt><code class="highlight language-html">.popover</code></dt>
<dd>Root wrapper. Must contain the trigger button and direct <code>[data-popover]</code> content.</dd>
<dt><code class="highlight language-html">[data-popover]</code></dt>
<dd>Popover content. Supports <code>data-side</code>, <code>data-align</code>, and <code>aria-hidden</code>.</dd>
<dt><code class="highlight language-html">data-side</code></dt>
<dd>Placement side: <code>top</code>, <code>right</code>, <code>bottom</code>, or <code>left</code>. Defaults to <code>bottom</code>.</dd>
<dt><code class="highlight language-html">data-align</code></dt>
<dd>Alignment: <code>start</code>, <code>center</code>, or <code>end</code>. Defaults to <code>start</code>.</dd>
</dl>
</section>
+193 -22
View File
@@ -1,48 +1,219 @@
---
layout: layouts/page.njk
title: Radio Group
description: A set of checkable buttons—known as radio buttons—where no more than one of the buttons can be checked at a time.
description: A set of checkable buttons where only one option can be selected at a time.
toc:
- label: Usage
id: usage
- label: Checked state
id: checked-state
- label: Invalid state
id: invalid-state
- label: Examples
id: examples
children:
- label: Form
id: example-form
- label: Basic
id: example-basic
- label: Description
id: example-description
- label: Choice Card
id: example-choice-card
- label: Fieldset
id: example-fieldset
- label: Disabled
id: example-disabled
- label: Invalid
id: example-invalid
- label: RTL
id: example-rtl
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<fieldset class="grid gap-3">
<label class="label"><input type="radio" name="radio-group" value="default" class="input">Default</label>
<label class="label"><input type="radio" name="radio-group" value="comfortable" class="input" checked>Comfortable</label>
<label class="label"><input type="radio" name="radio-group" value="compact" class="input">Compact</label>
{% set code %}<fieldset class="fieldset">
<legend>Plan</legend>
<div role="radiogroup" aria-label="Plan" data-slot="radio-group">
<div role="group" class="field flex-row items-center gap-3">
<input type="radio" id="plan-free" name="plan" value="free" class="input" checked>
<label for="plan-free">Free</label>
</div>
<div role="group" class="field flex-row items-center gap-3">
<input type="radio" id="plan-pro" name="plan" value="pro" class="input">
<label for="plan-pro">Pro</label>
</div>
</div>
</fieldset>{% endset %}
{{ code_preview("radio-group", code) }}
{{ code_preview("radio-group", code, "w-full max-w-sm") }}
<h2 id="usage">Usage</h2>
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Simply 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>
<p>Add the <code>input</code> class to your <code>&lt;input type="radio"&gt;</code> elements. Use a shared <code>name</code> attribute for mutually exclusive options, and wrap related options in a semantic <code>&lt;fieldset&gt;</code> or an element with <code>role="radiogroup"</code>.</p>
</section>
{% set code %}<input type="radio" name="plan" class="input">{% endset %}
{{ code_block(code) }}
<h2 id="checked-state"><a href="#checked-state">Checked state</a></h2>
<section class="prose">
<p>Use native HTML state. Add <code>checked</code> for the initially selected option, or read and update the element's <code>checked</code> property in JavaScript.</p>
</section>
{% set code %}<input type="radio" name="plan" class="input" checked>{% endset %}
{{ code_block(code) }}
<h2 id="invalid-state"><a href="#invalid-state">Invalid state</a></h2>
<section class="prose">
<p>Set <code>aria-invalid="true"</code> on the invalid radio and, when using <code>field</code>, set <code>data-invalid="true"</code> on the field wrapper.</p>
</section>
{% set code %}<div role="group" class="field flex-row items-start gap-3" data-invalid="true">
<input type="radio" id="radio-invalid" name="radio-invalid" class="input" aria-invalid="true">
<div class="grid gap-1.5">
<label for="radio-invalid">I agree to the terms</label>
<p class="field-error">You must select an option to continue.</p>
</div>
</div>{% endset %}
{{ code_preview("radio-invalid-state", code, "w-full max-w-sm") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-form"><a href="#example-form">Form</a></h3>
<h3 id="example-basic"><a href="#example-basic">Basic</a></h3>
{% set code %}<form class="form space-y-6 w-full">
<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>
{% set code %}<div role="radiogroup" aria-label="View density" data-slot="radio-group">
<div role="group" class="field flex-row items-center gap-3">
<input type="radio" id="density-default" name="density" value="default" class="input">
<label for="density-default">Default</label>
</div>
<button type="submit" class="btn">Submit</button>
</form>{% endset %}
{{ code_preview("radio-group-form", code, "w-full max-w-sm") }}
<div role="group" class="field flex-row items-center gap-3">
<input type="radio" id="density-comfortable" name="density" value="comfortable" class="input" checked>
<label for="density-comfortable">Comfortable</label>
</div>
<div role="group" class="field flex-row items-center gap-3">
<input type="radio" id="density-compact" name="density" value="compact" class="input">
<label for="density-compact">Compact</label>
</div>
</div>{% endset %}
{{ code_preview("radio-basic", code, "w-full max-w-sm") }}
<h3 id="example-description"><a href="#example-description">Description</a></h3>
{% set code %}<div role="radiogroup" aria-label="Notifications" data-slot="radio-group">
<div role="group" class="field flex-row items-start gap-3">
<input type="radio" id="notify-all" name="notifications" value="all" class="input" checked aria-describedby="notify-all-description">
<div class="grid gap-1.5">
<label for="notify-all">All new messages</label>
<p id="notify-all-description" class="field-description">Receive a notification for every new message.</p>
</div>
</div>
<div role="group" class="field flex-row items-start gap-3">
<input type="radio" id="notify-mentions" name="notifications" value="mentions" class="input" aria-describedby="notify-mentions-description">
<div class="grid gap-1.5">
<label for="notify-mentions">Direct messages and mentions</label>
<p id="notify-mentions-description" class="field-description">Only notify me for direct messages and mentions.</p>
</div>
</div>
</div>{% endset %}
{{ code_preview("radio-description", code, "w-full max-w-sm") }}
<h3 id="example-choice-card"><a href="#example-choice-card">Choice Card</a></h3>
<section class="prose">
<p>Wrap a <code>.field</code> in a native <code>&lt;label&gt;</code> when the whole card should select the option.</p>
</section>
{% set code %}<div role="radiogroup" aria-label="Plans" data-slot="radio-group" class="w-full max-w-md">
<label>
<div role="group" class="field flex-row items-center justify-between">
<div class="grid gap-1.5">
<span>Starter</span>
<span class="field-description">For small teams getting started.</span>
</div>
<input type="radio" name="choice-plan" value="starter" class="input" checked>
</div>
</label>
<label>
<div role="group" class="field flex-row items-center justify-between">
<div class="grid gap-1.5">
<span>Team</span>
<span class="field-description">Collaboration tools for growing teams.</span>
</div>
<input type="radio" name="choice-plan" value="team" class="input">
</div>
</label>
</div>{% endset %}
{{ code_preview("radio-choice-card", code, "w-full") }}
<h3 id="example-fieldset"><a href="#example-fieldset">Fieldset</a></h3>
{% set code %}<fieldset class="fieldset">
<legend>Notify me about...</legend>
<div role="radiogroup" aria-label="Notification preference" data-slot="radio-group">
<div role="group" class="field flex-row items-center gap-3">
<input class="input" type="radio" id="notify-all-fieldset" name="notify-fieldset" value="all" checked>
<label for="notify-all-fieldset">All new messages</label>
</div>
<div role="group" class="field flex-row items-center gap-3">
<input class="input" type="radio" id="notify-mentions-fieldset" name="notify-fieldset" value="mentions">
<label for="notify-mentions-fieldset">Direct messages and mentions</label>
</div>
<div role="group" class="field flex-row items-center gap-3">
<input class="input" type="radio" id="notify-none-fieldset" name="notify-fieldset" value="none">
<label for="notify-none-fieldset">Nothing</label>
</div>
</div>
</fieldset>{% endset %}
{{ code_preview("radio-fieldset", code, "w-full max-w-sm") }}
<h3 id="example-disabled"><a href="#example-disabled">Disabled</a></h3>
{% set code %}<div role="radiogroup" aria-label="Disabled notifications" data-slot="radio-group">
<div role="group" class="field flex-row items-center gap-3" data-disabled="true">
<input type="radio" id="radio-disabled" name="radio-disabled" class="input" disabled>
<label for="radio-disabled">Disabled option</label>
</div>
<div role="group" class="field flex-row items-center gap-3" data-disabled="true">
<input type="radio" id="radio-disabled-checked" name="radio-disabled" class="input" checked disabled>
<label for="radio-disabled-checked">Disabled checked option</label>
</div>
</div>{% endset %}
{{ code_preview("radio-disabled", code, "w-full max-w-sm") }}
<h3 id="example-invalid"><a href="#example-invalid">Invalid</a></h3>
{% set code %}<fieldset class="fieldset" data-invalid="true">
<legend>Plan</legend>
<div role="radiogroup" aria-label="Invalid plan" data-slot="radio-group" aria-describedby="radio-group-error">
<div role="group" class="field flex-row items-center gap-3" data-invalid="true">
<input type="radio" id="radio-invalid-free" name="radio-invalid-plan" class="input" aria-invalid="true">
<label for="radio-invalid-free">Free</label>
</div>
<div role="group" class="field flex-row items-center gap-3" data-invalid="true">
<input type="radio" id="radio-invalid-pro" name="radio-invalid-plan" class="input" aria-invalid="true">
<label for="radio-invalid-pro">Pro</label>
</div>
</div>
<p id="radio-group-error" class="field-error">Select a plan to continue.</p>
</fieldset>{% endset %}
{{ code_preview("radio-invalid", code, "w-full max-w-sm") }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Radio group layout uses document direction and logical spacing. Set <code>dir="rtl"</code> on a parent to render right-to-left.</p>
</section>
{% set code %}<div dir="rtl" role="radiogroup" aria-label="طريقة العرض" data-slot="radio-group">
<div role="group" class="field flex-row items-center gap-3">
<input type="radio" id="radio-rtl-default" name="radio-rtl" value="default" class="input" checked>
<label for="radio-rtl-default">افتراضي</label>
</div>
<div role="group" class="field flex-row items-center gap-3">
<input type="radio" id="radio-rtl-comfortable" name="radio-rtl" value="comfortable" class="input">
<label for="radio-rtl-comfortable">مريح</label>
</div>
</div>{% endset %}
{{ code_preview("radio-rtl", code, "w-full max-w-sm") }}
+113 -44
View File
@@ -6,8 +6,6 @@ toc:
- label: Usage
id: usage
children:
- label: HTML
id: usage-html
- label: HTML + JavaScript
id: usage-html-js
children:
@@ -26,45 +24,40 @@ toc:
- label: Examples
id: examples
children:
- label: Align
id: example-align
- label: Groups
id: example-groups
- label: Scrollable
id: example-scrollable
- label: Disabled
id: example-disabled
- label: Invalid
id: example-invalid
- label: With icon
id: example-with-icon
- label: Multiple
id: example-multiple
- label: RTL
id: example-rtl
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "select.njk" import select %}
{% set code_native %}<select class="select w-[180px]">
<optgroup label="Fruits">
<option>Apple</option>
<option>Banana</option>
<option>Blueberry</option>
<option>Grapes</option>
<option>Pineapple</option>
</optgroup>
</select>{% endset %}
{{ code_preview("select-native", code_native) }}
{% set code_html %}
{{ select(
trigger_attrs={"class": "w-[180px]"},
is_combobox=true,
items=[
{
type: "group",
label: "Fruits",
label: "Theme",
items: [
{ type: "item", value: "apple", label: "Apple" },
{ type: "item", value: "banana", label: "Banana" },
{ type: "item", value: "blueberry", label: "Blueberry" },
{ type: "item", value: "grapes", label: "Grapes" },
{ type: "item", value: "pineapple", label: "Pineapple" }
{ type: "item", value: "light", label: "Light" },
{ type: "item", value: "dark", label: "Dark" },
{ type: "item", value: "system", label: "System" }
]
}
]
@@ -78,13 +71,6 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-html"><a href="#usage-html">HTML</a></h3>
<section class="prose">
<p>If you use a <code>&lt;select&gt;</code> element, just add the <code>select</code> class to it or have a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>).</p>
</section>
{{ code_block(code_native) }}
<h3 id="usage-html-js"><a href="#usage-html-js">HTML + JavaScript</a></h3>
@@ -103,7 +89,7 @@ toc:
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
<a class="badge-outline" href="/cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
@@ -306,7 +292,7 @@ toc:
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
<a class="badge-outline" href="/cli#macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
@@ -339,6 +325,62 @@ toc:
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-align"><a href="#example-align">Align</a></h3>
<section class="prose">
<p>Use <code>data-align</code> on the popover to align the list to the trigger edge. Basecoat keeps the popup below the control; it does not implement shadcn/ui's selected-item-over-trigger positioning.</p>
</section>
{% set code %}
<div class="flex flex-wrap gap-3">
{{ select(
id="select-align-start",
trigger_attrs={"class": "w-[180px]"},
popover_attrs={"data-align": "start"},
items=[
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
{ value: "system", label: "System" }
]
) }}
{{ select(
id="select-align-end",
trigger_attrs={"class": "w-[180px]"},
popover_attrs={"data-align": "end"},
items=[
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
{ value: "system", label: "System" }
]
) }}
</div>
{% endset %}
{{ code_preview("select-align", code | prettyHtml) }}
<h3 id="example-groups"><a href="#example-groups">Groups</a></h3>
{% set code %}
{% call select(
id="select-groups",
trigger_attrs={"class": "w-[220px]"}
) %}
<div role="group" aria-labelledby="select-groups-fruits">
<div role="heading" id="select-groups-fruits">Fruits</div>
<div role="option" data-value="apple">Apple</div>
<div role="option" data-value="banana">Banana</div>
<div role="option" data-value="blueberry">Blueberry</div>
</div>
<hr role="separator">
<div role="group" aria-labelledby="select-groups-vegetables">
<div role="heading" id="select-groups-vegetables">Vegetables</div>
<div role="option" data-value="carrot">Carrot</div>
<div role="option" data-value="potato">Potato</div>
<div role="option" data-value="tomato">Tomato</div>
</div>
{% endcall %}
{% endset %}
{{ code_preview("select-groups", code | prettyHtml) }}
<h3 id="example-scrollable"><a href="#example-scrollable">Scrollable</a></h3>
{% set code %}
@@ -365,22 +407,27 @@ toc:
{{ code_preview("select-disabled", code | prettyHtml) }}
<h3 id="example-invalid-state"><a href="#example-invalid-state">Invalid State</a></h3>
<h3 id="example-invalid"><a href="#example-invalid">Invalid</a></h3>
{% set code %}
{{ select(
trigger_attrs={"aria-invalid": "true"},
items=[
{ value: "", label: "Select a role" },
{ value: "admin", label: "Admin" },
{ value: "editor", label: "Editor" },
{ value: "viewer", label: "Viewer" },
{ value: "guest", label: "Guest" }
]
) }}
<div role="group" class="field" data-invalid="true">
<label for="select-invalid-trigger">Role</label>
{{ select(
id="select-invalid",
trigger_attrs={"aria-invalid": "true", "class": "w-[220px]"},
items=[
{ value: "", label: "Select a role" },
{ value: "admin", label: "Admin" },
{ value: "editor", label: "Editor" },
{ value: "viewer", label: "Viewer" },
{ value: "guest", label: "Guest" }
]
) }}
<p class="field-error" role="alert">Select a role to continue.</p>
</div>
{% endset %}
{{ code_preview("select-disabled", code | prettyHtml) }}
{{ code_preview("select-invalid", code | prettyHtml, "w-full max-w-sm") }}
<h3 id="example-with-icon"><a href="#example-with-icon">With icon</a></h3>
@@ -389,19 +436,19 @@ toc:
id="select-with-icons",
trigger_attrs={'class': 'w-[180px]'}
) %}
<div type="button" role="option" data-value="bar">
<div 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">
<div 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">
<div role="option" data-value="pie">
<span class="flex items-center gap-2">
{% lucide "chart-pie", {"class": "text-muted-foreground"} %}
Pie
@@ -415,7 +462,7 @@ toc:
<h3 id="example-multiple"><a href="#example-multiple">Multiple</a></h3>
<section class="prose">
<p>Enable multiple selection by setting adding <code>aria-multiselectable="true"</code> to the <code>listbox</code>. The selected options are displayed as a comma-separated list in the trigger button. Use <code>data-label</code> to specify clean text labels for options that contain HTML (like icons), otherwise the text content will be extracted automatically.</p>
<p>Enable multiple selection by adding <code>aria-multiselectable="true"</code> to the <code>listbox</code>. The selected options are displayed as a comma-separated list in the trigger button. Use <code>data-label</code> to specify clean text labels for options that contain HTML (like icons), otherwise the text content will be extracted automatically.</p>
</section>
{% set code %}
@@ -455,3 +502,25 @@ toc:
{% endset %}
{{ code_preview("select-multiple", code | prettyHtml) }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Set <code>dir="rtl"</code> on the select root or a parent element.</p>
</section>
{% set code %}
<div dir="rtl">
{{ select(
id="select-rtl",
trigger_attrs={"class": "w-[180px]"},
popover_attrs={"data-align": "start"},
items=[
{ value: "light", label: "فاتح" },
{ value: "dark", label: "داكن" },
{ value: "system", label: "النظام" }
]
) }}
</div>
{% endset %}
{{ code_preview("select-rtl", code | prettyHtml) }}
+44 -62
View File
@@ -15,8 +15,10 @@ toc:
id: usage-html-js-2
- label: HTML structure
id: usage-html-js-3
- label: JavaScript events
- label: Basecoat differences
id: usage-html-js-4
- label: JavaScript events
id: usage-html-js-5
- label: Jinja and Nunjucks
id: usage-macro
---
@@ -43,7 +45,7 @@ toc:
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
<a class="badge-outline" href="/cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
@@ -90,80 +92,60 @@ toc:
<section class="prose">
<dl>
<dt><code class="highlight language-html">&lt;aside class="sidebar" aria-hidden="false"&gt;</code></dt>
<dd>Wraps around the entire component. It can have the following attributes:
<dd>Wraps the sidebar component. Supported attributes:
<ul>
<li><code>aria-hidden="true"</code>: controls the default state of the sidebar (hidden or visible).</li>
<li><code>data-side="left"</code>: specifies the side of the sidebar (<code>left</code> or <code>right</code>, defaults to <code>left</code>).</li>
<li><code>id</code>: target a specific sidebar from the <code>basecoat:sidebar</code> event.</li>
<li><code>aria-hidden</code>: current open state. <code>false</code> means visible, <code>true</code> means hidden.</li>
<li><code>data-side</code>: physical side of the viewport. Use <code>left</code> or <code>right</code>. Defaults to <code>left</code>.</li>
<li><code>data-initial-open</code>: set to <code>false</code> to start closed on desktop.</li>
<li><code>data-initial-mobile-open</code>: set to <code>true</code> to start open below the breakpoint.</li>
<li><code>data-breakpoint</code>: pixel width used to switch between desktop and mobile behavior. Defaults to <code>768</code>.</li>
</ul>
</dd>
<dl>
<dt><code class="highlight language-html">&lt;nav&gt;</code></dt>
<dd>
<p>The navigation element that contains the sidebar's content. It can 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>
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The header of the sidebar.</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code></dt>
<dd>The main navigation list.
<dl>
<dt><code class="highlight language-html">&lt;div role="group"&gt;</code></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>
<dt><code class="highlight language-html">&lt;ul&gt;</code></dt>
<dd>List of links or buttons.
<dl>
<dt><code class="highlight language-html">&lt;li&gt;</code></dt>
<dd>Individual item.
<dl>
<dt><code class="highlight language-html">&lt;a&gt;</code></dt>
<dd>A link. By default, clicking on a link will close the sidebar on mobile unless the <code>data-keep-mobile-sidebar-open</code> attribute is present. Set <code>aria-current="page"</code> on the active link.</dd>
<dt><code class="highlight language-html">&lt;button&gt;</code></dt>
<dd>A button. By default, clicking on a button will close the sidebar on mobile unless the <code>data-keep-mobile-sidebar-open</code> attribute is present. If your app uses buttons for navigation, set <code>aria-current="page"</code> on the active item.</dd>
<dt><code class="highlight language-html">&lt;details&gt;</code></dt>
<dd>Collapsible section.
<dl>
<dt><code class="highlight language-html">&lt;summary&gt;</code></dt>
<dd>Summary of the collapsible section.</dd>
<dt><code class="highlight language-html">&lt;ul&gt;</code></dt>
<dd>List of links or buttons.</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The footer of the sidebar.</dd>
</dl>
</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;nav aria-label="..."&gt;</code></dt>
<dd>The semantic navigation landmark inside the sidebar.</dd>
<dt><code class="highlight language-html">&lt;header&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Top area for branding, workspace switchers, or primary controls.</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code></dt>
<dd>Scrollable sidebar content.</dd>
<dt><code class="highlight language-html">&lt;div role="group" aria-labelledby="..."&gt;</code></dt>
<dd>A grouped set of navigation items. Use an <code>h3</code> with a matching <code>id</code> when the group has a visible label.</dd>
<dt><code class="highlight language-html">&lt;ul&gt;</code> / <code class="highlight language-html">&lt;li&gt;</code></dt>
<dd>Navigation list structure. Items may contain an <code>a</code>, a <code>button</code>, or a <code>details</code> submenu.</dd>
<dt><code class="highlight language-html">&lt;a aria-current="page"&gt;</code></dt>
<dd>Use <code>aria-current="page"</code> for the active link. You can also use <code>data-active="true"</code> for non-link active items.</dd>
<dt><code class="highlight language-html">&lt;button type="button"&gt;</code></dt>
<dd>Use buttons for actions. On mobile, links and buttons close the sidebar unless they include <code>data-keep-mobile-sidebar-open</code>.</dd>
<dt><code class="highlight language-html">&lt;details&gt;</code> / <code class="highlight language-html">&lt;summary&gt;</code></dt>
<dd>Use native disclosure for submenus. The nested <code>ul</code> contains the submenu items.</dd>
<dt><code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Bottom area for account controls, settings, or secondary actions.</dd>
<dt><code class="highlight language-html">&lt;main&gt;</code></dt>
<dd>A wrapper for the content of the page.</dd>
<dd>The sibling content wrapper. Basecoat applies the desktop margin to the sibling that follows the sidebar.</dd>
<dt><code class="highlight language-html">&lt;button type="button" onclick="document.dispatchEvent(new CustomEvent('basecoat:sidebar'))"&gt;</code></dt>
<dd>A button to toggle the sidebar. If you want to use multiple sidebars you will need to add unique ids to the sidebars (i.e. <code>&lt;aside class="sidebar" id="{SIDEBAR_ID}"&gt;</code>) and refer to them in the event <code>detail</code> (i.e. <code>document.dispatchEvent(new CustomEvent('basecoat:sidebar', { detail: { id: '{SIDEBAR_ID}' } }));</code>).</dd>
<dd>Dispatches the event used to toggle, open, or close the sidebar.</dd>
</dl>
</section>
{% set code_current %}<!-- Server-side or router-driven current link -->
<nav>
{% set code_current %}<!-- Server-side or router-driven current item -->
<nav aria-label="Sidebar navigation">
<ul>
<li><a href="/my-path?page=overview">Overview</a></li>
<li><a href="/my-path?page=settings" aria-current="page">Settings</a></li>
<li><button type="button" data-active="true">Active action</button></li>
</ul>
</nav>{% endset %}
{{ code_block(code_current | prettyHtml, "html") }}
<h4 id="usage-html-js-4"><a href="#usage-html-js-4">JavaScript events</a></h4>
<h4 id="usage-html-js-4"><a href="#usage-html-js-4">Basecoat differences</a></h4>
<section class="prose">
<p>Basecoat keeps sidebar intentionally smaller than shadcn/ui. It supports fixed left/right sidebars, mobile overlay behavior, grouped navigation, active states, and native <code>details</code> submenus.</p>
<p>It does not currently expose shadcn/ui's React provider, rail, inset/floating variants, icon-only collapse mode, menu actions, menu badges, or menu skeleton API. Those can be composed manually if needed, but they are not part of the Basecoat sidebar contract.</p>
<p>RTL content is supported through logical spacing and borders. The <code>data-side</code> value remains physical: <code>left</code> means the left side of the viewport and <code>right</code> means the right side.</p>
</section>
<h4 id="usage-html-js-5"><a href="#usage-html-js-5">JavaScript events</a></h4>
<section class="prose">
<dl>
@@ -190,7 +172,7 @@ toc:
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
<a class="badge-outline" href="/cli#macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
+71 -12
View File
@@ -8,44 +8,103 @@ toc:
- label: Examples
id: examples
children:
- label: Avatar
id: example-avatar
- label: Card
id: example-card
- label: Text
id: example-text
- label: Form
id: example-form
- label: Table
id: example-table
- label: RTL
id: example-rtl
---
{% 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 skeleton component in Basecoat.</h2>
</div>
{% set code %}<div class="flex items-center gap-4">
<div class="bg-accent animate-pulse size-10 shrink-0 rounded-full"></div>
<div class="skeleton 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 class="skeleton h-4 w-[150px]"></div>
<div class="skeleton h-4 w-[100px]"></div>
</div>
</div>{% endset %}
{{ code_preview("skeleton", code | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<p class="prose">Simply use the <code>animate-pulse</code> class to create a skeleton.</p>
<p class="prose">Use the <code>skeleton</code> class with sizing utilities to create loading placeholders.</p>
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-avatar"><a href="#example-avatar">Avatar</a></h3>
{% set code %}<div class="flex items-center gap-4">
<div class="skeleton size-12 rounded-full"></div>
<div class="grid gap-2">
<div class="skeleton h-4 w-32"></div>
<div class="skeleton h-4 w-48"></div>
</div>
</div>{% endset %}
{{ code_preview("skeleton-avatar", code | prettyHtml) }}
<h3 id="example-card"><a href="#example-card">Card</a></h3>
{% set code %}<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>
<div class="skeleton h-4 w-2/3"></div>
<div class="skeleton h-4 w-1/2"></div>
</header>
<section>
<div class="bg-accent animate-pulse rounded-md aspect-square w-full"></div>
<div class="skeleton aspect-square w-full"></div>
</section>
</div>{% endset %}
{{ code_preview("skeleton-card", code | prettyHtml, "w-full max-w-md") }}
<h3 id="example-text"><a href="#example-text">Text</a></h3>
{% set code %}<div class="grid gap-2">
<div class="skeleton h-4 w-full"></div>
<div class="skeleton h-4 w-5/6"></div>
<div class="skeleton h-4 w-2/3"></div>
</div>{% endset %}
{{ code_preview("skeleton-text", code | prettyHtml, "w-full max-w-md") }}
<h3 id="example-form"><a href="#example-form">Form</a></h3>
{% set code %}<div class="grid gap-4 w-full max-w-sm">
<div class="skeleton h-4 w-24"></div>
<div class="skeleton h-9 w-full"></div>
<div class="skeleton h-4 w-32"></div>
<div class="skeleton h-9 w-full"></div>
</div>{% endset %}
{{ code_preview("skeleton-form", code | prettyHtml, "w-full max-w-md") }}
<h3 id="example-table"><a href="#example-table">Table</a></h3>
{% set code %}<div class="grid gap-3 w-full">
<div class="skeleton h-8 w-full"></div>
<div class="skeleton h-8 w-full"></div>
<div class="skeleton h-8 w-full"></div>
</div>{% endset %}
{{ code_preview("skeleton-table", code | prettyHtml, "w-full max-w-md") }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Skeleton blocks are direction-neutral. Use logical layout utilities around them when spacing depends on direction.</p>
</section>
{% set code %}<div dir="rtl" class="flex items-center gap-4">
<div class="skeleton size-10 shrink-0 rounded-full"></div>
<div class="grid gap-2">
<div class="skeleton h-4 w-[150px]"></div>
<div class="skeleton h-4 w-[100px]"></div>
</div>
</div>{% endset %}
{{ code_preview("skeleton-rtl", code | prettyHtml) }}
+84 -22
View File
@@ -5,6 +5,22 @@ description: An input where the user selects a value from within a given range.
toc:
- label: Usage
id: usage
children:
- label: HTML + JavaScript
id: usage-html-js
- label: HTML structure
id: usage-structure
- label: Examples
id: examples
children:
- label: Default
id: example-default
- label: Controlled Label
id: example-controlled-label
- label: Disabled
id: example-disabled
- label: RTL
id: example-rtl
---
{% from "macros/code_preview.njk" import code_preview %}
@@ -16,32 +32,78 @@ toc:
min="0"
max="100"
value="50"
>
<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>{% endset %}
>{% endset %}
{{ code_preview("slider", code, "w-full max-w-sm") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-html-js"><a href="#usage-html-js">HTML + JavaScript</a></h3>
<section class="prose">
<p>You can style an <code>&lt;input type="range"&gt;</code> element by either adding the <code>input</code> class or having a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>). However, the range input also requries some Javacript code to be able to paint the left side of the slider.</p>
<p>Use a native <code>&lt;input type="range"&gt;</code> with the <code>input</code> class. Include <code>range.js</code> so Basecoat can keep the filled track in sync with the input value.</p>
</section>
{{ code_block(code) }}
{% 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/range.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="https://github.com/hunvreus/basecoat/blob/main/src/js/range.js" target="_blank">
range.js
{% lucide "arrow-right" %}
</a>
</div>
{{ code_block(code | prettyHtml, "html") }}
<h3 id="usage-structure"><a href="#usage-structure">HTML structure</a></h3>
<section class="prose">
<dl>
<dt><code class="highlight language-html">&lt;input type="range" class="input"&gt;</code></dt>
<dd>Native range input styled as a slider. Width utilities such as <code>w-full</code> can be added directly.</dd>
</dl>
<p>Basecoat intentionally uses the native range input. Unlike shadcn/ui's Base UI slider, it does not support multiple thumbs or vertical orientation.</p>
</section>
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-default"><a href="#example-default">Default</a></h3>
{% set code %}<input type="range" class="input w-full" min="0" max="100" value="50">{% endset %}
{{ code_preview("slider-default", code, "w-full max-w-sm") }}
<h3 id="example-controlled-label"><a href="#example-controlled-label">Controlled Label</a></h3>
{% set code %}<div class="grid w-full gap-3 max-w-sm">
<div class="flex items-center justify-between gap-2">
<label class="label" for="temperature">Temperature</label>
<output class="text-sm text-muted-foreground" for="temperature">50</output>
</div>
<input
id="temperature"
type="range"
class="input w-full"
min="0"
max="100"
value="50"
oninput="this.previousElementSibling.querySelector('output').value = this.value"
>
</div>{% endset %}
{{ code_preview("slider-controlled-label", code, "w-full") }}
<h3 id="example-disabled"><a href="#example-disabled">Disabled</a></h3>
{% set code %}<input type="range" class="input w-full" min="0" max="100" value="50" disabled>{% endset %}
{{ code_preview("slider-disabled", code, "w-full max-w-sm") }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
{% set code %}<div dir="rtl" class="w-full max-w-sm">
<input type="range" class="input w-full" min="0" max="100" value="50">
</div>{% endset %}
{{ code_preview("slider-rtl", code, "w-full") }}
+124 -24
View File
@@ -5,50 +5,150 @@ description: A control that allows the user to toggle between checked and not ch
toc:
- label: Usage
id: usage
- label: Checked state
id: checked-state
- label: Invalid state
id: invalid-state
- label: Examples
id: examples
children:
- label: Form
id: example-form
- label: Basic
id: example-basic
- label: Description
id: example-description
- label: Choice Card
id: example-choice-card
- label: Disabled
id: example-disabled
- label: Invalid
id: example-invalid
- label: Size
id: example-size
- label: RTL
id: example-rtl
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<label class="label">
<input type="checkbox" name="switch" role="switch" class="input">
Airplane Mode
</label>{% endset %}
{{ code_preview("switch", code) }}
{% set code %}<div role="group" class="field flex-row items-center justify-between">
<label for="airplane-mode">Airplane Mode</label>
<input type="checkbox" id="airplane-mode" role="switch" class="input">
</div>{% endset %}
{{ code_preview("switch", code, "w-full max-w-sm") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>You can style a <code>&lt;input type="checkbox"&gt;</code> element as a switch by adding a <code>role="switch"</code> attribute, and adding the <code>input</code> class or having a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>).</p>
<p>Add <code>role="switch"</code> and the <code>input</code> class to a native <code>&lt;input type="checkbox"&gt;</code>. Use a native <code>&lt;label&gt;</code> or <code>aria-label</code> for accessible naming.</p>
</section>
{% set code %}<input type="checkbox" role="switch" class="input">{% endset %}
{{ code_block(code) }}
<h2 id="checked-state"><a href="#checked-state">Checked state</a></h2>
<section class="prose">
<p>Use native HTML state. Add <code>checked</code> for an initially enabled switch, or read and update the element's <code>checked</code> property in JavaScript.</p>
</section>
{% set code %}<input type="checkbox" role="switch" class="input" checked>{% endset %}
{{ code_block(code) }}
<h2 id="invalid-state"><a href="#invalid-state">Invalid state</a></h2>
<section class="prose">
<p>Set <code>aria-invalid="true"</code> on the switch and, when using <code>field</code>, set <code>data-invalid="true"</code> on the field wrapper.</p>
</section>
{% set code %}<div role="group" class="field flex-row items-start justify-between gap-3" data-invalid="true">
<div class="grid gap-1.5">
<label for="switch-invalid-state">Marketing emails</label>
<p class="field-error">Choose whether marketing emails are enabled.</p>
</div>
<input type="checkbox" id="switch-invalid-state" role="switch" class="input" aria-invalid="true">
</div>{% endset %}
{{ code_preview("switch-invalid-state", code, "w-full max-w-sm") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-form"><a href="#example-form">Form</a></h3>
<h3 id="example-basic"><a href="#example-basic">Basic</a></h3>
{% set code %}<form class="form 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">
{% set code %}<div role="group" class="field flex-row items-center justify-between">
<label for="switch-basic">Airplane Mode</label>
<input type="checkbox" id="switch-basic" role="switch" class="input">
</div>{% endset %}
{{ code_preview("switch-basic", code, "w-full max-w-sm") }}
<h3 id="example-description"><a href="#example-description">Description</a></h3>
{% set code %}<div role="group" class="field flex-row items-start justify-between gap-3">
<div class="grid gap-1.5">
<label for="switch-description">Marketing emails</label>
<p id="switch-description-text" class="field-description">Receive emails about new products, features, and more.</p>
</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>
<input type="checkbox" id="switch-description" role="switch" class="input" aria-describedby="switch-description-text" checked>
</div>{% endset %}
{{ code_preview("switch-description", code, "w-full max-w-sm") }}
<h3 id="example-choice-card"><a href="#example-choice-card">Choice Card</a></h3>
<section class="prose">
<p>Wrap a <code>.field</code> in a native <code>&lt;label&gt;</code> when the whole card should toggle the switch.</p>
</section>
{% set code %}<label class="w-full max-w-md">
<div role="group" class="field flex-row items-center justify-between">
<div class="grid gap-1.5">
<span>Marketing emails</span>
<span class="field-description">Receive emails about new products, features, and more.</span>
</div>
<input type="checkbox" id="demo-form-switch-disabled" role="switch" disabled>
<input type="checkbox" role="switch" class="input" checked>
</div>
</form>{% endset %}
{{ code_preview("switch-form", code, "w-full") }}
</label>{% endset %}
{{ code_preview("switch-choice-card", code, "w-full") }}
<h3 id="example-disabled"><a href="#example-disabled">Disabled</a></h3>
{% set code %}<div role="group" class="field flex-row items-center justify-between" data-disabled="true">
<label for="switch-disabled">Airplane Mode</label>
<input type="checkbox" id="switch-disabled" role="switch" class="input" disabled>
</div>{% endset %}
{{ code_preview("switch-disabled", code, "w-full max-w-sm") }}
<h3 id="example-invalid"><a href="#example-invalid">Invalid</a></h3>
{% set code %}<div role="group" class="field flex-row items-start justify-between gap-3" data-invalid="true">
<div class="grid gap-1.5">
<label for="switch-invalid">Marketing emails</label>
<p class="field-error">Choose whether marketing emails are enabled.</p>
</div>
<input type="checkbox" id="switch-invalid" role="switch" class="input" aria-invalid="true">
</div>{% endset %}
{{ code_preview("switch-invalid", code, "w-full max-w-sm") }}
<h3 id="example-size"><a href="#example-size">Size</a></h3>
{% set code %}<div class="grid gap-4">
<div role="group" class="field flex-row items-center justify-between">
<label for="switch-size-default">Default</label>
<input type="checkbox" id="switch-size-default" role="switch" class="input" checked>
</div>
<div role="group" class="field flex-row items-center justify-between">
<label for="switch-size-sm">Small</label>
<input type="checkbox" id="switch-size-sm" role="switch" class="input" data-size="sm" checked>
</div>
</div>{% endset %}
{{ code_preview("switch-size", code, "w-full max-w-sm") }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Switch field layout uses document direction. Set <code>dir="rtl"</code> on a parent to render right-to-left.</p>
</section>
{% set code %}<div dir="rtl" class="field flex-row items-center justify-between">
<label for="switch-rtl">وضع الطيران</label>
<input type="checkbox" id="switch-rtl" role="switch" class="input" checked>
</div>{% endset %}
{{ code_preview("switch-rtl", code, "w-full max-w-sm") }}
+96 -12
View File
@@ -5,12 +5,21 @@ description: A responsive table component.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Footer
id: example-footer
- label: Actions
id: example-actions
- label: RTL
id: example-rtl
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<div class="overflow-x-auto">
{% set code %}<div class="table-container">
<table class="table">
<caption>A list of your recent invoices.</caption>
<thead>
@@ -26,49 +35,49 @@ toc:
<td class="font-medium">INV001</td>
<td>Paid</td>
<td>Credit Card</td>
<td class="text-right">$250.00</td>
<td class="text-end">$250.00</td>
</tr>
<tr>
<td class="font-medium">INV002</td>
<td>Pending</td>
<td>PayPal</td>
<td class="text-right">$150.00</td>
<td class="text-end">$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>
<td class="text-end">$350.00</td>
</tr>
<tr>
<td class="font-medium">INV004</td>
<td>Paid</td>
<td>Paypal</td>
<td class="text-right">$450.00</td>
<td class="text-end">$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>
<td class="text-end">$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>
<td class="text-end">$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>
<td class="text-end">$300.00</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3">Total</td>
<td class="text-right">$2,500.00</td>
<td class="text-end">$2,500.00</td>
</tr>
</tfoot>
</table>
@@ -78,10 +87,85 @@ toc:
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Simply add the <code>table</code> class to your table element.</p>
<p>Add the <code>table</code> class to your table element. Wrap it in <code>table-container</code> when horizontal scrolling is needed.</p>
</section>
{% set code %}<table class="table">
{% set code %}<div class="table-container">
<table class="table">
<!-- Content of your table -->
</table>
</div>{% endset %}
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-footer"><a href="#example-footer">Footer</a></h3>
{% set code_footer %}<table class="table">
<thead>
<tr>
<th>Invoice</th>
<th class="text-end">Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td>INV001</td>
<td class="text-end">$250.00</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>Total</td>
<td class="text-end">$250.00</td>
</tr>
</tfoot>
</table>{% endset %}
{{ code_block(code) }}
{{ code_preview("table-footer", code_footer, "w-full") }}
<h3 id="example-actions"><a href="#example-actions">Actions</a></h3>
{% set code_actions %}<table class="table">
<thead>
<tr>
<th>Name</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td class="font-medium">Project Alpha</td>
<td class="text-end">
<button type="button" class="btn-xs-outline">Open</button>
</td>
</tr>
</tbody>
</table>{% endset %}
{{ code_preview("table-actions", code_actions, "w-full") }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Tables support right-to-left layouts through native table direction and logical alignment utilities such as <code>text-end</code>.</p>
</section>
{% set code_rtl %}<div class="table-container" dir="rtl">
<table class="table">
<caption>قائمة الفواتير الأخيرة.</caption>
<thead>
<tr>
<th>الفاتورة</th>
<th>الحالة</th>
<th class="text-end">المبلغ</th>
</tr>
</thead>
<tbody>
<tr>
<td class="font-medium">INV001</td>
<td>مدفوعة</td>
<td class="text-end">$250.00</td>
</tr>
</tbody>
</table>
</div>{% endset %}
{{ code_preview("table-rtl", code_rtl, "w-full") }}
+154 -87
View File
@@ -1,17 +1,17 @@
---
layout: layouts/page.njk
title: Tabs
description: A set of layered sections of content—known as tab panels—that are displayed one at a time.
description: A set of layered sections of content that are displayed one at a time.
toc:
- label: Usage
id: usage
children:
- label: HTML + JavaScript
id: usage-default
id: usage-html-js
children:
- label: "Step 1: Include the JavaScript files"
id: usage-html-js-1
- label: "Step 2: Add your select HTML"
- label: "Step 2: Add your tabs HTML"
id: usage-html-js-2
- label: HTML structure
id: usage-html-js-3
@@ -19,13 +19,27 @@ toc:
id: usage-html-js-4
- label: Jinja and Nunjucks
id: usage-macro
- label: Examples
id: examples
children:
- label: Line
id: example-line
- label: Vertical
id: example-vertical
- label: Disabled
id: example-disabled
- label: Icons
id: example-icons
- label: RTL
id: example-rtl
- label: API Reference
id: api-reference
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "tabs.njk" import tabs %}
{% set code_html %}
{% set account_panel %}
<div class="card">
<header>
@@ -33,14 +47,14 @@ toc:
<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">
<form class="grid gap-4">
<div role="group" class="field">
<label for="demo-tabs-account-name">Name</label>
<input type="text" id="demo-tabs-account-name" value="Pedro Duarte" />
<input type="text" id="demo-tabs-account-name" value="Pedro Duarte">
</div>
<div class="grid gap-3">
<div role="group" class="field">
<label for="demo-tabs-account-username">Username</label>
<input type="text" id="demo-tabs-account-username" value="@peduarte" />
<input type="text" id="demo-tabs-account-username" value="@peduarte">
</div>
</form>
</section>
@@ -49,6 +63,7 @@ toc:
</footer>
</div>
{% endset %}
{% set password_panel %}
<div class="card">
<header>
@@ -56,38 +71,43 @@ toc:
<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">
<form class="grid gap-4">
<div role="group" class="field">
<label for="demo-tabs-password-current">Current password</label>
<input type="password" id="demo-tabs-password-current" />
<input type="password" id="demo-tabs-password-current">
</div>
<div class="grid gap-3">
<div role="group" class="field">
<label for="demo-tabs-password-new">New password</label>
<input type="password" id="demo-tabs-password-new"/>
<input type="password" id="demo-tabs-password-new">
</div>
</form>
</section>
<footer>
<button type="button" class="btn">Save Password</button>
<button type="button" class="btn">Save password</button>
</footer>
</div>
{% endset %}
{% set tabsets_demo = [
{ tab: "Account", panel: account_panel },
{ tab: "Password", panel: password_panel }
{ tab: "Account", panel: account_panel },
{ tab: "Password", panel: password_panel }
] %}
{% set code_html %}
{{ tabs(
id='demo-tabs-with-panels',
id="demo-tabs-with-panels",
tabsets=tabsets_demo,
main_attrs={ "class": "w-full" },
tablist_attrs={ "class": "w-full" }
tablist_attrs={ "class": "w-full", "aria-orientation": "horizontal" }
) }}
{% endset %}
{{ code_preview("tabs", code_html | prettyHtml, "max-w-[300px] w-full") }}
{{ code_preview("tabs", code_html | prettyHtml, "max-w-[400px] w-full") }}
<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">
@@ -103,7 +123,7 @@ toc:
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
<a class="badge-outline" href="/cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
@@ -113,7 +133,7 @@ toc:
</a>
</div>
<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add your select HTML</a></h4>
<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add your tabs HTML</a></h4>
{{ code_block(code_html | prettyHtml, "html") }}
@@ -122,21 +142,19 @@ toc:
<section class="prose">
<dl>
<dt><code class="highlight language-html">&lt;div class="tabs"&gt;</code></dt>
<dd>Wraps around the entire component.
<dd>Root tabs container. It becomes horizontal or vertical based on the child tablist's <code>aria-orientation</code>.
<dl>
<dt><code class="highlight language-html">&lt;nav role="tablist" aria-orientation="horizontal"&gt;</code></dt>
<dd>The tablist containing the tab buttons
<dd>Tablist containing tab buttons. Set <code>aria-orientation="vertical"</code> for vertical tabs and <code>data-variant="line"</code> for the line style.
<dl>
<dt><code class="highlight language-html">&lt;button role="tab" id="{ TAB_ID }" aria-controls="{ PANEL_ID }" aria-selected="false" tabindex="0"&gt;</code></dt>
<dd>The tab button. When active, the <code>aria-selected</code> attribute is set to <code>true</code>. <code>tabindex="0"</code> is required for keyboard navigation.</dd>
<dt><code class="highlight language-html">&lt;button role="tab" id="{ TAB_ID }" aria-controls="{ PANEL_ID }" aria-selected="true" tabindex="0"&gt;</code></dt>
<dd>Tab trigger. The active tab has <code>aria-selected="true"</code> and <code>tabindex="0"</code>. Inactive tabs use <code>tabindex="-1"</code>. Disabled tabs use <code>disabled</code> or <code>aria-disabled="true"</code>.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;div role="tabpanel" id="{ PANEL_ID }" aria-labelledby="{ TAB_ID }"&gt;</code></dt>
<dd>Panel controlled by a tab. Inactive panels are hidden.</dd>
</dl>
</dd>
<dl>
<dt><code class="highlight language-html">&lt;div role="tabpanel" id="{ PANEL_ID }" aria-labelledby="{ TAB_ID }" tabindex="-1" aria-selected="false"&gt;</code></dt>
<dd>The tab panel. When active, the <code>aria-selected</code> attribute is set to <code>true</code>.</dd>
</dl>
</dl>
</section>
@@ -145,7 +163,7 @@ toc:
<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>
<dd>Once the component is initialized, it dispatches a custom non-bubbling <code>basecoat:initialized</code> event on itself.</dd>
</dl>
</section>
@@ -156,7 +174,7 @@ toc:
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
<a class="badge-outline" href="/cli#macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
@@ -170,63 +188,112 @@ toc:
</a>
</div>
{% set raw_code %}{% raw %}{% 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 raw_code %}{% raw %}{{ tabs(
id="account-tabs",
tabsets=[
{ tab: "Account", panel: account_panel },
{ tab: "Password", panel: password_panel }
],
tablist_attrs={ "data-variant": "line" }
) }}{% endraw %}{% endset %}
{{ code_block(raw_code, "jinja") }}
{% 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 %}
<h2 id="examples"><a href="#examples">Examples</a></h2>
{% set tabsets_demo = [
{ tab: "Account", panel: account_panel },
{ tab: "Password", panel: password_panel }
] %}
<h3 id="example-line"><a href="#example-line">Line</a></h3>
{% set code %}
{{ tabs(
id='demo-tabs-with-panels',
tabsets=tabsets_demo,
id="tabs-line",
tabsets=[
{ tab: "Account", panel: "Make changes to your account here." },
{ tab: "Password", panel: "Change your password here." },
{ tab: "Settings", panel: "Update your settings here." }
],
tablist_attrs={ "data-variant": "line" }
) }}
{% endset %}
{{ code_preview("tabs-line", code | prettyHtml, "w-full max-w-md") }}
<h3 id="example-vertical"><a href="#example-vertical">Vertical</a></h3>
{% set code %}
{{ tabs(
id="tabs-vertical",
tabsets=[
{ tab: "Account", panel: "Manage account settings." },
{ tab: "Password", panel: "Update your password." },
{ tab: "Notifications", panel: "Configure notifications." }
],
main_attrs={ "class": "w-full" },
tablist_attrs={ "class": "w-full" }
) }}{% endraw %}{% endset%}
{{ code_block(raw_code, "jinja") }}
tablist_attrs={ "aria-orientation": "vertical" }
) }}
{% endset %}
{{ code_preview("tabs-vertical", code | prettyHtml, "w-full max-w-md") }}
<h3 id="example-disabled"><a href="#example-disabled">Disabled</a></h3>
{% set code %}
{{ tabs(
id="tabs-disabled",
tabsets=[
{ tab: "Account", panel: "Account content." },
{ tab: "Password", panel: "Password content.", tab_attrs: { "disabled": "disabled" } },
{ tab: "Settings", panel: "Settings content." }
]
) }}
{% endset %}
{{ code_preview("tabs-disabled", code | prettyHtml, "w-full max-w-md") }}
<h3 id="example-icons"><a href="#example-icons">Icons</a></h3>
{% set icon_account %}{% lucide "user" %}Account{% endset %}
{% set icon_password %}{% lucide "lock" %}Password{% endset %}
{% set icon_settings %}{% lucide "settings" %}Settings{% endset %}
{% set code %}
{{ tabs(
id="tabs-icons",
tabsets=[
{ tab: icon_account, panel: "Account content." },
{ tab: icon_password, panel: "Password content." },
{ tab: icon_settings, panel: "Settings content." }
]
) }}
{% endset %}
{{ code_preview("tabs-icons", code | prettyHtml, "w-full max-w-md") }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Tabs support document direction. Set <code>dir="rtl"</code> on the tabs root or a parent element.</p>
</section>
{% set code %}
<div dir="rtl">
{{ tabs(
id="tabs-rtl",
tabsets=[
{ tab: "الحساب", panel: "إعدادات الحساب." },
{ tab: "كلمة المرور", panel: "تحديث كلمة المرور." },
{ tab: "الإشعارات", panel: "إعدادات الإشعارات." }
],
tablist_attrs={ "data-variant": "line" }
) }}
</div>
{% endset %}
{{ code_preview("tabs-rtl", code | prettyHtml, "w-full max-w-md") }}
<h2 id="api-reference"><a href="#api-reference">API Reference</a></h2>
<section class="prose">
<dl>
<dt><code class="highlight language-html">.tabs</code></dt>
<dd>Root component class.</dd>
<dt><code class="highlight language-html">[role="tablist"]</code></dt>
<dd>Tablist. Supports <code>aria-orientation="horizontal"</code>, <code>aria-orientation="vertical"</code>, and <code>data-variant="line"</code>.</dd>
<dt><code class="highlight language-html">[role="tab"]</code></dt>
<dd>Tab trigger. Supports <code>aria-selected</code>, <code>disabled</code>, and <code>aria-disabled="true"</code>.</dd>
<dt><code class="highlight language-html">[role="tabpanel"]</code></dt>
<dd>Tab panel associated with a tab by <code>aria-controls</code> and <code>aria-labelledby</code>.</dd>
</dl>
</section>
+52 -42
View File
@@ -8,83 +8,93 @@ toc:
- label: Examples
id: examples
children:
- label: Default
id: example-default
- label: Invalid
id: example-invalid
- label: Field
id: example-field
- label: Disabled
id: example-disabled
- label: With label
id: example-with-label
- label: With text
id: example-with-text
- label: With button
id: example-with-button
- label: Invalid
id: example-invalid
- label: Button
id: example-button
- label: Form
id: example-form
- label: RTL
id: example-rtl
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<textarea class="textarea" placeholder="Type your message here"></textarea>{% endset %}
{{ code_preview("button", code, "w-full") }}
{{ code_preview("textarea", code, class="w-full max-w-xs") }}
<h2 id="usage">Usage</h2>
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Simply add the <code>textarea</code> class to your <code>&lt;textarea&gt;</code> element 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 textarea invalid.</p>
<p>Add the <code>textarea</code> class to a <code>&lt;textarea&gt;</code> element. Textareas inside a <code>form</code> or <code>field</code> container also receive the same styling automatically.</p>
<p>Use native HTML attributes for behavior: <code>disabled</code> for disabled textareas and <code>aria-invalid="true"</code> for invalid state styling.</p>
</section>
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
{{ code_preview("textarea-default", code, "w-full") }}
<h3 id="example-field"><a href="#example-field">Field</a></h3>
<h3 id="example-invalid"><a href="#example-invalid">Invalid</a></h3>
<section class="prose">
<p>Use <code>field</code> with a label and description to create an accessible textarea.</p>
</section>
{% set code %}<textarea class="textarea" placeholder="Type your message here" aria-invalid="true"></textarea>{% endset %}
{{ code_preview("textarea-invalid", code, "w-full") }}
{% set code_field %}<div role="group" class="field w-full max-w-xs">
<label for="textarea-field">Message</label>
<textarea id="textarea-field" placeholder="Type your message here"></textarea>
<p>Write a short message.</p>
</div>{% endset %}
{{ code_preview("textarea-field", code_field, class="w-full max-w-xs") }}
<h3 id="example-disabled"><a href="#example-disabled">Disabled</a></h3>
{% set code %}<textarea class="textarea" placeholder="Type your message here" disabled></textarea>{% endset %}
{{ code_preview("textarea-disabled", code, "w-full") }}
{% set code_disabled %}<textarea class="textarea" placeholder="Type your message here" disabled></textarea>{% endset %}
{{ code_preview("textarea-disabled", code_disabled, class="w-full max-w-xs") }}
<h3 id="example-with-label"><a href="#example-with-label">With label</a></h3>
<h3 id="example-invalid"><a href="#example-invalid">Invalid</a></h3>
{% set code %}<div class="grid gap-3">
<label for="textarea-with-label" class="label">Label</label>
<textarea id="textarea-with-label" class="textarea" placeholder="Type your message here"></textarea>
{% set code_invalid %}<div role="group" class="field w-full max-w-xs">
<label for="textarea-invalid">Message</label>
<textarea class="textarea" id="textarea-invalid" placeholder="Type your message here" aria-invalid="true"></textarea>
<p>Message is required.</p>
</div>{% endset %}
{{ code_preview("textarea-with-label", code, "w-full") }}
{{ code_preview("textarea-invalid", code_invalid, class="w-full max-w-xs") }}
<h3 id="example-with-text"><a href="#example-with-text">With text</a></h3>
<h3 id="example-button"><a href="#example-button">Button</a></h3>
{% set code %}<div class="grid gap-3">
<label for="textarea-with-label" class="label">Label</label>
<textarea id="textarea-with-label" 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>{% endset %}
{{ code_preview("textarea-with-text", code, "w-full") }}
<h3 id="example-with-button"><a href="#example-with-button">With button</a></h3>
{% set code %}<div class="grid gap-3">
<textarea id="textarea-with-label" class="textarea" placeholder="Type your message here"></textarea>
{% set code_button %}<div class="grid w-full max-w-xs gap-3">
<textarea class="textarea" placeholder="Type your message here"></textarea>
<button type="submit" class="btn">Submit</button>
</div>{% endset %}
{{ code_preview("textarea-form", code, "w-full") }}
{{ code_preview("textarea-button", code_button, class="w-full max-w-xs") }}
<h3 id="example-form"><a href="#example-form">Form</a></h3>
{% set code %}<form class="form space-y-6">
<div class="grid gap-3">
<label for="textarea-form" class="label">Bio</label>
{% set code_form %}<form class="w-full max-w-sm space-y-6">
<div role="group" class="field">
<label for="textarea-form">Bio</label>
<textarea id="textarea-form" placeholder="Tell us a bit about yourself"></textarea>
<p class="text-muted-foreground text-sm">You can @mention other users and organizations.</p>
<p>You can @mention other users and organizations.</p>
</div>
<button type="submit" class="btn">Submit</button>
</form>{% endset %}
{{ code_preview("textarea-form", code, "w-full max-w-sm") }}
{{ code_preview("textarea-form", code_form, class="w-full max-w-sm") }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
<section class="prose">
<p>Textareas support right-to-left layouts through native browser direction handling. Add <code>dir="rtl"</code> to the textarea, field, or an ancestor.</p>
</section>
{% set code_rtl %}<div role="group" class="field w-full max-w-xs" dir="rtl">
<label for="textarea-rtl">التعليقات</label>
<textarea id="textarea-rtl" placeholder="تعليقاتك تساعدنا على التحسين..." rows="4"></textarea>
<p>شاركنا أفكارك حول الخدمة.</p>
</div>{% endset %}
{{ code_preview("textarea-rtl", code_rtl, class="w-full max-w-xs") }}
+2 -2
View File
@@ -78,7 +78,7 @@ toc:
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
<a class="badge-outline" href="/cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
@@ -232,7 +232,7 @@ toc:
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
<a class="badge-outline" href="/cli#macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
+63 -21
View File
@@ -1,7 +1,7 @@
---
layout: layouts/page.njk
title: Tooltip
description: A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.
description: A popup that displays information related to an element when the mouse hovers over it.
toc:
- label: Usage
id: usage
@@ -10,44 +10,86 @@ toc:
children:
- label: Default
id: example-default
- label: Right
id: example-right
- label: Bottom + Start
id: example-bottom-start
- label: Sides
id: example-sides
- label: Icon Button
id: example-icon-button
- label: Disabled Button
id: example-disabled-button
- label: Link
id: example-link
- label: RTL
id: example-rtl
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<button class="btn-outline" data-tooltip="Add to library">Hover</button>{% endset %}
{{ code_preview("button", code) }}
{{ code_preview("tooltip", code) }}
<h2 id="usage">Usage</h2>
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>On any element, set the value of the <code>data-tooltip</code> attribute to the text you want to display in the tooltip. Additionally, you can set:</p>
<ul>
<li><code>data-side</code> attribute to <code>top</code>, <code>bottom</code>, <code>left</code>, or <code>right</code> to change the side of the tooltip.</li>
<li><code>data-align</code> attribute to <code>start</code>, <code>center</code>, or <code>end</code> to change the alignment of the tooltip.</li>
</ul>
<p>Add <code>data-tooltip</code> to the trigger element. Use <code>data-side</code> and <code>data-align</code> to change the position.</p>
<p>Basecoat tooltips are CSS-only and text-only. This intentionally differs from shadcn/ui's composed tooltip content, which can render arbitrary markup and uses a portal.</p>
</section>
{% set code %}<button class="btn-outline" data-tooltip="Tooltip text" data-side="bottom" data-align="center">Bottom left</button>{% endset %}
{{ code_block(code) }}
{% set code %}<button class="btn-outline" data-tooltip="Tooltip text" data-side="bottom" data-align="center">Bottom</button>{% endset %}
{{ code_block(code | prettyHtml, "html") }}
<section class="prose">
<dl>
<dt><code>data-tooltip</code></dt>
<dd>Tooltip text.</dd>
<dt><code>data-side</code> <span class="badge-secondary">Optional</span></dt>
<dd><code>top</code>, <code>bottom</code>, <code>left</code>, <code>right</code>, <code>inline-start</code>, or <code>inline-end</code>. Defaults to <code>top</code>.</dd>
<dt><code>data-align</code> <span class="badge-secondary">Optional</span></dt>
<dd><code>start</code>, <code>center</code>, or <code>end</code>. Defaults to <code>center</code>.</dd>
</dl>
</section>
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-default"><a href="#example-default">Default</a></h3>
{% set code %}<button class="btn-outline" data-tooltip="Default tooltip">Default</button>{% endset %}
{{ code_preview("button", code) }}
{{ code_preview("tooltip-default", code) }}
<h3 id="example-right"><a href="#example-right">Right</a></h3>
<h3 id="example-sides"><a href="#example-sides">Sides</a></h3>
{% set code %}<button class="btn-outline" data-tooltip="Right side tooltip" data-side="right">Right</button>{% endset %}
{{ code_preview("button", code) }}
{% set code %}<div class="flex flex-wrap gap-2">
<button class="btn-outline" data-tooltip="Tooltip" data-side="inline-start">Inline Start</button>
<button class="btn-outline" data-tooltip="Tooltip" data-side="top">Top</button>
<button class="btn-outline" data-tooltip="Tooltip" data-side="bottom">Bottom</button>
<button class="btn-outline" data-tooltip="Tooltip" data-side="inline-end">Inline End</button>
</div>{% endset %}
{{ code_preview("tooltip-sides", code) }}
<h3 id="example-bottom-start"><a href="#example-bottom-start">Bottom + Start</a></h3>
<h3 id="example-icon-button"><a href="#example-icon-button">Icon Button</a></h3>
{% set code %}<button class="btn-outline" data-tooltip="Bottom side and start aligned tooltip" data-side="bottom" data-align="start">Bottom + Start</button>{% endset %}
{{ code_preview("button", code) }}
{% set code %}<button class="btn-icon-ghost" data-tooltip="Additional information">
{% lucide "info" %}
<span class="sr-only">Info</span>
</button>{% endset %}
{{ code_preview("tooltip-icon-button", code) }}
<h3 id="example-disabled-button"><a href="#example-disabled-button">Disabled Button</a></h3>
{% set code %}<span class="inline-block" data-tooltip="This feature is currently unavailable">
<button class="btn-outline" disabled>Disabled</button>
</span>{% endset %}
{{ code_preview("tooltip-disabled-button", code) }}
<h3 id="example-link"><a href="#example-link">Link</a></h3>
{% set code %}<a href="#" class="text-primary text-sm underline-offset-4 hover:underline" data-tooltip="Click to read the documentation">Learn more</a>{% endset %}
{{ code_preview("tooltip-link", code) }}
<h3 id="example-rtl"><a href="#example-rtl">RTL</a></h3>
{% set code %}<div dir="rtl" class="flex gap-2">
<button class="btn-outline" data-tooltip="تلميح" data-side="inline-start">Inline Start</button>
<button class="btn-outline" data-tooltip="تلميح" data-side="inline-end">Inline End</button>
</div>{% endset %}
{{ code_preview("tooltip-rtl", code) }}