Overlays
Dropdown
A menu of actions behind a button. It opens below or above, aligns to whichever edge fits, and grows out of its trigger.
Installation
npx sinaui add dropdownCopy the src/lib/ui/dropdown/ folder, floating.ts and tokens.css. No dependencies (icons in the examples are optional).
Examples
Basic
Spread props onto your trigger: it carries the ARIA attributes, keyboard handling and a reference for positioning. Try the arrow keys, or type a letter.
<script lang="ts">
import Button from '#lib/ui/Button.svelte';
import Icon from '#lib/ui/Icon.svelte';
import * as Dropdown from '#lib/ui/dropdown/index.js';
import {
ArrowDown01Icon,
Book02Icon,
Copy01Icon,
Delete02Icon,
Edit02Icon,
Share08Icon
} from '@hugeicons/core-free-icons';
let lastAction = $state('');
</script>
<Dropdown.Root>
{#snippet trigger(props)}
<Button variant="secondary" {...props}>
Canon of Medicine <span class="chev"><Icon icon={ArrowDown01Icon} size={16} /></span>
</Button>
{/snippet}
<Dropdown.Item onselect={() => (lastAction = 'Opened the Canon of Medicine.')}>
<Icon icon={Book02Icon} /> Open
</Dropdown.Item>
<Dropdown.Item onselect={() => (lastAction = 'Added a note in the margin.')}>
<Icon icon={Edit02Icon} /> Annotate
</Dropdown.Item>
<Dropdown.Item onselect={() => (lastAction = 'A scribe is making a copy.')}>
<Icon icon={Copy01Icon} /> Transcribe a copy
</Dropdown.Item>
<Dropdown.Item disabled><Icon icon={Share08Icon} /> Send to Córdoba</Dropdown.Item>
<Dropdown.Separator />
<Dropdown.Item variant="danger" onselect={() => (lastAction = 'Loan withdrawn.')}>
<Icon icon={Delete02Icon} /> Withdraw loan
</Dropdown.Item>
</Dropdown.Root>
<p class="status" role="status">{lastAction}</p>
<style>
/* The chevron turns over while its menu is open. */
.chev {
display: grid;
transition: rotate var(--ui-dur) var(--ui-ease-out);
}
:global([aria-expanded='true']) > :global(.label) > .chev {
rotate: 180deg;
}
.status {
flex-basis: 100%;
min-block-size: 1.65em;
margin: 0;
color: var(--ui-muted);
}
</style>Icon trigger
Near the viewport's edge the menu aligns to the trigger's other side instead of being pushed in.
<script lang="ts">
import Button from '#lib/ui/Button.svelte';
import Icon from '#lib/ui/Icon.svelte';
import * as Dropdown from '#lib/ui/dropdown/index.js';
import { Book02Icon, MoreHorizontalIcon, Share08Icon } from '@hugeicons/core-free-icons';
</script>
<div class="end">
<Dropdown.Root>
{#snippet trigger(props)}
<Button variant="ghost" square aria-label="More actions" {...props}>
<Icon icon={MoreHorizontalIcon} />
</Button>
{/snippet}
<Dropdown.Item><Icon icon={Share08Icon} /> Copy link</Dropdown.Item>
<Dropdown.Item><Icon icon={Book02Icon} /> Move to reading room</Dropdown.Item>
</Dropdown.Root>
</div>
<style>
.end {
display: flex;
justify-content: flex-end;
inline-size: 100%;
}
</style>Submenus
Point at an item with an arrow and its submenu opens beside it; it stays open while you move across. From the keyboard, Right arrow opens it and Left arrow or Escape comes back.
Keeping the menu open
Call preventDefault() in onselect for items that toggle something the user
may want to toggle again.
<Dropdown.Item onselect={(e) => { e.preventDefault(); toggleBookmark(); }}>
Bookmark
</Dropdown.Item>Props
Dropdown.Root
| Prop | Type | Default |
|---|---|---|
open | boolean, bindable | false |
trigger | Snippet receiving trigger props (required) |
Dropdown.Item
| Prop | Type | Default |
|---|---|---|
onselect | (e: MouseEvent) => void | |
variant | 'default' | 'danger' | 'default' |
disabled | boolean | false |
Dropdown.Sub
| Prop | Type | Default |
|---|---|---|
label | string, the item that opens it | required |
start | Snippet before the label: an icon |
Accessibility
- Follows the WAI-ARIA menu button pattern:
aria-haspopup,aria-expandedandaria-controlson the trigger,role="menu"andmenuiteminside. - Opens in the top layer (Popover API), so no parent's overflow or z-index can clip it.
- Disabled items stay focusable so they're announced, but can't be chosen.
- Choosing an item or pressing Escape returns focus to the trigger.
| Key | Action |
|---|---|
| Enter / Space / ↓ | On the trigger: opens and focuses the first item. |
| ↑ | On the trigger: opens and focuses the last item. |
| ↓ / ↑ | Next / previous item, wrapping around. |
| Home / End | First / last item. |
| A letter | Next item starting with that letter. |
| → | On a submenu's item: opens it and focuses its first item. |
| ← / Escape | In a submenu: closes it, back to its item. |
| Enter / Space | Chooses the focused item. |
| Escape / Tab | Closes and returns focus to the trigger. |