Dropdown Menu
Anchored menu on bits-ui DropdownMenu. Content springs open on mount (snappy scale/opacity) and pointer-proximity tints items as the cursor approaches.
Usage
dropdown-menu.svelte
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.RootProps = $props();
</script>
<DropdownMenuPrimitive.Root
bind:open
{...restProps}
{...{ 'data-slot': 'dropdown-menu' } as Record<string, unknown>}
/>
Inverted
Set variant="inverted" on the content for a dark menu over
a light app — Bloom’s signature menu identity. Surface and accent CSS vars swap at the content root,
so item rows, separators, and focus rings flip contrast automatically.
dropdown-menu-inverted.svelte
<DropdownMenu.Content variant="inverted" class="w-48">
<DropdownMenu.Item>Edit</DropdownMenu.Item>
<DropdownMenu.Item>Duplicate</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item variant="destructive">Delete</DropdownMenu.Item>
</DropdownMenu.Content>Checkboxes & radio
dropdown-menu-checks.svelte
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.RootProps = $props();
</script>
<DropdownMenuPrimitive.Root
bind:open
{...restProps}
{...{ 'data-slot': 'dropdown-menu' } as Record<string, unknown>}
/>
Dialog from menu item
Close the menu first, then open the dialog after one tick — prevents focus from being trapped between two overlays simultaneously.
dropdown-menu-with-dialog.svelte
<script lang="ts">
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { Button } from '$lib/components/ui/button';
import { tick } from 'svelte';
let menuOpen = $state(false);
let dialogOpen = $state(false);
</script>
<AlertDialog.Root bind:open={dialogOpen}>
<DropdownMenu.Root bind:open={menuOpen}>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button variant="outline" {...props}>Actions</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content class="w-48">
<DropdownMenu.Item>Edit</DropdownMenu.Item>
<DropdownMenu.Item>Duplicate</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item
variant="destructive"
onSelect={async () => {
// Close the menu first, then open the dialog after one tick
// so focus isn't trapped between two overlays simultaneously.
menuOpen = false;
await tick();
dialogOpen = true;
}}
>
Delete
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>Delete this item?</AlertDialog.Title>
<AlertDialog.Description>
This action is permanent and cannot be undone.
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
<AlertDialog.Action>Delete</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>DropdownMenu.Root props
Inherits bits-ui DropdownMenu.Root props via spread.
| Prop | Type | Default | Description |
|---|---|---|---|
boolean (bindable) | false | Controls visibility. Two-way bindable. | |
(open: boolean) => void | — | Fires when the menu opens or closes. | |
'ltr' | 'rtl' | 'ltr' | Reading direction. |
DropdownMenu.Content props
| Prop | Type | Default | Description |
|---|---|---|---|
number | 4 | Distance in px from the trigger edge. | |
'start' | 'center' | 'end' | 'start' | Cross-axis alignment relative to the trigger. | |
'default' | 'inverted' | 'default' | Surface treatment. Inverted swaps popover/accent CSS vars to render dark on light (and light on dark) — Bloom’s signature menu identity. | |
ComponentProps<DropdownMenu.Portal> | — | Forwarded to the internal portal. | |
string | — | Merged onto the content surface via tailwind-merge. |