Menubar

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/base

Copy 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.

src/lib/components/ui/menubar/Menubar.svelte
<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)} />
src/lib/components/ui/menubar/MenubarCheckboxItem.svelte
<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>
src/lib/components/ui/menubar/MenubarContent.svelte
<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)}
/>
src/lib/components/ui/menubar/MenubarGroup.svelte
<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} />
src/lib/components/ui/menubar/MenubarItem.svelte
<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)} />
src/lib/components/ui/menubar/MenubarLabel.svelte
<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)} />
src/lib/components/ui/menubar/MenubarPortal.svelte
<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>
src/lib/components/ui/menubar/MenubarRadioGroup.svelte
<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} />
src/lib/components/ui/menubar/MenubarRadioItem.svelte
<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>
src/lib/components/ui/menubar/MenubarSeparator.svelte
<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)} />
src/lib/components/ui/menubar/MenubarShortcut.svelte
<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)} />
src/lib/components/ui/menubar/MenubarSubContent.svelte
<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)} />
src/lib/components/ui/menubar/MenubarSubTrigger.svelte
<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)} />
src/lib/components/ui/menubar/MenubarTrigger.svelte
<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)} />
src/lib/components/ui/menubar/index.ts
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
            └── MenubarItem

Checkbox

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.

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

Basic
With Submenu
Sides
With Checkboxes
With Radio
With Icons
With Shortcuts
Format
Insert
Destructive
In Dialog
With Inset

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.