Menu

Change language

GitHub repo

Context Menu

context-menuui

Right-click menu with items, checkbox/radio items, submenus, labels, and shortcuts. Hand-rolled runes behavior: cursor-anchored portal, viewport clamping, outside/Escape close, and arrow-key navigation with roving focus.

Installation

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

Examples

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
variant
'default''destructive'
defaultoptional
inset
'true''false'
—optional
openboolean—optional
onOpenChange(open: boolean) => void—optional
childrenSnippet—optional

Schema

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

MenuPosition
interface MenuPosition {
  x: number
  y: number
}
ContextMenuContextValue
interface ContextMenuContextValue {
  readonly open: boolean
  openAt: (position: MenuPosition) => void
  close: () => void
  readonly position: MenuPosition
  registerMenuElement: (el: HTMLElement) => void
  unregisterMenuElement: (el: HTMLElement) => void
  registerTrigger: (el: HTMLElement | null) => void
  isInsideMenu: (target: EventTarget | null) => boolean
}
ContextMenuSubContextValue
interface ContextMenuSubContextValue {
  readonly open: boolean
  setOpen: (open: boolean) => void
  getTriggerElement: () => HTMLElement | null
  registerTriggerElement: (el: HTMLElement | null) => void
  registerContentElement: (el: HTMLElement | null) => void
  focusFirstItem: () => void
  focusTrigger: () => void
}
ContextMenuRadioContextValue
interface ContextMenuRadioContextValue {
  readonly value: string
  setValue: (value: string) => void
}

Used by

Files installed (19)

  • src/lib/components/ui/context-menu/ContextMenu.svelte2.2 kB
    <script lang="ts" module>
      import type { Snippet } from 'svelte'
    
      export interface ContextMenuProps {
        open?: boolean
        onOpenChange?: (open: boolean) => void
        children?: Snippet
      }
    </script>
    
    <script lang="ts">
      import { setContextMenuContext, type MenuPosition } from './context'
    
      let { open = $bindable(false), onOpenChange, children }: ContextMenuProps = $props()
    
      let position = $state<MenuPosition>({ x: 0, y: 0 })
      const menuElements = new Set<HTMLElement>()
      let triggerEl: HTMLElement | null = null
    
      function setOpen(next: boolean) {
        if (next === open) return
        open = next
        onOpenChange?.(next)
      }
    
      function isInsideMenu(target: EventTarget | null): boolean {
        if (!(target instanceof HTMLElement)) return false
        if (triggerEl?.contains(target)) return true
        for (const el of menuElements) {
          if (el.contains(target)) return true
        }
        return false
      }
    
      $effect(() => {
        if (!open) return
        const onPointerDown = (e: PointerEvent) => {
          if (!isInsideMenu(e.target)) setOpen(false)
        }
        const onContextMenu = (e: MouseEvent) => {
          // Right-clicking elsewhere moves or closes the menu; the new trigger (if
          // any) reopens at its own position.
          if (!isInsideMenu(e.target)) setOpen(false)
        }
        const onKeyDown = (e: KeyboardEvent) => {
          if (e.key === 'Escape') {
            e.preventDefault()
            setOpen(false)
          }
        }
        window.addEventListener('pointerdown', onPointerDown, true)
        window.addEventListener('contextmenu', onContextMenu, true)
        window.addEventListener('keydown', onKeyDown)
        return () => {
          window.removeEventListener('pointerdown', onPointerDown, true)
          window.removeEventListener('contextmenu', onContextMenu, true)
          window.removeEventListener('keydown', onKeyDown)
        }
      })
    
      setContextMenuContext({
        get open() {
          return open
        },
        openAt(pos) {
          position = pos
          setOpen(true)
        },
        close() {
          setOpen(false)
        },
        get position() {
          return position
        },
        registerMenuElement(el) {
          menuElements.add(el)
        },
        unregisterMenuElement(el) {
          menuElements.delete(el)
        },
        registerTrigger(el) {
          triggerEl = el
        },
        isInsideMenu,
      })
    </script>
    
    {@render children?.()}
    
  • src/lib/components/ui/context-menu/ContextMenuCheckboxItem.svelte2.4 kB
  • src/lib/components/ui/context-menu/ContextMenuContent.svelte3 kB
  • src/lib/components/ui/context-menu/ContextMenuGroup.svelte0.5 kB
  • src/lib/components/ui/context-menu/ContextMenuItem.svelte1.7 kB
  • src/lib/components/ui/context-menu/context-menu-item.variants.ts1.3 kB
  • src/lib/components/ui/context-menu/ContextMenuLabel.svelte0.7 kB
  • src/lib/components/ui/context-menu/ContextMenuPortal.svelte0.4 kB
  • src/lib/components/ui/context-menu/ContextMenuRadioGroup.svelte0.8 kB
  • src/lib/components/ui/context-menu/ContextMenuRadioItem.svelte2.3 kB
  • src/lib/components/ui/context-menu/ContextMenuSeparator.svelte0.5 kB
  • src/lib/components/ui/context-menu/ContextMenuShortcut.svelte0.6 kB
  • src/lib/components/ui/context-menu/ContextMenuSub.svelte1.1 kB
  • src/lib/components/ui/context-menu/ContextMenuSubContent.svelte3.4 kB
  • src/lib/components/ui/context-menu/ContextMenuSubTrigger.svelte2.5 kB
  • src/lib/components/ui/context-menu/ContextMenuTrigger.svelte1.2 kB
  • src/lib/components/ui/context-menu/context.ts2.6 kB
  • src/lib/components/ui/context-menu/portal.ts0.5 kB
  • src/lib/components/ui/context-menu/index.ts1.7 kB

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