Dropdown Menu

Displays a menu to the user, such as a set of actions or functions, triggered by a button.

Basic
Complex
Sides
With Icons
With Shortcuts
With Submenu
With Checkboxes
Checkboxes with Icons
With Radio Group
Radio with Icons
With Destructive Items
With Avatar
In Dialog
With Inset

Installation

Install the dependencies

pnpm add cn github:sveltery/base

Copy the component

Copy the following files into src/lib/components/ui/dropdown-menu.

src/lib/components/ui/dropdown-menu/DropdownMenuCheckboxItem.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Menu as Primitive } from '@sveltery/base/menu';
  import { cn } from 'cn';
  import CheckIcon from '@lucide/svelte/icons/check';
  let { children, class: classProp, checked = $bindable(false), inset, ...props }: ComponentProps<typeof Primitive.CheckboxItem> & { inset?: boolean } = $props();
</script>
<Primitive.CheckboxItem
  data-slot="dropdown-menu-checkbox-item"
  data-inset={inset}
  bind:checked
  {...props}
  class={cn("focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", classProp)}
>
  <span class="absolute right-2 flex items-center justify-center pointer-events-none" data-slot="dropdown-menu-checkbox-item-indicator">
    <Primitive.CheckboxItemIndicator>
      <CheckIcon />
    </Primitive.CheckboxItemIndicator>
  </span>
  {@render children?.()}
</Primitive.CheckboxItem>
src/lib/components/ui/dropdown-menu/DropdownMenuContent.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Menu as Primitive } from '@sveltery/base/menu';
  import { cn } from 'cn';
  let {
    children,
    class: classProp,
    align = 'start',
    alignOffset = 0,
    side = 'bottom',
    sideOffset = 4,
    ...props
  }: ComponentProps<typeof Primitive.Popup> & Pick<ComponentProps<typeof Primitive.Positioner>, 'align' | 'alignOffset' | 'side' | 'sideOffset'> = $props();
</script>
<Primitive.Portal>
  <Primitive.Positioner {align} {alignOffset} {side} {sideOffset} class="isolate z-50 outline-none">
    <Primitive.Popup data-slot="dropdown-menu-content" {...props} class={cn('data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden', classProp)}>
      {@render children?.()}
    </Primitive.Popup>
  </Primitive.Positioner>
</Primitive.Portal>
src/lib/components/ui/dropdown-menu/DropdownMenuGroup.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Menu as Primitive } from '@sveltery/base/menu';
  let { children, ...props }: ComponentProps<typeof Primitive.Group> = $props();
</script>
<Primitive.Group data-slot="dropdown-menu-group" {children} {...props} />
src/lib/components/ui/dropdown-menu/DropdownMenuItem.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Menu as Primitive } from '@sveltery/base/menu';
  import { cn } from 'cn';
  let {
    children,
    class: classProp,
    inset,
    variant = 'default',
    ...props
  }: ComponentProps<typeof Primitive.Item> & { inset?: boolean; variant?: 'default' | 'destructive' } = $props();
</script>
<Primitive.Item
  data-slot="dropdown-menu-item"
  data-inset={inset}
  data-variant={variant}
  {children}
  {...props}
  class={cn("focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", classProp)}
/>
src/lib/components/ui/dropdown-menu/DropdownMenuLabel.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Menu as Primitive } from '@sveltery/base/menu';
  import { cn } from 'cn';
  let { children, class: classProp, inset, ...props }: ComponentProps<typeof Primitive.GroupLabel> & { inset?: boolean } = $props();
</script>
<Primitive.GroupLabel data-slot="dropdown-menu-label" data-inset={inset} {children} {...props} class={cn('text-muted-foreground px-1.5 py-1 text-xs font-medium data-inset:pl-7', classProp)} />
src/lib/components/ui/dropdown-menu/DropdownMenuPortal.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Menu as Primitive } from '@sveltery/base/menu';
  let { children, ...props }: ComponentProps<typeof Primitive.Portal> = $props();
</script>
<Primitive.Portal data-slot="dropdown-menu-portal" {...props}>
  {@render children?.()}
</Primitive.Portal>
src/lib/components/ui/dropdown-menu/DropdownMenuRadioGroup.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Menu as Primitive } from '@sveltery/base/menu';
  let { children, value = $bindable(), ...props }: ComponentProps<typeof Primitive.RadioGroup> = $props();
</script>
<Primitive.RadioGroup data-slot="dropdown-menu-radio-group" bind:value {children} {...props} />
src/lib/components/ui/dropdown-menu/DropdownMenuRadioItem.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Menu as Primitive } from '@sveltery/base/menu';
  import { cn } from 'cn';
  import CheckIcon from '@lucide/svelte/icons/check';
  let { children, class: classProp, inset, ...props }: ComponentProps<typeof Primitive.RadioItem> & { inset?: boolean } = $props();
</script>
<Primitive.RadioItem
  data-slot="dropdown-menu-radio-item"
  data-inset={inset}
  {...props}
  class={cn("focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", classProp)}
>
  <span class="absolute right-2 flex items-center justify-center pointer-events-none" data-slot="dropdown-menu-radio-item-indicator">
    <Primitive.RadioItemIndicator>
      <CheckIcon />
    </Primitive.RadioItemIndicator>
  </span>
  {@render children?.()}
</Primitive.RadioItem>
src/lib/components/ui/dropdown-menu/DropdownMenuSeparator.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Menu as Primitive } from '@sveltery/base/menu';
  import { cn } from 'cn';
  let { class: classProp, ...props }: ComponentProps<typeof Primitive.Separator> = $props();
</script>
<Primitive.Separator data-slot="dropdown-menu-separator" {...props} class={cn('bg-border -mx-1 my-1 h-px', classProp)} />
src/lib/components/ui/dropdown-menu/DropdownMenuShortcut.svelte
<script lang="ts">
  import type { HTMLAttributes } from 'svelte/elements';
  import { cn } from 'cn';
  let { children, class: classProp, ...props }: HTMLAttributes<HTMLSpanElement> = $props();
</script>
<span data-slot="dropdown-menu-shortcut" {...props} class={cn('text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest', classProp)}>
  {@render children?.()}
</span>
src/lib/components/ui/dropdown-menu/DropdownMenuSubContent.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { cn } from 'cn';
  import DropdownMenuContent from './DropdownMenuContent.svelte';
  let { class: classProp, align = 'start', alignOffset = -3, side = 'right', sideOffset = 0, ...props }: ComponentProps<typeof DropdownMenuContent> = $props();
</script>
<DropdownMenuContent
  data-slot="dropdown-menu-sub-content"
  {...props}
  class={cn('data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-[96px] rounded-lg p-1 shadow-lg ring-1 duration-100 w-auto', classProp)}
  {align}
  {alignOffset}
  {side}
  {sideOffset}
/>
src/lib/components/ui/dropdown-menu/DropdownMenuSubTrigger.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Menu as Primitive } from '@sveltery/base/menu';
  import { cn } from 'cn';
  import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
  let { children, class: classProp, inset, ...props }: ComponentProps<typeof Primitive.SubmenuTrigger> & { inset?: boolean } = $props();
</script>
<Primitive.SubmenuTrigger
  data-slot="dropdown-menu-sub-trigger"
  data-inset={inset}
  {...props}
  class={cn("focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-1.5 py-1 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-popup-open:bg-accent data-popup-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0", classProp)}
>
  {@render children?.()}
  <ChevronRightIcon class="ml-auto" />
</Primitive.SubmenuTrigger>
src/lib/components/ui/dropdown-menu/DropdownMenuTrigger.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Menu as Primitive } from '@sveltery/base/menu';
  let { children, ...props }: ComponentProps<typeof Primitive.Trigger> = $props();
</script>
<Primitive.Trigger data-slot="dropdown-menu-trigger" {children} {...props} />
src/lib/components/ui/dropdown-menu/index.ts
import { Menu as Primitive } from '@sveltery/base/menu';

export const DropdownMenu = Primitive.Root as typeof Primitive.Root;
export const DropdownMenuSub = Primitive.SubmenuRoot;
export { default as DropdownMenuPortal } from './DropdownMenuPortal.svelte';
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.svelte';
export { default as DropdownMenuContent } from './DropdownMenuContent.svelte';
export { default as DropdownMenuGroup } from './DropdownMenuGroup.svelte';
export { default as DropdownMenuLabel } from './DropdownMenuLabel.svelte';
export { default as DropdownMenuItem } from './DropdownMenuItem.svelte';
export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.svelte';
export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.svelte';
export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.svelte';
export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.svelte';
export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.svelte';
export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.svelte';
export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.svelte';

Update the import paths

Change the imports to match your project setup.

Usage

<script lang="ts">
  import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '$lib/components/ui/dropdown-menu';
</script>
<DropdownMenu>
  <DropdownMenuTrigger>Open</DropdownMenuTrigger>
  <DropdownMenuContent>
    <DropdownMenuGroup>
      <DropdownMenuLabel>My Account</DropdownMenuLabel>
      <DropdownMenuItem>Profile</DropdownMenuItem>
      <DropdownMenuItem>Billing</DropdownMenuItem>
    </DropdownMenuGroup>
    <DropdownMenuSeparator />
    <DropdownMenuItem variant="destructive">Log out</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>

The arrow keys move the highlight, Enter or a click activates an item and closes the menu, and Escape closes it and returns focus to the trigger. Bind open on DropdownMenu to read or change the state from outside.

Checkboxes and radio groups

Bind checked on DropdownMenuCheckboxItem and value on DropdownMenuRadioGroup. Neither closes the menu when picked.

<script lang="ts">
  let showStatusBar = $state(true);
  let position = $state('bottom');
</script>

<DropdownMenuContent>
  <DropdownMenuCheckboxItem bind:checked={showStatusBar}>Status Bar</DropdownMenuCheckboxItem>
  <DropdownMenuSeparator />
  <DropdownMenuRadioGroup bind:value={position}>
    <DropdownMenuRadioItem value="top">Top</DropdownMenuRadioItem>
    <DropdownMenuRadioItem value="bottom">Bottom</DropdownMenuRadioItem>
  </DropdownMenuRadioGroup>
</DropdownMenuContent>

Wrap a DropdownMenuSubTrigger and a DropdownMenuSubContent in DropdownMenuSub. The submenu opens on hover, on Enter and on the arrow key toward its side, and the opposite arrow key closes it.

<DropdownMenuSub>
  <DropdownMenuSubTrigger>Invite users</DropdownMenuSubTrigger>
  <DropdownMenuPortal>
    <DropdownMenuSubContent>
      <DropdownMenuItem>Email</DropdownMenuItem>
      <DropdownMenuItem>Message</DropdownMenuItem>
    </DropdownMenuSubContent>
  </DropdownMenuPortal>
</DropdownMenuSub>

Trigger

DropdownMenuTrigger renders a <button>. Pass class to style it, for example with buttonVariants. To use another element, pass a render snippet and spread its props onto that element.

API reference

DropdownMenu is styled on top of the Sveltery Base Menu primitive, which owns its keyboard navigation, typeahead, focus handling and dismissal. DropdownMenuContent renders the Base portal, positioner and popup, and takes the positioner's side, align, sideOffset and alignOffset props. DropdownMenuItem takes inset and variant (default or destructive). See the Base UI Menu documentation for the behavior it ports.