Dropdown Menu
Menus & NavigationLocal guide ↗
import { DropdownMenu } from "bits-ui" Complete menu
Commands, shortcuts, a checkbox, radio choices, and a nested workspace picker
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { Bell, Check, ChevronRight, MoreHorizontal } from '@lucide/svelte';
let notifications = $state(true);
let density = $state('comfortable');
const itemClass =
'flex cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2 text-sm outline-none data-highlighted:bg-ink-800 data-highlighted:text-ink-50 data-disabled:opacity-40';
</script>
<div class="flex justify-center">
<DropdownMenu.Root>
<DropdownMenu.Trigger class="btn" aria-label="Open project actions">
Project actions
<MoreHorizontal class="size-4" />
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="panel animate-pop-in w-64 p-1" sideOffset={8} align="end">
<DropdownMenu.Group aria-label="Document actions">
<DropdownMenu.Item class={itemClass}>
Rename project
<kbd class="ml-auto font-mono text-[0.68rem] text-ink-400">⌘ R</kbd>
</DropdownMenu.Item>
<DropdownMenu.Item class={itemClass}>
Duplicate
<kbd class="ml-auto font-mono text-[0.68rem] text-ink-400">⌘ D</kbd>
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator class="my-1 h-px bg-ink-800" />
<DropdownMenu.CheckboxItem bind:checked={notifications} class={itemClass}>
{#snippet children({ checked })}
<Bell class="size-4 text-ink-400" />
Release notifications
{#if checked}<Check class="ml-auto size-4 text-brand-400" />{/if}
{/snippet}
</DropdownMenu.CheckboxItem>
<DropdownMenu.Separator class="my-1 h-px bg-ink-800" />
<DropdownMenu.RadioGroup bind:value={density}>
<DropdownMenu.GroupHeading class="px-2.5 py-1.5 text-[0.68rem] font-semibold uppercase tracking-wider text-ink-400">
Row density
</DropdownMenu.GroupHeading>
{#each ['compact', 'comfortable', 'spacious'] as option (option)}
<DropdownMenu.RadioItem value={option} class={itemClass}>
{#snippet children({ checked })}
<span class="capitalize">{option}</span>
<span class="ml-auto size-2 rounded-full" class:bg-brand-400={checked}></span>
{/snippet}
</DropdownMenu.RadioItem>
{/each}
</DropdownMenu.RadioGroup>
<DropdownMenu.Separator class="my-1 h-px bg-ink-800" />
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger class={itemClass}>
Move to workspace
<ChevronRight class="ml-auto size-4 text-ink-400" />
</DropdownMenu.SubTrigger>
<DropdownMenu.Portal>
<DropdownMenu.SubContent class="panel animate-pop-in w-48 p-1" sideOffset={8}>
<DropdownMenu.Item class={itemClass}>Design systems</DropdownMenu.Item>
<DropdownMenu.Item class={itemClass}>Developer tools</DropdownMenu.Item>
<DropdownMenu.Item class={itemClass}>Research archive</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Portal>
</DropdownMenu.Sub>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</div>
Controlled open state
bind:open lets external controls and the trigger share one source of truth
closed
No command selected
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { ChevronDown } from '@lucide/svelte';
let open = $state(false);
let lastAction = $state('No command selected');
const itemClass =
'flex cursor-default select-none items-center rounded-md px-2.5 py-2 text-sm outline-none data-highlighted:bg-ink-800 data-highlighted:text-ink-50';
</script>
<div class="grid justify-items-center gap-3">
<div class="flex items-center gap-2">
<button class="btn btn-sm" onclick={() => (open = !open)}>{open ? 'Close' : 'Open'} menu</button>
<span class="chip">{open ? 'open' : 'closed'}</span>
</div>
<DropdownMenu.Root bind:open>
<DropdownMenu.Trigger class="btn btn-primary">
Create release
<ChevronDown class="size-4" />
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="panel animate-pop-in w-56 p-1" sideOffset={8}>
<DropdownMenu.Item class={itemClass} onSelect={() => (lastAction = 'Draft release created')}>
Save as draft
</DropdownMenu.Item>
<DropdownMenu.Item class={itemClass} onSelect={() => (lastAction = 'Release scheduled')}>
Schedule for tonight
</DropdownMenu.Item>
<DropdownMenu.Item class={itemClass} onSelect={() => (lastAction = 'Release published')}>
Publish now
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
<p class="muted text-xs" aria-live="polite">{lastAction}</p>
</div>
Arrow keys move the highlighted item, Enter or Space selects it, and Escape closes the menu. Set loop on Content to wrap keyboard navigation; it defaults to false. Supply textValue when an item's visible children do not provide a useful label, because that value drives typeahead.
RadioItem must be inside RadioGroup and its value is required. Portal renders Content into document.body by default; pass a target with to or set disabled to keep it in place.