A visually persistent menu common in desktop applications that provides quick access to a consistent set of commands.
Installation
Install the dependencies
pnpm add cn github:sveltery/baseCopy the component
Copy the following files into src/lib/components/ui/menubar. The component builds on dropdown-menu, so copy that component too.
It imports the dropdown-menu component, so copy its folder too.
<script lang="ts">
import type { ComponentProps } from 'svelte';
import { Menubar as Primitive } from '@sveltery/base/menubar';
import { cn } from 'cn';
let { children, class: classProp, ...props }: ComponentProps<typeof Primitive> = $props();
</script>
<Primitive data-slot="menubar" {children} {...props} class={cn('h-8 gap-0.5 rounded-lg border p-[3px] flex items-center', classProp)} /><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="menubar-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-1.5 pl-7 text-sm data-inset:pl-7 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="left-1.5 size-4 [&_svg:not([class*='size-'])]:size-4 pointer-events-none absolute flex items-center justify-center">
<Primitive.CheckboxItemIndicator>
<CheckIcon />
</Primitive.CheckboxItemIndicator>
</span>
{@render children?.()}
</Primitive.CheckboxItem><script lang="ts">
import type { ComponentProps } from 'svelte';
import { cn } from 'cn';
import { DropdownMenuContent } from '../dropdown-menu/index.js';
let { children, class: classProp, align = 'start', alignOffset = -4, sideOffset = 8, ...props }: ComponentProps<typeof DropdownMenuContent> = $props();
</script>
<DropdownMenuContent
data-slot="menubar-content"
{children}
{align}
{alignOffset}
{sideOffset}
{...props}
class={cn('bg-popover text-popover-foreground data-open:animate-in data-open:fade-in-0 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 min-w-36 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', classProp)}
/><script lang="ts">
import type { ComponentProps } from 'svelte';
import { DropdownMenuGroup } from '../dropdown-menu/index.js';
let { children, ...props }: ComponentProps<typeof DropdownMenuGroup> = $props();
</script>
<DropdownMenuGroup data-slot="menubar-group" {children} {...props} /><script lang="ts">
import type { ComponentProps } from 'svelte';
import { cn } from 'cn';
import { DropdownMenuItem } from '../dropdown-menu/index.js';
let { children, class: classProp, inset, variant = 'default', ...props }: ComponentProps<typeof DropdownMenuItem> = $props();
</script>
<DropdownMenuItem data-slot="menubar-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-disabled:opacity-50 data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/menubar-item", classProp)} /><script lang="ts">
import type { ComponentProps } from 'svelte';
import { cn } from 'cn';
import { DropdownMenuLabel } from '../dropdown-menu/index.js';
let { children, class: classProp, inset, ...props }: ComponentProps<typeof DropdownMenuLabel> = $props();
</script>
<DropdownMenuLabel data-slot="menubar-label" data-inset={inset} {children} {...props} class={cn('px-1.5 py-1 text-sm font-medium data-inset:pl-7', classProp)} /><script lang="ts">
import type { ComponentProps } from 'svelte';
import { DropdownMenuPortal } from '../dropdown-menu/index.js';
let { children, ...props }: ComponentProps<typeof DropdownMenuPortal> = $props();
</script>
<DropdownMenuPortal data-slot="menubar-portal" {...props}>
{@render children?.()}
</DropdownMenuPortal><script lang="ts">
import type { ComponentProps } from 'svelte';
import { DropdownMenuRadioGroup } from '../dropdown-menu/index.js';
let { children, value = $bindable(), ...props }: ComponentProps<typeof DropdownMenuRadioGroup> = $props();
</script>
<DropdownMenuRadioGroup data-slot="menubar-radio-group" bind:value {children} {...props} /><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="menubar-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-1.5 pl-7 text-sm data-disabled:opacity-50 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 [&_svg]:pointer-events-none [&_svg]:shrink-0", classProp)}
>
<span class="left-1.5 size-4 [&_svg:not([class*='size-'])]:size-4 pointer-events-none absolute flex items-center justify-center">
<Primitive.RadioItemIndicator>
<CheckIcon />
</Primitive.RadioItemIndicator>
</span>
{@render children?.()}
</Primitive.RadioItem><script lang="ts">
import type { ComponentProps } from 'svelte';
import { cn } from 'cn';
import { DropdownMenuSeparator } from '../dropdown-menu/index.js';
let { class: classProp, ...props }: ComponentProps<typeof DropdownMenuSeparator> = $props();
</script>
<DropdownMenuSeparator data-slot="menubar-separator" {...props} class={cn('bg-border -mx-1 my-1 h-px', classProp)} /><script lang="ts">
import type { ComponentProps } from 'svelte';
import { cn } from 'cn';
import { DropdownMenuShortcut } from '../dropdown-menu/index.js';
let { children, class: classProp, ...props }: ComponentProps<typeof DropdownMenuShortcut> = $props();
</script>
<DropdownMenuShortcut data-slot="menubar-shortcut" {children} {...props} class={cn('text-muted-foreground group-focus/menubar-item:text-accent-foreground text-xs tracking-widest ml-auto', classProp)} /><script lang="ts">
import type { ComponentProps } from 'svelte';
import { cn } from 'cn';
import { DropdownMenuSubContent } from '../dropdown-menu/index.js';
let { children, class: classProp, ...props }: ComponentProps<typeof DropdownMenuSubContent> = $props();
</script>
<DropdownMenuSubContent data-slot="menubar-sub-content" {children} {...props} class={cn('bg-popover text-popover-foreground 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 min-w-32 rounded-lg p-1 shadow-lg ring-1 duration-100', classProp)} /><script lang="ts">
import type { ComponentProps } from 'svelte';
import { cn } from 'cn';
import { DropdownMenuSubTrigger } from '../dropdown-menu/index.js';
let { children, class: classProp, inset, ...props }: ComponentProps<typeof DropdownMenuSubTrigger> = $props();
</script>
<DropdownMenuSubTrigger data-slot="menubar-sub-trigger" data-inset={inset} {children} {...props} class={cn("focus:bg-accent focus:text-accent-foreground data-popup-open:bg-accent data-popup-open: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", classProp)} /><script lang="ts">
import type { ComponentProps } from 'svelte';
import { cn } from 'cn';
import { DropdownMenuTrigger } from '../dropdown-menu/index.js';
let { children, class: classProp, ...props }: ComponentProps<typeof DropdownMenuTrigger> = $props();
</script>
<DropdownMenuTrigger data-slot="menubar-trigger" {children} {...props} class={cn('hover:bg-muted aria-expanded:bg-muted rounded-sm px-1.5 py-[2px] text-sm font-medium flex items-center outline-hidden select-none', classProp)} />export { DropdownMenu as MenubarMenu, DropdownMenuSub as MenubarSub } from '../dropdown-menu/index.js';
export { default as Menubar } from './Menubar.svelte';
export { default as MenubarPortal } from './MenubarPortal.svelte';
export { default as MenubarTrigger } from './MenubarTrigger.svelte';
export { default as MenubarContent } from './MenubarContent.svelte';
export { default as MenubarGroup } from './MenubarGroup.svelte';
export { default as MenubarSeparator } from './MenubarSeparator.svelte';
export { default as MenubarLabel } from './MenubarLabel.svelte';
export { default as MenubarItem } from './MenubarItem.svelte';
export { default as MenubarShortcut } from './MenubarShortcut.svelte';
export { default as MenubarCheckboxItem } from './MenubarCheckboxItem.svelte';
export { default as MenubarRadioGroup } from './MenubarRadioGroup.svelte';
export { default as MenubarRadioItem } from './MenubarRadioItem.svelte';
export { default as MenubarSubTrigger } from './MenubarSubTrigger.svelte';
export { default as MenubarSubContent } from './MenubarSubContent.svelte';Update the import paths
Change the imports to match your project setup.
Usage
<script lang="ts">
import { Menubar, MenubarContent, MenubarGroup, MenubarItem, MenubarMenu, MenubarSeparator, MenubarShortcut, MenubarTrigger } from '$lib/components/ui/menubar';
</script><Menubar>
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarGroup>
<MenubarItem>
New Tab <MenubarShortcut>⌘T</MenubarShortcut>
</MenubarItem>
<MenubarItem>New Window</MenubarItem>
</MenubarGroup>
<MenubarSeparator />
<MenubarGroup>
<MenubarItem>Share</MenubarItem>
<MenubarItem>Print</MenubarItem>
</MenubarGroup>
</MenubarContent>
</MenubarMenu>
</Menubar>Click a trigger to open its menu. While a menu is open, hovering another trigger or pressing ArrowLeft and ArrowRight switches to that menu. Escape closes the menu and returns focus to its trigger.
Composition
Use the following composition to build a Menubar:
Menubar
├── MenubarMenu
│ ├── MenubarTrigger
│ └── MenubarContent
│ ├── MenubarGroup
│ │ ├── MenubarLabel
│ │ ├── MenubarItem
│ │ └── MenubarItem
│ ├── MenubarSeparator
│ ├── MenubarGroup
│ │ ├── MenubarLabel
│ │ ├── MenubarCheckboxItem
│ │ └── MenubarCheckboxItem
│ ├── MenubarSeparator
│ ├── MenubarGroup
│ │ ├── MenubarLabel
│ │ └── MenubarRadioGroup
│ │ ├── MenubarRadioItem
│ │ └── MenubarRadioItem
│ └── MenubarSub
│ ├── MenubarSubTrigger
│ └── MenubarSubContent
│ └── MenubarGroup
│ ├── MenubarLabel
│ ├── MenubarItem
│ └── MenubarItem
└── MenubarMenu
├── MenubarTrigger
└── MenubarContent
└── MenubarGroup
├── MenubarLabel
├── MenubarItem
└── MenubarItemCheckbox
Use MenubarCheckboxItem for toggleable options. Bind checked to read or change the state.
Radio
Use MenubarRadioGroup and MenubarRadioItem for single-select options. Bind value on the group.
Submenu
Use MenubarSub, MenubarSubTrigger, and MenubarSubContent for nested menus.
With Icons
RTL
Wrap the menubar in a DirectionProvider and set dir on Menubar and MenubarContent. Use the buttons to switch the language.
Examples
API reference
Menubar is styled on top of the Sveltery Base Menubar and Menu primitives, which own the roving focus between triggers, keyboard navigation, typeahead and dismissal. MenubarMenu and MenubarSub are the Base menu roots. MenubarContent renders the Base portal, positioner and popup, and takes the positioner's side, align, sideOffset and alignOffset props. MenubarItem takes inset and variant (default or destructive). See the Base UI Menubar documentation for the behavior it ports.