Dropdown
A floating menu of links or actions surfaced from a trigger.
Basic
A trigger opens the menu via data-stisla-dropdown-trigger="<id>". The menu carries the matching id plus data-stisla-dropdown. The wrapper .dropdown only provides positioning context. Floating UI computes the actual placement and keeps the menu anchored on scroll or resize.
<div class="dropdown">
<button type="button" class="btn btn--outline btn--neutral" data-stisla-dropdown-trigger="ddBasic" aria-haspopup="menu" aria-expanded="false" aria-controls="ddBasic">
Actions
</button>
<div class="dropdown-menu" id="ddBasic" data-stisla-dropdown role="menu" data-state="closed">
<button type="button" class="dropdown-menu__item" role="menuitem">Profile</button>
<button type="button" class="dropdown-menu__item" role="menuitem">Settings</button>
<button type="button" class="dropdown-menu__item" role="menuitem">Sign out</button>
</div>
</div>With icons
Drop an <svg> or <i data-lucide> as the first child of an item. The icon pins to 1rem and inherits the row color on hover so it tracks the surface naturally.
<div class="dropdown">
<button type="button" class="btn btn--outline btn--neutral" data-stisla-dropdown-trigger="ddIcons" aria-haspopup="menu" aria-expanded="false" aria-controls="ddIcons">
Account
</button>
<div class="dropdown-menu" id="ddIcons" data-stisla-dropdown role="menu" data-state="closed">
<a href="#" class="dropdown-menu__item" role="menuitem">
<i data-lucide="user"></i>
<span>Profile</span>
</a>
<a href="#" class="dropdown-menu__item" role="menuitem">
<i data-lucide="settings"></i>
<span>Settings</span>
</a>
<a href="#" class="dropdown-menu__item" role="menuitem">
<i data-lucide="bell"></i>
<span>Notifications</span>
</a>
<hr class="dropdown-menu__divider" role="separator">
<a href="#" class="dropdown-menu__item" role="menuitem">
<i data-lucide="log-out"></i>
<span>Sign out</span>
</a>
</div>
</div>Headers and dividers
Use .dropdown-menu__header to label a section and .dropdown-menu__divider to separate groups. Wrap rows in a role="group" with aria-labelledby pointing at the header so screen readers announce the grouping.
<div class="dropdown">
<button type="button" class="btn btn--outline btn--neutral" data-stisla-dropdown-trigger="ddGroups" aria-haspopup="menu" aria-expanded="false" aria-controls="ddGroups">
Workspace
</button>
<div class="dropdown-menu" id="ddGroups" data-stisla-dropdown role="menu" data-state="closed">
<div class="dropdown-menu__group" role="group" aria-labelledby="ddGroupsAccount">
<h3 class="dropdown-menu__header" id="ddGroupsAccount">Account</h3>
<a href="#" class="dropdown-menu__item" role="menuitem">Profile</a>
<a href="#" class="dropdown-menu__item" role="menuitem">Billing</a>
</div>
<hr class="dropdown-menu__divider" role="separator">
<div class="dropdown-menu__group" role="group" aria-labelledby="ddGroupsWorkspace">
<h3 class="dropdown-menu__header" id="ddGroupsWorkspace">Workspace</h3>
<a href="#" class="dropdown-menu__item" role="menuitem">Members</a>
<a href="#" class="dropdown-menu__item" role="menuitem">Settings</a>
</div>
</div>
</div>Active and disabled
Mark the user's currently-applied choice with aria-current="true" or data-state="active". Both paint --st-highlight as the persistent selected fill. Disabled rows take aria-disabled="true" on anchors or the native disabled attribute on buttons.
<div class="dropdown">
<button type="button" class="btn btn--outline btn--neutral" data-stisla-dropdown-trigger="ddSort" aria-haspopup="menu" aria-expanded="false" aria-controls="ddSort">
Sort by
</button>
<div class="dropdown-menu" id="ddSort" data-stisla-dropdown role="menu" data-state="closed">
<button type="button" class="dropdown-menu__item" role="menuitem" aria-current="true">Newest first</button>
<button type="button" class="dropdown-menu__item" role="menuitem">Oldest first</button>
<button type="button" class="dropdown-menu__item" role="menuitem">Alphabetical</button>
<button type="button" class="dropdown-menu__item" role="menuitem" disabled>By owner (Pro)</button>
</div>
</div>Destructive items
Add .dropdown-menu__item--danger for actions that delete data or sign the user out. The color flips to --st-danger and hover paints a soft danger tint instead of the standard accent fill so the row never reads like a routine choice.
<div class="dropdown">
<button type="button" class="btn btn--outline btn--neutral" data-stisla-dropdown-trigger="ddDanger" aria-haspopup="menu" aria-expanded="false" aria-controls="ddDanger">
Manage project
</button>
<div class="dropdown-menu" id="ddDanger" data-stisla-dropdown role="menu" data-state="closed">
<button type="button" class="dropdown-menu__item" role="menuitem">
<i data-lucide="pencil"></i>
<span>Rename</span>
</button>
<button type="button" class="dropdown-menu__item" role="menuitem">
<i data-lucide="copy"></i>
<span>Duplicate</span>
</button>
<button type="button" class="dropdown-menu__item" role="menuitem">
<i data-lucide="archive"></i>
<span>Archive</span>
</button>
<hr class="dropdown-menu__divider" role="separator">
<button type="button" class="dropdown-menu__item dropdown-menu__item--danger" role="menuitem">
<i data-lucide="trash-2"></i>
<span>Delete project</span>
</button>
</div>
</div>Checkbox items
Items with role="menuitemcheckbox" toggle between checked and unchecked on click. The framework flips data-state and aria-checked; the leading .dropdown-menu__indicator slot paints the check glyph when checked. The menu stays open between toggles via data-stisla-dropdown-auto-close="outside".
<div class="dropdown">
<button type="button" class="btn btn--outline btn--neutral" data-stisla-dropdown-trigger="ddCheck" aria-haspopup="menu" aria-expanded="false" aria-controls="ddCheck">
View
</button>
<div class="dropdown-menu" id="ddCheck" data-stisla-dropdown data-stisla-dropdown-auto-close="outside" role="menu" data-state="closed">
<button type="button" class="dropdown-menu__item" role="menuitemcheckbox" data-state="checked" aria-checked="true">
<span class="dropdown-menu__indicator"><i data-lucide="check"></i></span>
<span>Show grid</span>
</button>
<button type="button" class="dropdown-menu__item" role="menuitemcheckbox" data-state="unchecked" aria-checked="false">
<span class="dropdown-menu__indicator"><i data-lucide="check"></i></span>
<span>Show ruler</span>
</button>
<button type="button" class="dropdown-menu__item" role="menuitemcheckbox" data-state="checked" aria-checked="true">
<span class="dropdown-menu__indicator"><i data-lucide="check"></i></span>
<span>Snap to pixels</span>
</button>
</div>
</div>Radio items
Items with role="menuitemradio" inside a role="group" behave like a radio group. Clicking one item checks it and unchecks every sibling in the same group. They use the same indicator slot as checkbox items.
<div class="dropdown">
<button type="button" class="btn btn--outline btn--neutral" data-stisla-dropdown-trigger="ddRadio" aria-haspopup="menu" aria-expanded="false" aria-controls="ddRadio">
Theme
</button>
<div class="dropdown-menu" id="ddRadio" data-stisla-dropdown data-stisla-dropdown-auto-close="outside" role="menu" data-state="closed">
<div role="group" aria-labelledby="ddRadioHeader" class="d-flex flex-column" style="gap: 2px;">
<h3 class="dropdown-menu__header" id="ddRadioHeader">Appearance</h3>
<button type="button" class="dropdown-menu__item" role="menuitemradio" data-state="checked" aria-checked="true">
<span class="dropdown-menu__indicator"><i data-lucide="check"></i></span>
<span>Light</span>
</button>
<button type="button" class="dropdown-menu__item" role="menuitemradio" data-state="unchecked" aria-checked="false">
<span class="dropdown-menu__indicator"><i data-lucide="check"></i></span>
<span>Dark</span>
</button>
<button type="button" class="dropdown-menu__item" role="menuitemradio" data-state="unchecked" aria-checked="false">
<span class="dropdown-menu__indicator"><i data-lucide="check"></i></span>
<span>System</span>
</button>
</div>
</div>
</div>Keyboard shortcuts
Append a .dropdown-menu__shortcut chip after the label and auto-margin pushes it to the trailing edge of the row. Pair with <kbd> for the keystroke glyphs. The chip color inherits in hover and active paint so it stays readable on the highlight surface.
<div class="dropdown">
<button type="button" class="btn btn--outline btn--neutral" data-stisla-dropdown-trigger="ddShortcut" aria-haspopup="menu" aria-expanded="false" aria-controls="ddShortcut">
File
</button>
<div class="dropdown-menu" id="ddShortcut" data-stisla-dropdown role="menu" data-state="closed" style="min-width: 14rem;">
<button type="button" class="dropdown-menu__item" role="menuitem">
<span>New file</span>
<span class="dropdown-menu__shortcut"><kbd>⌘</kbd><kbd>N</kbd></span>
</button>
<button type="button" class="dropdown-menu__item" role="menuitem">
<span>Open…</span>
<span class="dropdown-menu__shortcut"><kbd>⌘</kbd><kbd>O</kbd></span>
</button>
<button type="button" class="dropdown-menu__item" role="menuitem">
<span>Save</span>
<span class="dropdown-menu__shortcut"><kbd>⌘</kbd><kbd>S</kbd></span>
</button>
<hr class="dropdown-menu__divider" role="separator">
<button type="button" class="dropdown-menu__item" role="menuitem">
<span>Print</span>
<span class="dropdown-menu__shortcut"><kbd>⌘</kbd><kbd>P</kbd></span>
</button>
</div>
</div>Form inside
Drop a form into the menu and set data-stisla-dropdown-auto-close="outside" so clicks inside don't dismiss the menu while the user fills out fields. The menu auto-sizes its height to the viewport, so long content scrolls inside.
<div class="dropdown">
<button type="button" class="btn btn--primary" data-stisla-dropdown-trigger="ddForm" aria-haspopup="menu" aria-expanded="false" aria-controls="ddForm">
Sign in
</button>
<form class="dropdown-menu gap-3 p-3" id="ddForm" data-stisla-dropdown data-stisla-dropdown-auto-close="outside" data-state="closed" style="min-width: 18rem;">
<div class="d-flex flex-column gap-1">
<label for="ddFormEmail" class="form-label">Email</label>
<input type="email" class="input" id="ddFormEmail" placeholder="you@example.com">
</div>
<div class="d-flex flex-column gap-1">
<label for="ddFormPassword" class="form-label">Password</label>
<input type="password" class="input" id="ddFormPassword">
</div>
<label class="field-row" for="ddFormRemember">
<input type="checkbox" class="checkbox" id="ddFormRemember">
<span class="field-row__label">Remember me</span>
</label>
<button type="submit" class="btn btn--primary w-100">Sign in</button>
</form>
</div>Placement
Set data-stisla-dropdown-placement on the menu to override the default bottom-start. Floating UI flips the menu automatically when it would overflow the viewport, so the value is a preference. It isn't a hard constraint.
<div class="d-flex flex-wrap gap-2">
<div class="dropdown">
<button type="button" class="btn btn--outline btn--neutral" data-stisla-dropdown-trigger="ddTop" aria-haspopup="menu" aria-expanded="false" aria-controls="ddTop">
Top
</button>
<div class="dropdown-menu" id="ddTop" data-stisla-dropdown data-stisla-dropdown-placement="top" role="menu" data-state="closed">
<button type="button" class="dropdown-menu__item" role="menuitem">Action</button>
<button type="button" class="dropdown-menu__item" role="menuitem">Another action</button>
</div>
</div>
<div class="dropdown">
<button type="button" class="btn btn--outline btn--neutral" data-stisla-dropdown-trigger="ddRight" aria-haspopup="menu" aria-expanded="false" aria-controls="ddRight">
Right
</button>
<div class="dropdown-menu" id="ddRight" data-stisla-dropdown data-stisla-dropdown-placement="right-start" role="menu" data-state="closed">
<button type="button" class="dropdown-menu__item" role="menuitem">Action</button>
<button type="button" class="dropdown-menu__item" role="menuitem">Another action</button>
</div>
</div>
<div class="dropdown">
<button type="button" class="btn btn--outline btn--neutral" data-stisla-dropdown-trigger="ddLeft" aria-haspopup="menu" aria-expanded="false" aria-controls="ddLeft">
Left
</button>
<div class="dropdown-menu" id="ddLeft" data-stisla-dropdown data-stisla-dropdown-placement="left-start" role="menu" data-state="closed">
<button type="button" class="dropdown-menu__item" role="menuitem">Action</button>
<button type="button" class="dropdown-menu__item" role="menuitem">Another action</button>
</div>
</div>
<div class="dropdown">
<button type="button" class="btn btn--outline btn--neutral" data-stisla-dropdown-trigger="ddBottomEnd" aria-haspopup="menu" aria-expanded="false" aria-controls="ddBottomEnd">
Bottom-end
</button>
<div class="dropdown-menu" id="ddBottomEnd" data-stisla-dropdown data-stisla-dropdown-placement="bottom-end" role="menu" data-state="closed">
<button type="button" class="dropdown-menu__item" role="menuitem">Action</button>
<button type="button" class="dropdown-menu__item" role="menuitem">Another action</button>
</div>
</div>
</div>Customization
Every variable that retunes .dropdown-menu is below. Override on a single menu, on a parent scope, or on :root. The concentric inner radius derives from --dropdown-radius minus --dropdown-padding so the rows stay inset when the outer chip rounds.
Geometry
| Variable | Default | Use |
|---|---|---|
--dropdown-width-min | 10rem | Lower bound on menu width. Long labels grow past it. |
--dropdown-padding | 0.25rem | Inner padding around the row stack. Drives the concentric inset. |
--dropdown-radius | var(--st-radius) | Outer chip radius. Medium tier, matches btn / input / popover. |
--dropdown-item-radius | calc(var(--dropdown-radius) - var(--dropdown-padding)) | Row radius. Derived; rarely overridden directly. |
--dropdown-gap | 2px | Vertical gap between rows. |
--dropdown-z-index | 1030 | Stack level. Below drawer (1045) and dialog (1055). |
--dropdown-font-size | 0.875rem | Row text size. |
Surface
| Variable | Default | Use |
|---|---|---|
--dropdown-bg | var(--st-surface) | Menu background. |
--dropdown-color | var(--st-foreground) | Rest text color. |
--dropdown-border-color | var(--st-border) | Outer rim. |
--dropdown-shadow | var(--st-shadow) | Lift off the page. |
Item
| Variable | Default | Use |
|---|---|---|
--dropdown-item-padding-y | calc(0.125rem * var(--st-density)) | Top and bottom inset. |
--dropdown-item-padding-x | calc(0.75rem * var(--st-density)) | Left and right inset. |
--dropdown-item-min-height | calc(2rem * var(--st-density)) | Click-target floor. Density-multiplied. |
--dropdown-item-gap | 0.5rem | Space between icon, label, indicator, shortcut. |
--dropdown-item-icon-size | 1rem | Leading icon and indicator footprint. |
Item states
| Variable | Default | Use |
|---|---|---|
--dropdown-item-bg-hover | var(--st-accent) | Hover and [data-highlighted] fill. |
--dropdown-item-color-hover | var(--st-accent-foreground) | Hover text. |
--dropdown-item-bg-active | var(--st-highlight) | aria-current / data-state="active" fill. |
--dropdown-item-color-active | var(--st-highlight-foreground) | Active text. |
--dropdown-item-color-disabled | var(--st-muted-foreground) | Disabled row color. |
--dropdown-item-color-danger | var(--st-danger) | Destructive row color. |
--dropdown-item-bg-danger-hover | color-mix(in oklch, var(--st-danger) 12%, transparent) | Destructive hover fill. Soft tint instead of the standard accent. |
Header
| Variable | Default | Use |
|---|---|---|
--dropdown-header-padding-y | 0.25rem | Section label top/bottom inset. |
--dropdown-header-padding-x | 0.75rem | Section label left/right inset. |
--dropdown-header-font-size | 0.75rem | Section label text size. |
--dropdown-header-font-weight | 600 | Section label weight. |
--dropdown-header-color | var(--st-muted-foreground) | Section label color. |
Divider
| Variable | Default | Use |
|---|---|---|
--dropdown-divider-color | var(--st-border) | Separator line. |
--dropdown-divider-margin-y | 0.5rem | Breathing space above and below. |
Shortcut + motion
| Variable | Default | Use |
|---|---|---|
--dropdown-shortcut-color | var(--st-muted-foreground) | Trailing keystroke chip color. |
--dropdown-shortcut-font-size | 0.75rem | Shortcut chip text size. |
--dropdown-transition-duration | 0.15s | Open and close fade. Zeroed under prefers-reduced-motion. |