Menubar
menubaruiTop-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
$pnpm dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/menubar.json$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/menubar.json$yarn dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/menubar.json$bunx shadcn-svelte@latest add https://uipkge.dev/r/svelte/menubar.jsonManifest:
https://uipkge.dev/r/svelte/menubar.jsonInstalls to:src/lib/components/ui/menubar/Examples
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
valueControlled open menu id. When omitted the menubar is uncontrolled. | string | — | optional |
defaultValue | string | — | optional |
loopWhen false, arrow keys stop at the first/last trigger instead of wrapping. | boolean | true | optional |
ref | HTMLDivElement | null | — | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
MenubarRootContextValueinterface 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
}MenubarMenuContextValueinterface 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
}MenubarScopeContextValueinterface MenubarScopeContextValue {
registerItem: (el: HTMLElement, isDisabled: () => boolean) => () => void
moveFocus: (current: HTMLElement | null, target: 1 | -1 | 'first' | 'last') => void
closeScope: (refocusTrigger: boolean) => void
}MenubarSubContextValueinterface 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
}MenubarRadioContextValueinterface 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
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLAttributes } from 'svelte/elements' export interface MenubarCheckboxItemProps extends HTMLAttributes<HTMLDivElement> { checked?: boolean | 'indeterminate' defaultChecked?: boolean disabled?: boolean /** Indicator icon override (default: Check). */ indicatorIcon?: Snippet ref?: HTMLDivElement | null } </script> <script lang="ts"> import { Check } from '@lucide/svelte' import { cn } from '$lib/utils' import { getMenubarScopeContext } from './MenubarContext' let { class: className, checked = $bindable(), defaultChecked = false, disabled = false, indicatorIcon, children, ref = $bindable(null), onclick, onkeydown, ...restProps }: MenubarCheckboxItemProps = $props() const scope = getMenubarScopeContext() let seeded = false $effect.pre(() => { if (!seeded && checked === undefined) checked = defaultChecked seeded = true }) let el: HTMLDivElement | null = $state(null) $effect(() => { ref = el }) $effect(() => { if (el && scope) { const isDisabled = () => disabled return scope.registerItem(el, isDisabled) } }) // NOTE: named `checkState`, not `state` — a local `state` variable shadows // the `$state` rune (Svelte reads `$state` as a store subscription then). const checkState = $derived(checked === 'indeterminate' ? 'indeterminate' : checked ? 'checked' : 'unchecked') </script> <div bind:this={el} role="menuitemcheckbox" tabindex="-1" aria-checked={checked === 'indeterminate' ? 'mixed' : Boolean(checked)} aria-disabled={disabled || undefined} data-uipkge="" data-slot="menubar-checkbox-item" data-state={checkState} data-disabled={disabled ? '' : undefined} class={cn( "focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus-visible:ring-2 focus-visible:ring-inset data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} onclick={(e) => { if (disabled) return checked = checked === true ? false : true // Checkbox items toggle without closing the menu. onclick?.(e) }} onmouseenter={(e) => { if (!disabled) (e.currentTarget as HTMLElement).focus({ preventScroll: true }) }} onkeydown={(e) => { const target = e.currentTarget as HTMLElement if (e.key === 'ArrowDown') { e.preventDefault() scope?.moveFocus(target, 1) } else if (e.key === 'ArrowUp') { e.preventDefault() scope?.moveFocus(target, -1) } else if (e.key === 'Home') { e.preventDefault() scope?.moveFocus(target, 'first') } else if (e.key === 'End') { e.preventDefault() scope?.moveFocus(target, 'last') } else if (e.key === 'Enter' || e.key === ' ') { if (!disabled) { e.preventDefault() target.click() } } onkeydown?.(e) }} {...restProps} > <span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> {#if checked === 'indeterminate'} <span class="size-2 rounded-[1px] bg-current" aria-hidden="true"></span> {:else if checked} {#if indicatorIcon} {@render indicatorIcon()} {:else} <Check class="size-4" aria-hidden="true" /> {/if} {/if} </span> {@render children?.()} </div>src/lib/components/ui/menubar/MenubarContent.svelte2.7 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface MenubarContentProps extends HTMLAttributes<HTMLDivElement> { align?: 'start' | 'center' | 'end' alignOffset?: number sideOffset?: number /** When false, arrow keys stop at the first/last item instead of wrapping. */ loop?: boolean ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' import { getMenubarMenuContext, getMenubarRootContext } from './MenubarContext' let { class: className, align = 'start', alignOffset = -4, sideOffset = 8, loop: _loop = true, children, ref = $bindable(null), onkeydown, ...restProps }: MenubarContentProps = $props() // Roving wrap is handled by the shared scope (always loops, like reka's // default); the prop is destructured for API parity and otherwise unused. const root = getMenubarRootContext() const menu = getMenubarMenuContext() let el: HTMLDivElement | null = $state(null) $effect(() => { ref = el menu?.setContentEl(el) }) const isOpen = $derived(menu?.isOpen() ?? false) </script> {#if isOpen && menu} <!-- No portal: the content renders inline, absolutely positioned under its trigger (the menu wrapper is relative). Exit animations don't run on unmount — enter animations still play via data-[state=open]. --> <div bind:this={el} role="menu" id={menu.contentId} aria-labelledby={menu.triggerId} aria-orientation="vertical" data-uipkge="" data-slot="menubar-content" data-state="open" data-align={align} class={cn( 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 absolute top-full z-50 min-w-48 origin-top overflow-hidden rounded-md border p-1 shadow-md', align === 'start' && 'left-0', align === 'center' && 'left-1/2 -translate-x-1/2', align === 'end' && 'right-0', className, )} style:margin-top="{sideOffset}px" style:margin-left={align === 'start' || align === 'center' ? `${alignOffset}px` : undefined} style:margin-right={align === 'end' ? `${-alignOffset}px` : undefined} onkeydown={(e) => { if (e.key === 'Escape') { e.preventDefault() root?.setOpenMenu(null, true) } else if (e.key === 'Tab') { root?.setOpenMenu(null) } onkeydown?.(e) }} {...restProps} > {@render children?.()} </div> {/if}src/lib/components/ui/menubar/MenubarContext.ts4.3 kB
import { getContext, setContext } from 'svelte' /** Root-level open-menu state + trigger registry for sibling navigation. */ export 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 } /** Trigger/content pairing for one top-level menu. */ export 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 } /** * Roving-focus scope. Provided by MenubarMenu for its content and shadowed by * MenubarSub for its flyout, so arrow keys stay inside the nearest open menu. */ export interface MenubarScopeContextValue { registerItem: (el: HTMLElement, isDisabled: () => boolean) => () => void moveFocus: (current: HTMLElement | null, target: 1 | -1 | 'first' | 'last') => void closeScope: (refocusTrigger: boolean) => void } /** Open state for one submenu. */ export 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 } /** Selected value for one radio group. */ export interface MenubarRadioContextValue { getValue: () => string | undefined setValue: (value: string) => void } const RootKey = Symbol('MenubarRoot') const MenuKey = Symbol('MenubarMenu') const ScopeKey = Symbol('MenubarScope') const SubKey = Symbol('MenubarSub') const RadioKey = Symbol('MenubarRadio') export function setMenubarRootContext(v: MenubarRootContextValue) { setContext(RootKey, v) } export function getMenubarRootContext(): MenubarRootContextValue | null { return getContext<MenubarRootContextValue | null>(RootKey) ?? null } export function setMenubarMenuContext(v: MenubarMenuContextValue) { setContext(MenuKey, v) } export function getMenubarMenuContext(): MenubarMenuContextValue | null { return getContext<MenubarMenuContextValue | null>(MenuKey) ?? null } export function setMenubarScopeContext(v: MenubarScopeContextValue) { setContext(ScopeKey, v) } export function getMenubarScopeContext(): MenubarScopeContextValue | null { return getContext<MenubarScopeContextValue | null>(ScopeKey) ?? null } export function setMenubarSubContext(v: MenubarSubContextValue) { setContext(SubKey, v) } export function getMenubarSubContext(): MenubarSubContextValue | null { return getContext<MenubarSubContextValue | null>(SubKey) ?? null } export function setMenubarRadioContext(v: MenubarRadioContextValue) { setContext(RadioKey, v) } export function getMenubarRadioContext(): MenubarRadioContextValue | null { return getContext<MenubarRadioContextValue | null>(RadioKey) ?? null } /** Shared roving-focus list with wrap-around (reka `loop` default). */ export function createItemScope(closeScope: (refocusTrigger: boolean) => void): MenubarScopeContextValue { const entries: Array<{ el: HTMLElement; isDisabled: () => boolean }> = [] return { registerItem(el, isDisabled) { const entry = { el, isDisabled } entries.push(entry) return () => { const i = entries.indexOf(entry) if (i >= 0) entries.splice(i, 1) } }, moveFocus(current, target) { const enabled = entries.filter((e) => e.el.isConnected && !e.isDisabled()) if (enabled.length === 0) return let index: number if (target === 'first') index = 0 else if (target === 'last') index = enabled.length - 1 else { const currentIndex = current ? enabled.findIndex((e) => e.el === current) : -1 index = (currentIndex + target + enabled.length) % enabled.length } enabled[index]?.el.focus({ preventScroll: true }) }, closeScope, } }src/lib/components/ui/menubar/MenubarGroup.svelte0.5 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface MenubarGroupProps extends HTMLAttributes<HTMLDivElement> { ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: MenubarGroupProps = $props() </script> <div bind:this={ref} role="group" data-uipkge="" data-slot="menubar-group" class={cn(className)} {...restProps}> {@render children?.()} </div>src/lib/components/ui/menubar/MenubarItem.svelte2.9 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface MenubarItemProps extends HTMLAttributes<HTMLDivElement> { disabled?: boolean inset?: boolean variant?: 'default' | 'destructive' ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' import { getMenubarRootContext, getMenubarScopeContext } from './MenubarContext' let { class: className, disabled = false, inset = false, variant = 'default', children, ref = $bindable(null), onclick, onkeydown, ...restProps }: MenubarItemProps = $props() const root = getMenubarRootContext() const scope = getMenubarScopeContext() let el: HTMLDivElement | null = $state(null) $effect(() => { ref = el }) $effect(() => { if (el && scope) { const isDisabled = () => disabled return scope.registerItem(el, isDisabled) } }) </script> <div bind:this={el} role="menuitem" tabindex="-1" aria-disabled={disabled || undefined} data-uipkge="" data-slot="menubar-item" data-inset={inset ? '' : undefined} data-variant={variant} data-disabled={disabled ? '' : undefined} class={cn( "focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus-visible:ring-2 focus-visible:ring-inset data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} onclick={(e) => { if (disabled) return onclick?.(e) // Plain items select + close; sub scopes bubble the close to the root. scope?.closeScope(false) root?.setOpenMenu(null) }} onmouseenter={(e) => { if (!disabled) (e.currentTarget as HTMLElement).focus({ preventScroll: true }) }} onkeydown={(e) => { const target = e.currentTarget as HTMLElement if (e.key === 'ArrowDown') { e.preventDefault() scope?.moveFocus(target, 1) } else if (e.key === 'ArrowUp') { e.preventDefault() scope?.moveFocus(target, -1) } else if (e.key === 'Home') { e.preventDefault() scope?.moveFocus(target, 'first') } else if (e.key === 'End') { e.preventDefault() scope?.moveFocus(target, 'last') } else if (e.key === 'Enter' || e.key === ' ') { if (!disabled) { e.preventDefault() target.click() } } onkeydown?.(e) }} {...restProps} > {@render children?.()} </div>src/lib/components/ui/menubar/MenubarLabel.svelte0.6 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface MenubarLabelProps extends HTMLAttributes<HTMLDivElement> { inset?: boolean ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, inset = false, children, ref = $bindable(null), ...restProps }: MenubarLabelProps = $props() </script> <div bind:this={ref} data-uipkge="" data-slot="menubar-label" data-inset={inset ? '' : undefined} class={cn('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', className)} {...restProps} > {@render children?.()} </div>src/lib/components/ui/menubar/MenubarMenu.svelte1.7 kB
<script lang="ts" module> import type { Snippet } from 'svelte' export interface MenubarMenuProps { /** * Stable id for this menu. Defaults to a generated id. Pass one when * controlling the open menu through the root `value` / `defaultValue`. */ value?: string children?: Snippet } let menuCounter = 0 </script> <script lang="ts"> import { untrack } from 'svelte' import { getMenubarRootContext, setMenubarMenuContext, setMenubarScopeContext, createItemScope } from './MenubarContext' let { value, children }: MenubarMenuProps = $props() const root = getMenubarRootContext() // Computed once: ids must stay stable for the component's lifetime (aria // references + root tracking depend on it). const id = untrack(() => value ?? `menu-${++menuCounter}`) const triggerId = `menubar-trigger-${id}` const contentId = `menubar-content-${id}` // The root is the single source of open state (see its `value` prop). const isOpen = $derived(root?.getOpenMenu() === id) function setOpen(next: boolean, refocusTrigger = false) { root?.setOpenMenu(next ? id : null, refocusTrigger) } let triggerEl: HTMLElement | null = $state(null) let contentEl: HTMLElement | null = $state(null) setMenubarMenuContext({ id, triggerId, contentId, isOpen: () => isOpen, getTriggerEl: () => triggerEl, setTriggerEl: (el) => { triggerEl = el }, getContentEl: () => contentEl, setContentEl: (el) => { contentEl = el }, }) setMenubarScopeContext(createItemScope((refocusTrigger) => setOpen(false, refocusTrigger))) </script> <div data-uipkge="" data-slot="menubar-menu" data-state={isOpen ? 'open' : 'closed'} class="relative"> {@render children?.()} </div>src/lib/components/ui/menubar/MenubarRadioGroup.svelte0.7 kB
<script lang="ts" module> import type { Snippet } from 'svelte' export interface MenubarRadioGroupProps { value?: string defaultValue?: string children?: Snippet } </script> <script lang="ts"> import { setMenubarRadioContext } from './MenubarContext' let { value = $bindable(), defaultValue, children }: MenubarRadioGroupProps = $props() let seeded = false $effect.pre(() => { if (!seeded && value === undefined && defaultValue !== undefined) value = defaultValue seeded = true }) setMenubarRadioContext({ getValue: () => value, setValue: (v) => { value = v }, }) </script> <div role="group" data-uipkge="" data-slot="menubar-radio-group"> {@render children?.()} </div>src/lib/components/ui/menubar/MenubarRadioItem.svelte3 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLAttributes } from 'svelte/elements' export interface MenubarRadioItemProps extends HTMLAttributes<HTMLDivElement> { value: string disabled?: boolean /** Indicator icon override (default: filled Circle). */ indicatorIcon?: Snippet ref?: HTMLDivElement | null } </script> <script lang="ts"> import { Circle } from '@lucide/svelte' import { cn } from '$lib/utils' import { getMenubarRadioContext, getMenubarScopeContext } from './MenubarContext' let { class: className, value, disabled = false, indicatorIcon, children, ref = $bindable(null), onclick, onkeydown, ...restProps }: MenubarRadioItemProps = $props() const scope = getMenubarScopeContext() const radio = getMenubarRadioContext() let el: HTMLDivElement | null = $state(null) $effect(() => { ref = el }) $effect(() => { if (el && scope) { const isDisabled = () => disabled return scope.registerItem(el, isDisabled) } }) const isChecked = $derived(radio?.getValue() === value) </script> <div bind:this={el} role="menuitemradio" tabindex="-1" aria-checked={isChecked} aria-disabled={disabled || undefined} data-uipkge="" data-slot="menubar-radio-item" data-state={isChecked ? 'checked' : 'unchecked'} data-disabled={disabled ? '' : undefined} class={cn( "focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus-visible:ring-2 focus-visible:ring-inset data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className, )} onclick={(e) => { if (disabled) return radio?.setValue(value) // Radio items select without closing the menu. onclick?.(e) }} onmouseenter={(e) => { if (!disabled) (e.currentTarget as HTMLElement).focus({ preventScroll: true }) }} onkeydown={(e) => { const target = e.currentTarget as HTMLElement if (e.key === 'ArrowDown') { e.preventDefault() scope?.moveFocus(target, 1) } else if (e.key === 'ArrowUp') { e.preventDefault() scope?.moveFocus(target, -1) } else if (e.key === 'Home') { e.preventDefault() scope?.moveFocus(target, 'first') } else if (e.key === 'End') { e.preventDefault() scope?.moveFocus(target, 'last') } else if (e.key === 'Enter' || e.key === ' ') { if (!disabled) { e.preventDefault() target.click() } } onkeydown?.(e) }} {...restProps} > <span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> {#if isChecked} {#if indicatorIcon} {@render indicatorIcon()} {:else} <Circle class="size-2 fill-current" aria-hidden="true" /> {/if} {/if} </span> {@render children?.()} </div>src/lib/components/ui/menubar/MenubarSeparator.svelte0.6 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface MenubarSeparatorProps extends HTMLAttributes<HTMLDivElement> { ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, ref = $bindable(null), ...restProps }: MenubarSeparatorProps = $props() </script> <div bind:this={ref} role="separator" aria-orientation="horizontal" data-uipkge="" data-slot="menubar-separator" class={cn('bg-border -mx-1 my-1 h-px', className)} {...restProps} ></div>src/lib/components/ui/menubar/MenubarShortcut.svelte0.6 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface MenubarShortcutProps extends HTMLAttributes<HTMLElement> { ref?: HTMLElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: MenubarShortcutProps = $props() </script> <span bind:this={ref} data-uipkge="" data-slot="menubar-shortcut" class={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)} {...restProps} > {@render children?.()} </span>src/lib/components/ui/menubar/MenubarSub.svelte1.4 kB
<script lang="ts" module> import type { Snippet } from 'svelte' export interface MenubarSubProps { open?: boolean defaultOpen?: boolean children?: Snippet } </script> <script lang="ts"> import { tick } from 'svelte' import { createItemScope, getMenubarScopeContext, setMenubarScopeContext, setMenubarSubContext, } from './MenubarContext' let { open = $bindable(false), defaultOpen = false, children }: MenubarSubProps = $props() let seeded = false $effect.pre(() => { if (!seeded && !open && defaultOpen) open = true seeded = true }) let triggerEl: HTMLElement | null = $state(null) function setOpen(next: boolean, refocusTrigger = false) { open = next if (refocusTrigger && !next) tick().then(() => triggerEl?.focus({ preventScroll: true })) } // Capture the enclosing scope BEFORE shadowing it below. const parentScope = getMenubarScopeContext() setMenubarSubContext({ isOpen: () => open, setOpen, getTriggerEl: () => triggerEl, setTriggerEl: (el) => { triggerEl = el }, parentScope, }) // Shadow the parent menu's roving scope so arrows stay inside the flyout. setMenubarScopeContext(createItemScope((refocusTrigger) => setOpen(false, refocusTrigger))) </script> <div data-uipkge="" data-slot="menubar-sub" data-state={open ? 'open' : 'closed'} class="relative"> {@render children?.()} </div>src/lib/components/ui/menubar/MenubarSubContent.svelte1.9 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface MenubarSubContentProps extends HTMLAttributes<HTMLDivElement> { alignOffset?: number sideOffset?: number ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' import { getMenubarSubContext } from './MenubarContext' let { class: className, alignOffset = 0, sideOffset = 8, children, ref = $bindable(null), onkeydown, ...restProps }: MenubarSubContentProps = $props() const sub = getMenubarSubContext() const isOpen = $derived(sub?.isOpen() ?? false) </script> {#if isOpen} <!-- No portal: the flyout renders inline, absolutely positioned to the right of its trigger (the sub wrapper is relative). --> <div bind:this={ref} role="menu" aria-orientation="vertical" data-uipkge="" data-slot="menubar-sub-content" data-state="open" class={cn( 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 absolute top-0 left-full z-50 min-w-[8rem] origin-top-left overflow-hidden rounded-md border p-1 shadow-lg', className, )} style:margin-left="{sideOffset}px" style:margin-top="{alignOffset}px" onkeydown={(e) => { if (e.key === 'Escape' || e.key === 'ArrowLeft') { e.preventDefault() e.stopPropagation() sub?.setOpen(false) sub?.getTriggerEl()?.focus({ preventScroll: true }) } else if (e.key === 'Tab') { sub?.setOpen(false) } onkeydown?.(e) }} {...restProps} > {@render children?.()} </div> {/if}src/lib/components/ui/menubar/MenubarSubTrigger.svelte2.9 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface MenubarSubTriggerProps extends HTMLAttributes<HTMLDivElement> { disabled?: boolean inset?: boolean ref?: HTMLDivElement | null } </script> <script lang="ts"> import { tick } from 'svelte' import { ChevronRight } from '@lucide/svelte' import { cn } from '$lib/utils' import { getMenubarScopeContext, getMenubarSubContext } from './MenubarContext' let { class: className, disabled = false, inset = false, children, ref = $bindable(null), onclick, onkeydown, ...restProps }: MenubarSubTriggerProps = $props() const sub = getMenubarSubContext() // The sub shadows the parent scope for its *content*, but the trigger // itself registers with the parent scope so arrows flow through it. const parentScope = $derived(sub?.parentScope ?? null) const subScope = getMenubarScopeContext() let el: HTMLDivElement | null = $state(null) $effect(() => { ref = el sub?.setTriggerEl(el) }) $effect(() => { if (el && parentScope) { const isDisabled = () => disabled return parentScope.registerItem(el, isDisabled) } }) const isOpen = $derived(sub?.isOpen() ?? false) </script> <div bind:this={el} role="menuitem" tabindex="-1" aria-haspopup="menu" aria-expanded={isOpen} aria-disabled={disabled || undefined} data-uipkge="" data-slot="menubar-sub-trigger" data-state={isOpen ? 'open' : 'closed'} data-inset={inset ? '' : undefined} data-disabled={disabled ? '' : undefined} class={cn( 'focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none focus-visible:ring-2 focus-visible:ring-inset data-[inset]:pl-8', className, )} onclick={(e) => { if (disabled) return sub?.setOpen(!isOpen) onclick?.(e) }} onmouseenter={(e) => { if (disabled) return ;(e.currentTarget as HTMLElement).focus({ preventScroll: true }) sub?.setOpen(true) }} onkeydown={(e) => { const target = e.currentTarget as HTMLElement if (e.key === 'ArrowDown') { e.preventDefault() parentScope?.moveFocus(target, 1) } else if (e.key === 'ArrowUp') { e.preventDefault() parentScope?.moveFocus(target, -1) } else if (e.key === 'Home') { e.preventDefault() parentScope?.moveFocus(target, 'first') } else if (e.key === 'End') { e.preventDefault() parentScope?.moveFocus(target, 'last') } else if (e.key === 'ArrowRight' || e.key === 'Enter' || e.key === ' ') { if (!disabled) { e.preventDefault() sub?.setOpen(true) tick().then(() => subScope?.moveFocus(null, 'first')) } } onkeydown?.(e) }} {...restProps} > {@render children?.()} <ChevronRight class="ml-auto size-4" aria-hidden="true" /> </div>src/lib/components/ui/menubar/MenubarTrigger.svelte2.8 kB
<script lang="ts" module> import type { HTMLButtonAttributes } from 'svelte/elements' export interface MenubarTriggerProps extends HTMLButtonAttributes { disabled?: boolean ref?: HTMLButtonElement | null } </script> <script lang="ts"> import { tick } from 'svelte' import { cn } from '$lib/utils' import { getMenubarMenuContext, getMenubarRootContext, getMenubarScopeContext } from './MenubarContext' let { class: className, disabled, children, ref = $bindable(null), onclick, onkeydown, onmouseenter, ...restProps }: MenubarTriggerProps = $props() const root = getMenubarRootContext() const menu = getMenubarMenuContext() const scope = getMenubarScopeContext() let el: HTMLButtonElement | null = $state(null) $effect(() => { ref = el menu?.setTriggerEl(el) }) $effect(() => { if (menu && root) return root.registerTrigger(menu.id, () => el) }) const isOpen = $derived(menu?.isOpen() ?? false) function openAndFocusFirst() { if (!menu || !root) return if (!isOpen) root.setOpenMenu(menu.id) tick().then(() => scope?.moveFocus(null, 'first')) } </script> <button bind:this={el} type="button" id={menu?.triggerId} aria-haspopup="menu" aria-expanded={isOpen} aria-controls={menu?.contentId} aria-disabled={disabled || undefined} data-uipkge="" data-slot="menubar-trigger" data-state={isOpen ? 'open' : 'closed'} data-disabled={disabled ? '' : undefined} {disabled} class={cn( 'focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none', className, )} onclick={(e) => { if (disabled) return if (menu && root) root.setOpenMenu(isOpen ? null : menu.id) onclick?.(e) }} onmouseenter={(e) => { // Hovering a sibling trigger while a menu is open switches to it. if (!disabled && menu && root && root.getOpenMenu() !== null && root.getOpenMenu() !== menu.id) { root.setOpenMenu(menu.id) } onmouseenter?.(e) }} onkeydown={(e) => { if (disabled) { onkeydown?.(e) return } if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') { e.preventDefault() openAndFocusFirst() } else if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') { e.preventDefault() if (menu && root) { const next = root.focusSiblingTrigger(menu.id, e.key === 'ArrowRight' ? 1 : -1) // When a menu is already open, arrows switch the open menu. if (next && root.getOpenMenu() !== null) root.setOpenMenu(next) } } else if (e.key === 'Escape') { if (isOpen && menu && root) { e.preventDefault() root.setOpenMenu(null) } } onkeydown?.(e) }} {...restProps} > {@render children?.()} </button>src/lib/components/ui/menubar/MenubarPortal.svelte0.6 kB
<script lang="ts" module> import type { Snippet } from 'svelte' export interface MenubarPortalProps { children?: Snippet } </script> <script lang="ts"> let { children }: MenubarPortalProps = $props() </script> <!-- Pass-through (not a real portal): menubar content is absolutely positioned relative to its menu wrapper, so moving it to document.body would break placement. This part exists for React import parity — wrapping content in it renders exactly as unwrapped. --> <span data-uipkge="" data-slot="menubar-portal" class="contents"> {@render children?.()} </span>src/lib/components/ui/menubar/index.ts1.5 kB
export { default as Menubar, type MenubarProps } from './Menubar.svelte' export { default as MenubarCheckboxItem, type MenubarCheckboxItemProps } from './MenubarCheckboxItem.svelte' export { default as MenubarContent, type MenubarContentProps } from './MenubarContent.svelte' export { default as MenubarGroup, type MenubarGroupProps } from './MenubarGroup.svelte' export { default as MenubarItem, type MenubarItemProps } from './MenubarItem.svelte' export { default as MenubarLabel, type MenubarLabelProps } from './MenubarLabel.svelte' export { default as MenubarMenu, type MenubarMenuProps } from './MenubarMenu.svelte' export { default as MenubarPortal, type MenubarPortalProps } from './MenubarPortal.svelte' export { default as MenubarRadioGroup, type MenubarRadioGroupProps } from './MenubarRadioGroup.svelte' export { default as MenubarRadioItem, type MenubarRadioItemProps } from './MenubarRadioItem.svelte' export { default as MenubarSeparator, type MenubarSeparatorProps } from './MenubarSeparator.svelte' export { default as MenubarShortcut, type MenubarShortcutProps } from './MenubarShortcut.svelte' export { default as MenubarSub, type MenubarSubProps } from './MenubarSub.svelte' export { default as MenubarSubContent, type MenubarSubContentProps } from './MenubarSubContent.svelte' export { default as MenubarSubTrigger, type MenubarSubTriggerProps } from './MenubarSubTrigger.svelte' export { default as MenubarTrigger, type MenubarTriggerProps } from './MenubarTrigger.svelte'
Raw manifest:https://uipkge.dev/r/svelte/menubar.json