Skip to content
On this page Checkbox and radio items

Navigation

Dropdown Menu

A floating action menu with items, checkbox and radio items, submenus and context-menu anchoring.

<script>
  import {
    DropdownMenu,
    DropdownMenuItem,
    DropdownMenuLabel,
    DropdownMenuSeparator,
    Button,
  } from "@codeatlasdev/ui";
</script>

<DropdownMenu>
  {#snippet triggerChild(props)}<Button {...props}>Open menu</Button>{/snippet}
  <DropdownMenuLabel>File actions</DropdownMenuLabel>
  <DropdownMenuItem onclick={edit}>{#snippet children()}Edit{/snippet}</DropdownMenuItem>
  <DropdownMenuSeparator />
  <DropdownMenuItem variant="destructive" onclick={remove}>{#snippet children()}Delete{/snippet}</DropdownMenuItem>
</DropdownMenu>

Keyboard support follows the WAI-ARIA menu pattern: Enter, Space or ArrowDown opens the menu on the first item, ArrowUp opens it on the last, arrows then move, Enter or Space selects, Escape closes and focus returns to the trigger. Tab closes the menu. Selecting an item closes it. The menu is a popover layer in the browser top layer, positioned with CSS anchor positioning where supported, so it stays on top of a dialog it is opened from. Pass an interactive trigger (Button, IconButton) through triggerChild and spread the received props on it. Use trigger only for non-interactive content such as text or an icon. Use DropdownMenuLabel for a non-interactive group heading and DropdownMenuSeparator for a divider.

Checkbox and radio items

DropdownMenuCheckboxItem renders role="menuitemcheckbox" with aria-checked and a check indicator; bind checked or listen to onCheckedChange. DropdownMenuRadioGroup wraps DropdownMenuRadioItems (role="menuitemradio"): bind the group’s value, each item sets it with its own value. Enter, Space or a click toggles the item and the menu stays open so several options can be set in one visit (the WAI-ARIA menu pattern); pass closeOnSelect to close after one pick. Arrow keys, Home, End and typeahead reach these items like any other, and disabled ones are skipped. Give the group an aria-label or put a DropdownMenuLabel first and the group is named for screen readers.

Controlled and context menus

bind:open and onOpenChange control the menu from outside: set open = true from any handler and it opens; onOpenChange fires for user-driven changes (a trigger press, Escape, an outside press, picking an item), like Dialog. For a right-click menu pass anchor={{ x, y }} (viewport coordinates) together with open = true and leave out trigger: the menu opens at that point on the first item and focus returns to the element that had it when the menu closes. Setting anchor again while the menu is open moves it instead of toggling it, so a second right-click relocates the menu. Handle Shift+F10 and the ContextMenu key in your own onkeydown the same way (the demo shows both); on the built-in trigger they open the menu on its first item.

Put a DropdownMenuSub among the items. Its trigger snippet holds a DropdownMenuSubTrigger (an item with a chevron, aria-haspopup="menu" and aria-expanded) and its children are the submenu’s items, labels and further DropdownMenuSubs, nested to any depth. ArrowRight, Enter or Space on the trigger opens the submenu with focus on its first enabled item; ArrowLeft or Escape closes only that level and returns focus to the trigger; arrows, Home, End and typeahead stay inside the open level. Hovering the trigger opens it after openDelay (120 ms) and it stays open for closeDelay (200 ms) after the pointer leaves, so the pointer can cross the gap. Selecting any item closes every level and returns focus to the root trigger. A disabled sub-trigger neither opens nor takes ArrowRight. In a right-to-left layout use side="left" on the sub, and the chevron mirrors.

Leading icons and brand logos

Pass a leading icon with the icon snippet instead of placing it in children: it gets a fixed 14px box, the standard gap and aria-hidden (the label stays the accessible name), so icon size and alignment never drift between menus. Any svg or img inside the box fills it, including one wrapped in SchemeAsset, whatever width or height the asset declares. A plain icon (Lucide, any currentColor glyph) follows the item’s text color, including the highlighted, disabled and destructive states.

A brand logo (a multi-color or fixed-color SVG) must not be recolored: set brandIcon on the item and no state color or dimming reaches the icon. A monochrome mark such as the OpenAI blossom needs a black asset on light themes and a white one on dark: wrap both in SchemeAsset, which renders the variant of the kit’s color scheme (the .dark class on <html>, or a data-theme-scope island) with CSS only, so it is correct in SSR and on first paint and no app has to read the theme store. The same icon / brandIcon pair exists on TabsTrigger. Follow each brand’s own guidelines for the marks you ship.

<DropdownMenuItem brandIcon onclick={openCodex}>
  {#snippet icon()}
    <SchemeAsset>
      {#snippet light()}<img src="/logos/codex-black.svg" alt="" />{/snippet}
      {#snippet dark()}<img src="/logos/codex-white.svg" alt="" />{/snippet}
    </SchemeAsset>
  {/snippet}
  Codex
</DropdownMenuItem>
#158

Default

A button trigger with a label, items, a separator and a destructive item.

Reference #158 · Pick in the gallery

Code
svelte
<script lang="ts">
  import { DropdownMenu, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel, Button } from '@codeatlasdev/ui';
  import { Edit2, Copy, Trash2, Download } from '@lucide/svelte';

  let last = $state('');
</script>

<DropdownMenu>
  {#snippet triggerChild(props)}
    <Button variant="glass" {...props}>Open menu</Button>
  {/snippet}
  <DropdownMenuLabel>File actions</DropdownMenuLabel>
  <DropdownMenuItem onclick={() => (last = 'Edit')}>
    {#snippet icon()}<Edit2 />{/snippet}
    Edit
  </DropdownMenuItem>
  <DropdownMenuItem onclick={() => (last = 'Duplicate')}>
    {#snippet icon()}<Copy />{/snippet}
    Duplicate
  </DropdownMenuItem>
  <DropdownMenuItem onclick={() => (last = 'Download')}>
    {#snippet icon()}<Download />{/snippet}
    Download
  </DropdownMenuItem>
  <DropdownMenuSeparator />
  <DropdownMenuItem variant="destructive" onclick={() => (last = 'Delete')}>
    {#snippet icon()}<Trash2 />{/snippet}
    Delete
  </DropdownMenuItem>
</DropdownMenu>

{#if last}<span class="note" aria-live="polite">Last action: {last}</span>{/if}

<style>
  .note { font-size: var(--ca-font-size-body2); color: var(--ca-text-secondary); }
</style>
#239

Leading icons and brand logos

The icon slot gives every item the same box and gap. brandIcon keeps a logo's own colors, and SchemeAsset swaps a monochrome mark between light and dark.

Reference #239 · Pick in the gallery

Code
svelte
<script lang="ts">
  import { DropdownMenu, DropdownMenuItem, DropdownMenuLabel, SchemeAsset, Button } from '@codeatlasdev/ui';
  import { Bot } from '@lucide/svelte';

  let last = $state('');
</script>

<DropdownMenu>
  {#snippet triggerChild(props)}
    <Button variant="glass" {...props}>New tab</Button>
  {/snippet}
  <DropdownMenuLabel>Agents</DropdownMenuLabel>
  <DropdownMenuItem onclick={() => (last = 'Shell')}>
    {#snippet icon()}<Bot />{/snippet}
    Shell
  </DropdownMenuItem>
  <!-- A brand mark keeps its own colors: brandIcon opts it out of the item's text color. -->
  <DropdownMenuItem brandIcon onclick={() => (last = 'Claude Code')}>
    {#snippet icon()}
      <svg viewBox="0 0 16 16"><rect width="16" height="16" rx="4" fill="#d97757" /><path d="M5 11 8 4l3 7" fill="none" stroke="#fff" stroke-width="1.6" /></svg>
    {/snippet}
    Claude Code
  </DropdownMenuItem>
  <!-- A monochrome mark needs a black asset on light and a white one on dark: SchemeAsset picks by the kit's scheme. -->
  <DropdownMenuItem brandIcon onclick={() => (last = 'Codex')}>
    {#snippet icon()}
      <SchemeAsset>
        {#snippet light()}<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="#111" /></svg>{/snippet}
        {#snippet dark()}<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="#fff" /></svg>{/snippet}
      </SchemeAsset>
    {/snippet}
    Codex
  </DropdownMenuItem>
  <DropdownMenuItem brandIcon disabled>
    {#snippet icon()}
      <svg viewBox="0 0 16 16"><rect width="16" height="16" rx="4" fill="#d97757" /></svg>
    {/snippet}
    Coming soon
  </DropdownMenuItem>
</DropdownMenu>

{#if last}<span class="note" aria-live="polite">Opened: {last}</span>{/if}

<style>
  .note { font-size: var(--ca-font-size-body2); color: var(--ca-text-secondary); }
</style>
#400

Checkbox and radio items

Two toggles and a sort order. The menu stays open while options change; each item reports its state with aria-checked.

Sorted by name. Hidden files hidden, preview on.

Reference #400 · Pick in the gallery

Code
svelte
<script lang="ts">
  import {
    DropdownMenu,
    DropdownMenuCheckboxItem,
    DropdownMenuLabel,
    DropdownMenuRadioGroup,
    DropdownMenuRadioItem,
    DropdownMenuSeparator,
    Button,
  } from '@codeatlasdev/ui';

  let showHidden = $state(false);
  let showPreview = $state(true);
  let sort = $state('name');
</script>

<DropdownMenu>
  {#snippet triggerChild(props)}
    <Button variant="glass" {...props}>View options</Button>
  {/snippet}
  <DropdownMenuCheckboxItem bind:checked={showHidden}>Show hidden files</DropdownMenuCheckboxItem>
  <DropdownMenuCheckboxItem bind:checked={showPreview}>Show preview</DropdownMenuCheckboxItem>
  <DropdownMenuSeparator />
  <DropdownMenuRadioGroup bind:value={sort} aria-label="Sort by">
    <DropdownMenuLabel>Sort by</DropdownMenuLabel>
    <DropdownMenuRadioItem value="name">Name</DropdownMenuRadioItem>
    <DropdownMenuRadioItem value="date">Date modified</DropdownMenuRadioItem>
    <DropdownMenuRadioItem value="size">Size</DropdownMenuRadioItem>
  </DropdownMenuRadioGroup>
</DropdownMenu>

<span class="note" aria-live="polite">
  Sorted by {sort}. Hidden files {showHidden ? 'shown' : 'hidden'}, preview {showPreview ? 'on' : 'off'}.
</span>

<style>
  .note { font-size: var(--ca-font-size-body2); color: var(--ca-text-secondary); }
</style>
#402

Context menu

Right-click opens the menu at the pointer and a second right-click moves it. Shift+F10 or the ContextMenu key opens it from the keyboard.

Reference #402 · Pick in the gallery

Code
svelte
<script lang="ts">
  import { announce, DropdownMenu, DropdownMenuItem, DropdownMenuSeparator } from '@codeatlasdev/ui';

  let open = $state(false);
  let point = $state<{ x: number; y: number } | null>(null);
  let last = $state('');

  function pick(action: string) {
    last = action;
    announce(`Last action: ${action}`);
  }

  function oncontextmenu(e: MouseEvent) {
    e.preventDefault();
    point = { x: e.clientX, y: e.clientY };
    open = true;
  }

  function onkeydown(e: KeyboardEvent) {
    if (e.key !== 'ContextMenu' && !(e.shiftKey && e.key === 'F10')) return;
    e.preventDefault();
    const box = e.currentTarget.getBoundingClientRect();
    point = { x: box.left + 16, y: box.top + 16 };
    open = true;
  }
</script>

<button type="button" class="area" {oncontextmenu} {onkeydown}>
  Right-click here, or focus it and press Shift+F10.
</button>

<DropdownMenu bind:open anchor={point}>
  <DropdownMenuItem onclick={() => pick('Rename')}>Rename</DropdownMenuItem>
  <DropdownMenuItem onclick={() => pick('Duplicate')}>Duplicate</DropdownMenuItem>
  <DropdownMenuSeparator />
  <DropdownMenuItem variant="destructive" onclick={() => pick('Delete')}>Delete</DropdownMenuItem>
</DropdownMenu>

{#if last}<span class="note">Last action: {last}</span>{/if}

<style>
  .area {
    display: grid;
    place-items: center;
    inline-size: 100%;
    min-block-size: 8rem;
    background: none;
    font: inherit;
    cursor: context-menu;
    padding: var(--ca-space-4);
    border: var(--ca-border-width) dashed var(--ca-outline-subtle);
    border-radius: var(--ca-radius-md);
    color: var(--ca-text-secondary);
    font-size: var(--ca-font-size-body2);
  }
  .note { font-size: var(--ca-font-size-body2); color: var(--ca-text-secondary); }
</style>
#401

Color and Move to open beside their item. ArrowRight opens, ArrowLeft or Escape closes one level, hover opens after a short delay.

Reference #401 · Pick in the gallery

Code
svelte
<script lang="ts">
  import {
    DropdownMenu,
    DropdownMenuItem,
    DropdownMenuSeparator,
    DropdownMenuSub,
    DropdownMenuSubTrigger,
    Button,
  } from '@codeatlasdev/ui';

  let last = $state('');
  const colors = ['Red', 'Green', 'Blue'];
  const groups = ['Inbox', 'Archive', 'Projects'];
</script>

<DropdownMenu>
  {#snippet triggerChild(props)}
    <Button variant="glass" {...props}>Item actions</Button>
  {/snippet}
  <DropdownMenuItem onclick={() => (last = 'Rename')}>Rename</DropdownMenuItem>
  <DropdownMenuSub>
    {#snippet trigger()}<DropdownMenuSubTrigger>Color</DropdownMenuSubTrigger>{/snippet}
    {#each colors as color}
      <DropdownMenuItem onclick={() => (last = `Color ${color}`)}>{color}</DropdownMenuItem>
    {/each}
  </DropdownMenuSub>
  <DropdownMenuSub>
    {#snippet trigger()}<DropdownMenuSubTrigger>Move to</DropdownMenuSubTrigger>{/snippet}
    {#each groups as group}
      <DropdownMenuItem onclick={() => (last = `Moved to ${group}`)}>{group}</DropdownMenuItem>
    {/each}
  </DropdownMenuSub>
  <DropdownMenuSeparator />
  <DropdownMenuItem variant="destructive" onclick={() => (last = 'Delete')}>Delete</DropdownMenuItem>
</DropdownMenu>

{#if last}<span class="note" aria-live="polite">Last action: {last}</span>{/if}

<style>
  .note { font-size: var(--ca-font-size-body2); color: var(--ca-text-secondary); }
</style>
#159

Icon trigger

An IconButton trigger. IconButton needs an aria-label.

Reference #159 · Pick in the gallery

Code
svelte
<script lang="ts">
  import { DropdownMenu, DropdownMenuItem, DropdownMenuLabel, IconButton } from '@codeatlasdev/ui';
  import { MoreHorizontal, Copy, Share2 } from '@lucide/svelte';
</script>

<DropdownMenu align="end">
  {#snippet triggerChild(props)}
    <IconButton aria-label="More options" {...props}><MoreHorizontal size={18} /></IconButton>
  {/snippet}
  <DropdownMenuLabel>Actions</DropdownMenuLabel>
  <DropdownMenuItem>
    {#snippet children()}<Share2 size={14} /> Share{/snippet}
  </DropdownMenuItem>
  <DropdownMenuItem>
    {#snippet children()}<Copy size={14} /> Copy link{/snippet}
  </DropdownMenuItem>
</DropdownMenu>
PropTypeDefaultDescription
triggerChildSnippet<[Record<string, unknown>]>–Interactive trigger. Receives the trigger props to spread onto your Button or IconButton.
triggerSnippet–Non-interactive trigger content (text, icon), wrapped in the menu's own button.
childrenSnippet–Menu content: items, labels, separators, checkbox and radio items and submenus.
openbooleanfalseBindable. Whether the menu is open. Set it from code to open or close the menu.
onOpenChange(open: boolean) => void–Called when the user opens or closes the menu.
anchor{ x: number; y: number } | nullnullViewport point the menu opens at (a context menu). Changing it while open moves the menu. Omit `trigger` when you use it.
side'top' | 'right' | 'bottom' | 'left''bottom'Preferred side of the trigger.
align'start' | 'center' | 'end''start'Alignment against the trigger.
sideOffsetnumber6Distance from the trigger in pixels (0 when opened at an `anchor` point).
classstring''Extra CSS classes on the content.
PropTypeDefaultDescription
onclick() => void–Called when the item is selected.
variant'default' | 'destructive''default'destructive colors the item with the error token.
disabledbooleanfalsePrevents selection.
iconSnippet–Leading icon in a fixed 14px box, `aria-hidden`. A plain icon follows the item's text color.
brandIconbooleanfalseThe icon is a brand mark with its own colors: no highlighted, disabled or destructive color reaches it. Pair with `SchemeAsset` for light and dark assets.
childrenSnippet–Item label.
classstring''Extra CSS classes.
PropTypeDefaultDescription
checkedbooleanfalseBindable. Whether the item is checked.
onCheckedChange(checked: boolean) => void–Called with the new state when the user toggles the item.
disabledbooleanfalsePrevents toggling; arrow keys skip the item.
closeOnSelectbooleanfalseClose the menu after toggling.
childrenSnippet–Item label.
classstring''Extra CSS classes.
PropTypeDefaultDescription
valuestring–Bindable. Value of the checked radio item.
onValueChange(value: string) => void–Called with the new value when the user picks an item.
childrenSnippet–DropdownMenuRadioItem children (and an optional DropdownMenuLabel).
classstring''Extra CSS classes.
PropTypeDefaultDescription
valuestring–Required. The group value this item sets.
disabledbooleanfalsePrevents selection; arrow keys skip the item.
closeOnSelectbooleanfalseClose the menu after picking.
childrenSnippet–Item label.
classstring''Extra CSS classes.
PropTypeDefaultDescription
triggerSnippet–Required. The `DropdownMenuSubTrigger` item that opens the submenu.
childrenSnippet–Submenu content: items, labels, separators and nested submenus.
openbooleanfalseBindable. Whether the submenu is open.
onOpenChange(open: boolean) => void–Called when the submenu opens or closes.
disabledbooleanfalseThe trigger is aria-disabled and the submenu never opens.
side'top' | 'right' | 'bottom' | 'left''right'Preferred side of the trigger item.
align'start' | 'center' | 'end''start'Alignment against the trigger item.
sideOffsetnumber2Distance from the trigger item in pixels.
openDelaynumber120Milliseconds the pointer rests on the trigger before it opens.
closeDelaynumber200Milliseconds it stays open after the pointer leaves.
classstring''Extra CSS classes on the submenu content.
PropTypeDefaultDescription
iconSnippet–Leading icon in a fixed 14px box, `aria-hidden`.
childrenSnippet–Item label.
classstring''Extra CSS classes.
PropTypeDefaultDescription
childrenSnippet–Heading text.
classstring''Extra CSS classes.

Every item, the radio group and the checkbox item forward their remaining attributes (data-*, id, aria-*, title) to their element, so data-testid and aria-keyshortcuts land on the menuitem. role and tabindex stay owned by the component.

DropdownMenuSeparator takes no props.

SchemeAsset

PropTypeDefaultDescription
lightSnippet–Required. Rendered while the light scheme is active.
darkSnippet–Required. Rendered while the dark scheme is active.
classstring''Extra CSS classes.