Menu

Change language

GitHub repo

Menubar

menubarui

Top-level menu bar — File / Edit / View — for desktop-style apps. Same primitives as Dropdown Menu but laid out horizontally and keyboard-navigable across siblings (left/right arrows).

Installation

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

Examples

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
value

Controlled open menu id. When omitted the menubar is uncontrolled.

string—optional
defaultValuestring—optional
loop

When false, arrow keys stop at the first/last trigger instead of wrapping.

booleantrueoptional
refHTMLDivElement | null—optional

Schema

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

MenubarRootContextValue
interface MenubarRootContextValue {
  getOpenMenu: () => string | null
  setOpenMenu: (id: string | null, refocusTrigger?: boolean) => void
  registerTrigger: (id: string, getEl: () => HTMLElement | null) => () => void
  /** Focus the sibling trigger in `dir`; returns the newly focused menu id. */
  focusSiblingTrigger: (id: string, dir: 1 | -1) => string | null
  getRootEl: () => HTMLElement | null
}
MenubarMenuContextValue
interface MenubarMenuContextValue {
  id: string
  triggerId: string
  contentId: string
  isOpen: () => boolean
  getTriggerEl: () => HTMLElement | null
  setTriggerEl: (el: HTMLElement | null) => void
  getContentEl: () => HTMLElement | null
  setContentEl: (el: HTMLElement | null) => void
}
MenubarScopeContextValue
interface MenubarScopeContextValue {
  registerItem: (el: HTMLElement, isDisabled: () => boolean) => () => void
  moveFocus: (current: HTMLElement | null, target: 1 | -1 | 'first' | 'last') => void
  closeScope: (refocusTrigger: boolean) => void
}
MenubarSubContextValue
interface MenubarSubContextValue {
  isOpen: () => boolean
  setOpen: (open: boolean) => void
  getTriggerEl: () => HTMLElement | null
  setTriggerEl: (el: HTMLElement | null) => void
  /**
   * The scope enclosing the submenu (captured before the sub shadows it).
   * The sub trigger registers here so parent-menu arrows flow through it.
   */
  parentScope: MenubarScopeContextValue | null
}
MenubarRadioContextValue
interface MenubarRadioContextValue {
  getValue: () => string | undefined
  setValue: (value: string) => void
}

npm dependencies

Files installed (18)

  • src/lib/components/ui/menubar/Menubar.svelte3 kB
    <script lang="ts" module>
      import type { HTMLAttributes } from 'svelte/elements'
    
      export interface MenubarProps extends HTMLAttributes<HTMLDivElement> {
        /** Controlled open menu id. When omitted the menubar is uncontrolled. */
        value?: string
        defaultValue?: string
        /** When false, arrow keys stop at the first/last trigger instead of wrapping. */
        loop?: boolean
        ref?: HTMLDivElement | null
      }
    </script>
    
    <script lang="ts">
      import { cn } from '$lib/utils'
      import { setMenubarRootContext } from './MenubarContext'
    
      let {
        class: className,
        value = $bindable(),
        defaultValue,
        loop = true,
        children,
        ref = $bindable(null),
        ...restProps
      }: MenubarProps = $props()
    
      let seeded = false
      $effect.pre(() => {
        if (!seeded && value === undefined && defaultValue !== undefined) {
          value = defaultValue
        }
        seeded = true
      })
    
      let rootEl: HTMLDivElement | null = $state(null)
      $effect(() => {
        ref = rootEl
      })
    
      const triggers: Array<{ id: string; getEl: () => HTMLElement | null }> = []
    
      function focusTriggerById(id: string) {
        triggers.find((t) => t.id === id)?.getEl()?.focus()
      }
    
      setMenubarRootContext({
        getOpenMenu: () => value ?? null,
        setOpenMenu: (id, refocusTrigger = false) => {
          const prev = value ?? null
          value = id ?? undefined
          if (refocusTrigger && prev) {
            requestAnimationFrame(() => focusTriggerById(prev))
          }
        },
        registerTrigger(id, getEl) {
          const entry = { id, getEl }
          triggers.push(entry)
          return () => {
            const i = triggers.indexOf(entry)
            if (i >= 0) triggers.splice(i, 1)
          }
        },
        focusSiblingTrigger(id, dir) {
          const live = triggers.filter((t) => t.getEl()?.isConnected)
          if (live.length === 0) return null
          const index = Math.max(0, live.findIndex((t) => t.id === id))
          let next = index + dir
          if (loop) next = (next + live.length) % live.length
          else next = Math.min(live.length - 1, Math.max(0, next))
          const target = live[next]!
          target.getEl()?.focus()
          return target.id
        },
        getRootEl: () => rootEl,
      })
    
      // Outside pointer down closes the open menu (no portal here — the open
      // content renders inline inside its menu, so "outside root" is the test).
      $effect(() => {
        if (value == null) return
        function onPointerDown(e: PointerEvent) {
          if (rootEl && !rootEl.contains(e.target as Node)) value = undefined
        }
        // Register async so the opening click itself doesn't immediately close.
        const t = setTimeout(() => document.addEventListener('pointerdown', onPointerDown), 0)
        return () => {
          clearTimeout(t)
          document.removeEventListener('pointerdown', onPointerDown)
        }
      })
    </script>
    
    <div
      bind:this={rootEl}
      role="menubar"
      aria-orientation="horizontal"
      data-uipkge=""
      data-slot="menubar"
      class={cn('bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs', className)}
      {...restProps}
    >
      {@render children?.()}
    </div>
    
  • src/lib/components/ui/menubar/MenubarCheckboxItem.svelte3.5 kB
  • src/lib/components/ui/menubar/MenubarContent.svelte2.7 kB
  • src/lib/components/ui/menubar/MenubarContext.ts4.3 kB
  • src/lib/components/ui/menubar/MenubarGroup.svelte0.5 kB
  • src/lib/components/ui/menubar/MenubarItem.svelte2.9 kB
  • src/lib/components/ui/menubar/MenubarLabel.svelte0.6 kB
  • src/lib/components/ui/menubar/MenubarMenu.svelte1.7 kB
  • src/lib/components/ui/menubar/MenubarRadioGroup.svelte0.7 kB
  • src/lib/components/ui/menubar/MenubarRadioItem.svelte3 kB
  • src/lib/components/ui/menubar/MenubarSeparator.svelte0.6 kB
  • src/lib/components/ui/menubar/MenubarShortcut.svelte0.6 kB
  • src/lib/components/ui/menubar/MenubarSub.svelte1.4 kB
  • src/lib/components/ui/menubar/MenubarSubContent.svelte1.9 kB
  • src/lib/components/ui/menubar/MenubarSubTrigger.svelte2.9 kB
  • src/lib/components/ui/menubar/MenubarTrigger.svelte2.8 kB
  • src/lib/components/ui/menubar/MenubarPortal.svelte0.6 kB
  • src/lib/components/ui/menubar/index.ts1.5 kB

Raw manifest:https://uipkge.dev/r/svelte/menubar.json