Menu

Change language

GitHub repo

Dropdown Menu

dropdown-menuui

Floating menu launched from a trigger button — for account switchers, row actions, editor menus, and any short list of commands. Supports labels, icons, separators, keyboard shortcuts, checkbox/radio items, and nested submenus. Hand-rolled on Svelte 5 runes (no headless dependency); ARIA + keyboard navigation handled.

Installation

$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/dropdown-menu.json
Manifest:https://uipkge.dev/r/svelte/dropdown-menu.jsonInstalls to:src/lib/components/ui/dropdown-menu/

Examples

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
side
'bottom''left''right''top'
—optional
open

Controlled open state. Use `bind:open` for two-way binding.

boolean—optional
onOpenChange

Called whenever the open state changes.

(open: boolean) => void—optional
childrenSnippet—optional

Schema

Type aliases from this item's source — use them to shape the data you pass in.

DropdownMenuContextValue
interface DropdownMenuContextValue {
  readonly ids: { trigger: string; content: string }
  isOpen: () => boolean
  setOpen: (open: boolean) => void
  closeAndFocusTrigger: () => void
}
DropdownMenuRadioContextValue
interface DropdownMenuRadioContextValue {
  getValue: () => string
  setValue: (value: string) => void
}
DropdownMenuSubContextValue
interface DropdownMenuSubContextValue {
  isSubOpen: () => boolean
  setSubOpen: (open: boolean) => void
  /** Close after a grace period so the pointer can travel between the
   *  trigger and the panel. Any setSubOpen call cancels the pending close. */
  scheduleClose: (delay?: number) => void
  readonly ids: { trigger: string; content: string }
}

Used by

Files installed (19)

  • src/lib/components/ui/dropdown-menu/DropdownMenu.svelte2.3 kB
    <script lang="ts" module>
      import type { Snippet } from 'svelte'
    
      export interface DropdownMenuProps {
        /** Controlled open state. Use `bind:open` for two-way binding. */
        open?: boolean
        /** Called whenever the open state changes. */
        onOpenChange?: (open: boolean) => void
        children?: Snippet
      }
    </script>
    
    <script lang="ts">
      import { focusMenuItem, setMenuContext } from './dropdown-menu-context'
    
      let { open = $bindable(false), onOpenChange, children }: DropdownMenuProps = $props()
    
      const uid = $props.id()
      const ids = { trigger: `${uid}-trigger`, content: `${uid}-content` }
    
      function setOpen(next: boolean) {
        if (open === next) return
        open = next
        onOpenChange?.(next)
      }
    
      function closeAndFocusTrigger() {
        setOpen(false)
        document.getElementById(ids.trigger)?.focus()
      }
    
      setMenuContext({
        ids,
        isOpen: () => open,
        setOpen,
        closeAndFocusTrigger,
      })
    
      $effect(() => {
        if (!open) return
        function onPointerDown(e: PointerEvent) {
          const target = e.target as Node | null
          if (!target || !(target instanceof Element)) return
          // Clicks inside the trigger, content, or any sub-content keep the menu open.
          if (target.closest(`#${CSS.escape(ids.trigger)}, #${CSS.escape(ids.content)}, [data-slot="dropdown-menu-sub-content"]`))
            return
          setOpen(false)
        }
        function onKeyDown(e: KeyboardEvent) {
          if (e.key === 'Escape') closeAndFocusTrigger()
          // Tabbing away dismisses; focus continues naturally.
          if (e.key === 'Tab') setOpen(false)
          // Arrowing from a focused trigger jumps into the menu (covers both the
          // default button and `child` triggers like Button).
          if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && document.activeElement?.id === ids.trigger) {
            e.preventDefault()
            focusMenuItem(ids.content, e.key === 'ArrowDown' ? 'first' : 'last')
          }
        }
        // Capture phase so a trigger toggle click (bubble) doesn't instantly re-close.
        document.addEventListener('pointerdown', onPointerDown, true)
        document.addEventListener('keydown', onKeyDown)
        return () => {
          document.removeEventListener('pointerdown', onPointerDown, true)
          document.removeEventListener('keydown', onKeyDown)
        }
      })
    </script>
    
    {@render children?.()}
    
  • src/lib/components/ui/dropdown-menu/DropdownMenuCheckboxItem.svelte2.2 kB
  • src/lib/components/ui/dropdown-menu/DropdownMenuContent.svelte4.2 kB
  • src/lib/components/ui/dropdown-menu/dropdown-menu-content.variants.ts1.5 kB
  • src/lib/components/ui/dropdown-menu/dropdown-menu-context.ts2.7 kB
  • src/lib/components/ui/dropdown-menu/DropdownMenuGroup.svelte0.4 kB
  • src/lib/components/ui/dropdown-menu/DropdownMenuItem.svelte2.1 kB
  • src/lib/components/ui/dropdown-menu/DropdownMenuLabel.svelte0.6 kB
  • src/lib/components/ui/dropdown-menu/DropdownMenuRadioGroup.svelte1 kB
  • src/lib/components/ui/dropdown-menu/DropdownMenuRadioItem.svelte2.1 kB
  • src/lib/components/ui/dropdown-menu/DropdownMenuSeparator.svelte0.5 kB
  • src/lib/components/ui/dropdown-menu/DropdownMenuShortcut.svelte0.6 kB
  • src/lib/components/ui/dropdown-menu/DropdownMenuSub.svelte1.3 kB
  • src/lib/components/ui/dropdown-menu/DropdownMenuSubContent.svelte3.4 kB
  • src/lib/components/ui/dropdown-menu/DropdownMenuSubTrigger.svelte2.5 kB
  • src/lib/components/ui/dropdown-menu/DropdownMenuTrigger.svelte1.8 kB
  • src/lib/components/ui/dropdown-menu/DropdownMenuPortal.svelte0.4 kB
  • src/lib/components/ui/dropdown-menu/portal.ts0.5 kB
  • src/lib/components/ui/dropdown-menu/index.ts1.7 kB

Raw manifest:https://uipkge.dev/r/svelte/dropdown-menu.json