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

VariableDefaultUse
--dropdown-width-min10remLower bound on menu width. Long labels grow past it.
--dropdown-padding0.25remInner padding around the row stack. Drives the concentric inset.
--dropdown-radiusvar(--st-radius)Outer chip radius. Medium tier, matches btn / input / popover.
--dropdown-item-radiuscalc(var(--dropdown-radius) - var(--dropdown-padding))Row radius. Derived; rarely overridden directly.
--dropdown-gap2pxVertical gap between rows.
--dropdown-z-index1030Stack level. Below drawer (1045) and dialog (1055).
--dropdown-font-size0.875remRow text size.

Surface

VariableDefaultUse
--dropdown-bgvar(--st-surface)Menu background.
--dropdown-colorvar(--st-foreground)Rest text color.
--dropdown-border-colorvar(--st-border)Outer rim.
--dropdown-shadowvar(--st-shadow)Lift off the page.

Item

VariableDefaultUse
--dropdown-item-padding-ycalc(0.125rem * var(--st-density))Top and bottom inset.
--dropdown-item-padding-xcalc(0.75rem * var(--st-density))Left and right inset.
--dropdown-item-min-heightcalc(2rem * var(--st-density))Click-target floor. Density-multiplied.
--dropdown-item-gap0.5remSpace between icon, label, indicator, shortcut.
--dropdown-item-icon-size1remLeading icon and indicator footprint.

Item states

VariableDefaultUse
--dropdown-item-bg-hovervar(--st-accent)Hover and [data-highlighted] fill.
--dropdown-item-color-hovervar(--st-accent-foreground)Hover text.
--dropdown-item-bg-activevar(--st-highlight)aria-current / data-state="active" fill.
--dropdown-item-color-activevar(--st-highlight-foreground)Active text.
--dropdown-item-color-disabledvar(--st-muted-foreground)Disabled row color.
--dropdown-item-color-dangervar(--st-danger)Destructive row color.
--dropdown-item-bg-danger-hovercolor-mix(in oklch, var(--st-danger) 12%, transparent)Destructive hover fill. Soft tint instead of the standard accent.
VariableDefaultUse
--dropdown-header-padding-y0.25remSection label top/bottom inset.
--dropdown-header-padding-x0.75remSection label left/right inset.
--dropdown-header-font-size0.75remSection label text size.
--dropdown-header-font-weight600Section label weight.
--dropdown-header-colorvar(--st-muted-foreground)Section label color.

Divider

VariableDefaultUse
--dropdown-divider-colorvar(--st-border)Separator line.
--dropdown-divider-margin-y0.5remBreathing space above and below.

Shortcut + motion

VariableDefaultUse
--dropdown-shortcut-colorvar(--st-muted-foreground)Trailing keystroke chip color.
--dropdown-shortcut-font-size0.75remShortcut chip text size.
--dropdown-transition-duration0.15sOpen and close fade. Zeroed under prefers-reduced-motion.