Skip to content
edmi-ui

Menubar

A persistent row of menus, like a desktop app menu bar.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Terminal window
npx shadcn@latest add @edmi-ui/menubar
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/menubar
Terminal window
yarn dlx shadcn@latest add @edmi-ui/menubar
Terminal window
bunx shadcn@latest add @edmi-ui/menubar

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/menubar
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/menubar
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/menubar
Terminal window
bunx shadcn-vue@latest add @edmi-ui/menubar

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/menubar.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/menubar.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/menubar.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/menubar.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import {
Menubar,
MenubarContent,
MenubarGroup,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarShortcut,
MenubarTrigger,
} from "@/components/ui/menubar";
<Menubar>
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarGroup>
<MenubarItem>
New index <MenubarShortcut>⌘N</MenubarShortcut>
</MenubarItem>
<MenubarItem>Open…</MenubarItem>
</MenubarGroup>
<MenubarSeparator />
<MenubarItem variant="destructive">Discard draft</MenubarItem>
</MenubarContent>
</MenubarMenu>
{/* … */}
</Menubar>
<script setup lang="ts">
import {
Menubar,
MenubarContent,
MenubarGroup,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarShortcut,
MenubarTrigger,
} from "@/components/ui/menubar";
</script>
<template>
<Menubar>
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarGroup>
<MenubarItem>
New index <MenubarShortcut>⌘N</MenubarShortcut>
</MenubarItem>
<MenubarItem>Open…</MenubarItem>
</MenubarGroup>
<MenubarSeparator />
<MenubarItem variant="destructive">Discard draft</MenubarItem>
</MenubarContent>
</MenubarMenu>
<!-- … -->
</Menubar>
</template>
<script lang="ts">
import * as Menubar from "$lib/components/ui/menubar";
</script>
<Menubar.Root>
<Menubar.Menu>
<Menubar.Trigger>File</Menubar.Trigger>
<Menubar.Content>
<Menubar.Group>
<Menubar.Item>
New index <Menubar.Shortcut>⌘N</Menubar.Shortcut>
</Menubar.Item>
<Menubar.Item>Open…</Menubar.Item>
</Menubar.Group>
<Menubar.Separator />
<Menubar.Item variant="destructive">Discard draft</Menubar.Item>
</Menubar.Content>
</Menubar.Menu>
<!-- … -->
</Menubar.Root>
registry/ui/menubar.tsx
"use client";
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
import { Menubar as MenubarPrimitive } from "@base-ui/react/menubar";
import { cn } from "cn";
import type * as React from "react";
import { CheckIcon } from "@phosphor-icons/react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/registry/edmi/ui/dropdown-menu";
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation";
// ✦ depth (v4): the bar itself rises.
const barElevation = {
sunken: "",
flat: "",
raised: "border-transparent shadow-raised",
floating: "border-transparent shadow-floating",
};
function Menubar({
className,
elevation,
...props
}: MenubarPrimitive.Props & {
/** ✦ depth for the bar: flat 0, raised +1 (bevel), floating +2. */
elevation?: Elevation;
}) {
const level = useElevation(elevation, "control");
return (
<MenubarPrimitive
data-slot="menubar"
className={cn(
"flex items-center gap-0.5 rounded-[10px] border border-border bg-card p-[3px]",
barElevation[level],
className,
)}
{...props}
/>
);
}
function MenubarMenu({ ...props }: React.ComponentProps<typeof DropdownMenu>) {
return <DropdownMenu data-slot="menubar-menu" {...props} />;
}
function MenubarGroup({
...props
}: React.ComponentProps<typeof DropdownMenuGroup>) {
return <DropdownMenuGroup data-slot="menubar-group" {...props} />;
}
function MenubarPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPortal>) {
return <DropdownMenuPortal data-slot="menubar-portal" {...props} />;
}
function MenubarTrigger({
className,
...props
}: React.ComponentProps<typeof DropdownMenuTrigger>) {
return (
<DropdownMenuTrigger
data-slot="menubar-trigger"
className={cn(
"flex h-[30px] items-center rounded-[7px] px-3 text-[13.5px] font-medium outline-none select-none hover:bg-accent data-[popup-open]:bg-accent aria-expanded:bg-accent",
className,
)}
{...props}
/>
);
}
function MenubarContent({
className,
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
}: React.ComponentProps<typeof DropdownMenuContent>) {
return (
<DropdownMenuContent
data-slot="menubar-content"
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"w-auto min-w-36 rounded-xl border border-border bg-popover p-1.5 text-popover-foreground duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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 data-[open]:animate-in data-[open]:fade-in-0 data-[open]:zoom-in-95 data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95",
className,
)}
{...props}
/>
);
}
function MenubarItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuItem>) {
return (
<DropdownMenuItem
data-slot="menubar-item"
data-inset={inset}
data-variant={variant}
className={cn(
"group/menu-item relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:text-destructive-text data-[variant=destructive]:data-[highlighted]:bg-destructive-soft data-[variant=destructive]:data-[highlighted]:text-destructive-text data-[variant=destructive]:*:[svg]:text-destructive-text",
className,
)}
{...props}
/>
);
}
function MenubarCheckboxItem({
className,
children,
checked,
inset,
...props
}: MenuPrimitive.CheckboxItem.Props & {
inset?: boolean;
}) {
return (
<MenuPrimitive.CheckboxItem
data-slot="menubar-checkbox-item"
data-inset={inset}
className={cn(
"relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground pl-8",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4">
<MenuPrimitive.CheckboxItemIndicator>
<CheckIcon
/>
</MenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</MenuPrimitive.CheckboxItem>
);
}
function MenubarRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuRadioGroup>) {
return <DropdownMenuRadioGroup data-slot="menubar-radio-group" {...props} />;
}
function MenubarRadioItem({
className,
children,
inset,
...props
}: MenuPrimitive.RadioItem.Props & {
inset?: boolean;
}) {
return (
<MenuPrimitive.RadioItem
data-slot="menubar-radio-item"
data-inset={inset}
className={cn(
"relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground pl-8",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4">
<MenuPrimitive.RadioItemIndicator>
<span className="block size-1.5 rounded-full bg-current" />
</MenuPrimitive.RadioItemIndicator>
</span>
{children}
</MenuPrimitive.RadioItem>
);
}
function MenubarLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuLabel> & {
inset?: boolean;
}) {
return (
<DropdownMenuLabel
data-slot="menubar-label"
data-inset={inset}
className={cn(
"px-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function MenubarSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuSeparator>) {
return (
<DropdownMenuSeparator
data-slot="menubar-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
);
}
function MenubarShortcut({
className,
...props
}: React.ComponentProps<typeof DropdownMenuShortcut>) {
return (
<DropdownMenuShortcut
data-slot="menubar-shortcut"
className={cn(
"ml-auto font-mono text-[11.5px] tracking-wide text-muted-foreground",
className,
)}
{...props}
/>
);
}
function MenubarSub({
...props
}: React.ComponentProps<typeof DropdownMenuSub>) {
return <DropdownMenuSub data-slot="menubar-sub" {...props} />;
}
function MenubarSubTrigger({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuSubTrigger> & {
inset?: boolean;
}) {
return (
<DropdownMenuSubTrigger
data-slot="menubar-sub-trigger"
data-inset={inset}
className={cn(
"relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground data-[popup-open]:bg-accent data-[popup-open]:text-accent-foreground data-[open]:bg-accent data-[open]:text-accent-foreground",
className,
)}
{...props}
/>
);
}
function MenubarSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuSubContent>) {
return (
<DropdownMenuSubContent
data-slot="menubar-sub-content"
className={cn(
"w-auto min-w-36 rounded-xl border border-border bg-popover p-1.5 text-popover-foreground duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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 data-[open]:animate-in data-[open]:fade-in-0 data-[open]:zoom-in-95 data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95",
className,
)}
{...props}
/>
);
}
export {
Menubar,
MenubarCheckboxItem,
MenubarContent,
MenubarGroup,
MenubarItem,
MenubarLabel,
MenubarMenu,
MenubarPortal,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSeparator,
MenubarShortcut,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
MenubarTrigger,
};
registry/ui/menubar/Menubar.vue
<script setup lang="ts">
import type { MenubarRootEmits, MenubarRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
MenubarRoot,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation"
const props = withDefaults(defineProps<MenubarRootProps & { class?: HTMLAttributes["class"], elevation?: Elevation }>(), { elevation: undefined })
const emits = defineEmits<MenubarRootEmits>()
const delegatedProps = reactiveOmit(props, "class", "elevation")
// ✦ depth (v4): the bar itself rises.
const barElevation = {
sunken: '',
flat: '',
raised: 'border-transparent shadow-raised',
floating: 'border-transparent shadow-floating',
}
const level = useElevation(() => props.elevation, 'control')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<MenubarRoot
v-slot="slotProps"
data-slot="menubar"
v-bind="forwarded"
:class="
cn(
`flex items-center gap-0.5 rounded-[10px] border border-border bg-card p-[3px]`,
barElevation[level],
props.class,
)
"
>
<slot v-bind="slotProps" />
</MenubarRoot>
</template>
registry/ui/menubar/MenubarCheckboxItem.vue
<script setup lang="ts">
import type { MenubarCheckboxItemEmits, MenubarCheckboxItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { Check } from "@lucide/vue"
import { reactiveOmit } from "@vueuse/core"
import {
MenubarCheckboxItem,
MenubarItemIndicator,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<MenubarCheckboxItemProps & { class?: HTMLAttributes["class"], closeOnSelect?: boolean }>()
const emits = defineEmits<MenubarCheckboxItemEmits>()
const delegatedProps = reactiveOmit(props, "class", "closeOnSelect")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
// Checkbox and radio items keep the menu open (like Base UI and Bits); pass close-on-select to close it.
function keepOpen(event: Event) {
if (!props.closeOnSelect) event.preventDefault()
}
</script>
<template>
<MenubarCheckboxItem
data-slot="menubar-checkbox-item"
v-bind="forwarded"
@select="keepOpen"
:class="cn(
`relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground pl-8`,
props.class,
)"
>
<span class="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4">
<MenubarItemIndicator>
<slot name="indicator-icon">
<Check />
</slot>
</MenubarItemIndicator>
</span>
<slot />
</MenubarCheckboxItem>
</template>
registry/ui/menubar/MenubarContent.vue
<script setup lang="ts">
import type { MenubarContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
MenubarContent,
MenubarPortal,
useForwardProps,
} from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(
defineProps<MenubarContentProps & { class?: HTMLAttributes["class"] }>(),
{
align: "start",
alignOffset: -4,
sideOffset: 8,
},
)
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<MenubarPortal>
<MenubarContent
data-slot="menubar-content"
v-bind="{ ...$attrs, ...forwardedProps }"
:class="
cn(
`z-50 max-h-(--reka-menubar-content-available-height) min-w-40 origin-(--reka-menubar-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border border-border bg-popover p-1.5 text-popover-foreground duration-100 outline-none 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 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:overflow-hidden`,
props.class,
)
"
>
<slot />
</MenubarContent>
</MenubarPortal>
</template>
registry/ui/menubar/MenubarGroup.vue
<script setup lang="ts">
import type { MenubarGroupProps } from "reka-ui"
import { MenubarGroup } from "reka-ui"
const props = defineProps<MenubarGroupProps>()
</script>
<template>
<MenubarGroup
data-slot="menubar-group"
v-bind="props"
>
<slot />
</MenubarGroup>
</template>
registry/ui/menubar/MenubarItem.vue
<script setup lang="ts">
import type { MenubarItemEmits, MenubarItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
MenubarItem,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<MenubarItemProps & {
class?: HTMLAttributes["class"]
inset?: boolean
variant?: "default" | "destructive"
}>()
const emits = defineEmits<MenubarItemEmits>()
const delegatedProps = reactiveOmit(props, "class", "inset", "variant")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<MenubarItem
data-slot="menubar-item"
:data-inset="inset ? '' : undefined"
:data-variant="variant"
v-bind="forwarded"
:class="cn(
`group/menu-item relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:text-destructive-text data-[variant=destructive]:data-[highlighted]:bg-destructive-soft data-[variant=destructive]:data-[highlighted]:text-destructive-text data-[variant=destructive]:*:[svg]:text-destructive-text`,
props.class,
)"
>
<slot />
</MenubarItem>
</template>
registry/ui/menubar/MenubarLabel.vue
<script setup lang="ts">
import type { MenubarLabelProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { MenubarLabel } from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<MenubarLabelProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
const delegatedProps = reactiveOmit(props, "class", "inset")
</script>
<template>
<MenubarLabel
:data-inset="inset ? '' : undefined"
v-bind="delegatedProps"
:class="cn(
`px-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground data-[inset]:pl-8`, props.class)"
>
<slot />
</MenubarLabel>
</template>
registry/ui/menubar/MenubarMenu.vue
<script setup lang="ts">
import type { MenubarMenuProps } from "reka-ui"
import { MenubarMenu } from "reka-ui"
const props = defineProps<MenubarMenuProps>()
</script>
<template>
<MenubarMenu
data-slot="menubar-menu"
v-bind="props"
>
<slot />
</MenubarMenu>
</template>
registry/ui/menubar/MenubarRadioGroup.vue
<script setup lang="ts">
import type { MenubarRadioGroupEmits, MenubarRadioGroupProps } from "reka-ui"
import {
MenubarRadioGroup,
useForwardPropsEmits,
} from "reka-ui"
const props = defineProps<MenubarRadioGroupProps>()
const emits = defineEmits<MenubarRadioGroupEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<MenubarRadioGroup
data-slot="menubar-radio-group"
v-bind="forwarded"
>
<slot />
</MenubarRadioGroup>
</template>
registry/ui/menubar/MenubarRadioItem.vue
<script setup lang="ts">
import type { MenubarRadioItemEmits, MenubarRadioItemProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
MenubarItemIndicator,
MenubarRadioItem,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<MenubarRadioItemProps & { class?: HTMLAttributes["class"], closeOnSelect?: boolean }>()
const emits = defineEmits<MenubarRadioItemEmits>()
const delegatedProps = reactiveOmit(props, "class", "closeOnSelect")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
// Checkbox and radio items keep the menu open (like Base UI and Bits); pass close-on-select to close it.
function keepOpen(event: Event) {
if (!props.closeOnSelect) event.preventDefault()
}
</script>
<template>
<MenubarRadioItem
data-slot="menubar-radio-item"
v-bind="forwarded"
@select="keepOpen"
:class="cn(
`relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground pl-8`,
props.class,
)"
>
<span class="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4">
<MenubarItemIndicator>
<slot name="indicator-icon">
<span class="block size-1.5 rounded-full bg-current" />
</slot>
</MenubarItemIndicator>
</span>
<slot />
</MenubarRadioItem>
</template>
registry/ui/menubar/MenubarSeparator.vue
<script setup lang="ts">
import type { MenubarSeparatorProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { MenubarSeparator, useForwardProps } from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<MenubarSeparatorProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<MenubarSeparator
data-slot="menubar-separator"
:class=" cn(
`-mx-1 my-1 h-px bg-border`, props.class)"
v-bind="forwardedProps"
/>
</template>
registry/ui/menubar/MenubarShortcut.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<span
data-slot="menubar-shortcut"
:class="cn(
`ml-auto font-mono text-[11.5px] tracking-wide text-muted-foreground`, props.class)"
>
<slot />
</span>
</template>
registry/ui/menubar/MenubarSub.vue
<script setup lang="ts">
import type { MenubarSubEmits } from "reka-ui"
import { MenubarSub, useForwardPropsEmits } from "reka-ui"
interface MenubarSubRootProps {
defaultOpen?: boolean
open?: boolean
}
const props = defineProps<MenubarSubRootProps>()
const emits = defineEmits<MenubarSubEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<MenubarSub
v-slot="slotProps"
data-slot="menubar-sub"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</MenubarSub>
</template>
registry/ui/menubar/MenubarSubContent.vue
<script setup lang="ts">
import type { MenubarSubContentEmits, MenubarSubContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
MenubarPortal,
MenubarSubContent,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
defineOptions({
inheritAttrs: false,
})
const props = defineProps<MenubarSubContentProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<MenubarSubContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<MenubarPortal>
<MenubarSubContent
data-slot="menubar-sub-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="
cn(
`z-50 w-auto min-w-36 origin-(--reka-menubar-content-transform-origin) overflow-hidden rounded-xl border border-border bg-popover p-1.5 text-popover-foreground duration-100 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 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95`,
props.class,
)
"
>
<slot />
</MenubarSubContent>
</MenubarPortal>
</template>
registry/ui/menubar/MenubarSubTrigger.vue
<script setup lang="ts">
import type { MenubarSubTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { ChevronRight } from "@lucide/vue"
import { reactiveOmit } from "@vueuse/core"
import { MenubarSubTrigger, useForwardProps } from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<MenubarSubTriggerProps & { class?: HTMLAttributes["class"], inset?: boolean }>()
const delegatedProps = reactiveOmit(props, "class", "inset")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<MenubarSubTrigger
data-slot="menubar-sub-trigger"
:data-inset="inset ? '' : undefined"
v-bind="forwardedProps"
:class="cn(
`relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground`,
props.class,
)"
>
<slot />
<ChevronRight class="ml-auto rtl:rotate-180" />
</MenubarSubTrigger>
</template>
registry/ui/menubar/MenubarTrigger.vue
<script setup lang="ts">
import type { MenubarTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { MenubarTrigger, useForwardProps } from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<MenubarTriggerProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<MenubarTrigger
data-slot="menubar-trigger"
v-bind="forwardedProps"
:class="
cn(
`flex h-[30px] items-center rounded-[7px] px-3 text-[13.5px] font-medium outline-none select-none hover:bg-accent data-[state=open]:bg-accent`,
props.class,
)
"
>
<slot />
</MenubarTrigger>
</template>
registry/ui/menubar/index.ts
export { default as Menubar } from "./Menubar.vue";
export { default as MenubarCheckboxItem } from "./MenubarCheckboxItem.vue";
export { default as MenubarContent } from "./MenubarContent.vue";
export { default as MenubarGroup } from "./MenubarGroup.vue";
export { default as MenubarItem } from "./MenubarItem.vue";
export { default as MenubarLabel } from "./MenubarLabel.vue";
export { default as MenubarMenu } from "./MenubarMenu.vue";
export { default as MenubarRadioGroup } from "./MenubarRadioGroup.vue";
export { default as MenubarRadioItem } from "./MenubarRadioItem.vue";
export { default as MenubarSeparator } from "./MenubarSeparator.vue";
export { default as MenubarShortcut } from "./MenubarShortcut.vue";
export { default as MenubarSub } from "./MenubarSub.vue";
export { default as MenubarSubContent } from "./MenubarSubContent.vue";
export { default as MenubarSubTrigger } from "./MenubarSubTrigger.vue";
export { default as MenubarTrigger } from "./MenubarTrigger.vue";
src/lib/registry/ui/menubar/menubar-checkbox-item.svelte
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import MinusIcon from "phosphor-svelte/lib/Minus";
import CheckIcon from "phosphor-svelte/lib/Check";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { Snippet } from "svelte";
let {
ref = $bindable(null),
class: className,
checked = $bindable(false),
indeterminate = $bindable(false),
inset,
children: childrenProp,
closeOnSelect = false,
...restProps
}: WithoutChildrenOrChild<MenubarPrimitive.CheckboxItemProps> & {
inset?: boolean;
children?: Snippet;
} = $props();
</script>
<MenubarPrimitive.CheckboxItem
bind:ref
{closeOnSelect}
bind:checked
bind:indeterminate
data-slot="menubar-checkbox-item"
data-inset={inset}
class={cn(
"relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground pl-8",
className
)}
{...restProps}
>
{#snippet children({ checked: checked, indeterminate: indeterminate })}
<span
class="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center"
>
{#if indeterminate}
<MinusIcon
/>
{:else if checked}
<CheckIcon
/>
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</MenubarPrimitive.CheckboxItem>
src/lib/registry/ui/menubar/menubar-content.svelte
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import MenubarPortal from "./menubar-portal.svelte";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
sideOffset = 8,
alignOffset = -4,
align = "start",
side = "bottom",
portalProps,
...restProps
}: MenubarPrimitive.ContentProps & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof MenubarPortal>>;
} = $props();
</script>
<MenubarPortal {...portalProps}>
<MenubarPrimitive.Content
bind:ref
data-slot="menubar-content"
{align}
{alignOffset}
{side}
{sideOffset}
class={cn(
"z-50 w-auto min-w-36 origin-(--bits-menubar-content-transform-origin) overflow-hidden rounded-xl border border-border bg-popover p-1.5 text-popover-foreground duration-100 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 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
className
)}
{...restProps}
/>
</MenubarPortal>
src/lib/registry/ui/menubar/menubar-group-heading.svelte
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
inset,
class: className,
...restProps
}: ComponentProps<typeof MenubarPrimitive.GroupHeading> & {
inset?: boolean;
} = $props();
</script>
<MenubarPrimitive.GroupHeading
bind:ref
data-slot="menubar-group-heading"
data-inset={inset}
class={cn(
"px-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground data-[inset]:pl-8", className)}
{...restProps}
/>
src/lib/registry/ui/menubar/menubar-group.svelte
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
let {
ref = $bindable(null),
...restProps
}: MenubarPrimitive.GroupProps & {
inset?: boolean;
} = $props();
</script>
<MenubarPrimitive.Group bind:ref data-slot="menubar-group" {...restProps} />
src/lib/registry/ui/menubar/menubar-item.svelte
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset = undefined,
variant = "default",
...restProps
}: MenubarPrimitive.ItemProps & {
inset?: boolean;
variant?: "default" | "destructive";
} = $props();
</script>
<MenubarPrimitive.Item
bind:ref
data-slot="menubar-item"
data-inset={inset}
data-variant={variant}
class={cn(
"group/menu-item relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:text-destructive-text data-[variant=destructive]:data-[highlighted]:bg-destructive-soft data-[variant=destructive]:data-[highlighted]:text-destructive-text data-[variant=destructive]:*:[svg]:text-destructive-text",
className
)}
{...restProps}
/>
src/lib/registry/ui/menubar/menubar-label.svelte
<script lang="ts">
import { type WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
inset,
children,
class: className,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
inset?: boolean;
} = $props();
</script>
<div
bind:this={ref}
data-slot="menubar-label"
data-inset={inset}
class={cn(
"px-2 pt-1.5 pb-1 text-xs font-semibold text-muted-foreground data-[inset]:pl-8", className)}
{...restProps}
>
{@render children?.()}
</div>
src/lib/registry/ui/menubar/menubar-menu.svelte
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
let { ...restProps }: MenubarPrimitive.MenuProps = $props();
</script>
<MenubarPrimitive.Menu {...restProps} />
src/lib/registry/ui/menubar/menubar-portal.svelte
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
let { ...restProps }: MenubarPrimitive.PortalProps = $props();
</script>
<MenubarPrimitive.Portal {...restProps} />
src/lib/registry/ui/menubar/menubar-radio-group.svelte
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
let {
ref = $bindable(null),
value = $bindable(""),
...restProps
}: MenubarPrimitive.RadioGroupProps = $props();
</script>
<MenubarPrimitive.RadioGroup bind:ref bind:value data-slot="menubar-radio-group" {...restProps} />
src/lib/registry/ui/menubar/menubar-radio-item.svelte
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn, type WithoutChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset,
children: childrenProp,
closeOnSelect = false,
...restProps
}: WithoutChild<MenubarPrimitive.RadioItemProps> & {
inset?: boolean;
} = $props();
</script>
<MenubarPrimitive.RadioItem
bind:ref
{closeOnSelect}
data-slot="menubar-radio-item"
data-inset={inset}
class={cn(
"relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground pl-8",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span
class="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center"
>
{#if checked}
<span class="size-1.5 rounded-full bg-current"></span>
{/if}
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</MenubarPrimitive.RadioItem>
src/lib/registry/ui/menubar/menubar-separator.svelte
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: MenubarPrimitive.SeparatorProps = $props();
</script>
<MenubarPrimitive.Separator
bind:ref
data-slot="menubar-separator"
class={cn(
"-mx-1 my-1 h-px bg-border", className)}
{...restProps}
/>
src/lib/registry/ui/menubar/menubar-shortcut.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="menubar-shortcut"
class={cn(
"ml-auto font-mono text-[11.5px] tracking-wide text-muted-foreground", className)}
{...restProps}
>
{@render children?.()}
</span>
src/lib/registry/ui/menubar/menubar-sub-content.svelte
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import MenubarPortal from "./menubar-portal.svelte";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
align = "start",
alignOffset = -3,
portalProps,
...restProps
}: MenubarPrimitive.SubContentProps & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof MenubarPortal>>;
} = $props();
</script>
<MenubarPortal {...portalProps}>
<MenubarPrimitive.SubContent
bind:ref
data-slot="menubar-sub-content"
{align}
{alignOffset}
class={cn(
"z-50 w-auto min-w-36 origin-(--bits-menubar-content-transform-origin) overflow-hidden rounded-xl border border-border bg-popover p-1.5 text-popover-foreground duration-100 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 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
className
)}
{...restProps}
/>
</MenubarPortal>
src/lib/registry/ui/menubar/menubar-sub-trigger.svelte
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import CaretRightIcon from "phosphor-svelte/lib/CaretRight";
import { cn, type WithoutChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset = undefined,
children,
...restProps
}: WithoutChild<MenubarPrimitive.SubTriggerProps> & {
inset?: boolean;
} = $props();
</script>
<MenubarPrimitive.SubTrigger
bind:ref
data-slot="menubar-sub-trigger"
data-inset={inset}
class={cn(
"relative flex h-8 cursor-default items-center gap-2.5 rounded-[7px] px-2 text-[13.5px] outline-none select-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[inset]:pl-8 data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-[15px] [&_svg:not([class*='text-'])]:text-muted-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className
)}
{...restProps}
>
{@render children?.()}
<CaretRightIcon
class="ml-auto rtl:rotate-180"
/>
</MenubarPrimitive.SubTrigger>
src/lib/registry/ui/menubar/menubar-sub.svelte
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
let { open = $bindable(false), ...restProps }: MenubarPrimitive.SubProps = $props();
</script>
<MenubarPrimitive.Sub bind:open {...restProps} />
src/lib/registry/ui/menubar/menubar-trigger.svelte
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: MenubarPrimitive.TriggerProps = $props();
</script>
<MenubarPrimitive.Trigger
bind:ref
data-slot="menubar-trigger"
class={cn(
"flex h-[30px] items-center rounded-[7px] px-3 text-[13.5px] font-medium outline-none select-none hover:bg-accent data-[state=open]:bg-accent", className)}
{...restProps}
/>
src/lib/registry/ui/menubar/menubar.svelte
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js";
let {
ref = $bindable(null),
class: className,
elevation = "auto",
...restProps
}: MenubarPrimitive.RootProps & {
/** ✦ depth for the bar: flat 0, raised +1 (bevel), floating +2. */
elevation?: Elevation;
} = $props();
// ✦ depth (v4): the bar itself rises.
const barElevation = {
sunken: "",
flat: "",
raised: "border-transparent shadow-raised",
floating: "border-transparent shadow-floating",
};
const level = useElevation(() => elevation, "control");
</script>
<MenubarPrimitive.Root
bind:ref
data-slot="menubar"
class={cn(
"flex items-center gap-0.5 rounded-[10px] border border-border bg-card p-[3px]",
barElevation[level.current],
className
)}
{...restProps}
/>
src/lib/registry/ui/menubar/index.ts
import Root from "./menubar.svelte";
import CheckboxItem from "./menubar-checkbox-item.svelte";
import Content from "./menubar-content.svelte";
import Group from "./menubar-group.svelte";
import GroupHeading from "./menubar-group-heading.svelte";
import Item from "./menubar-item.svelte";
import Label from "./menubar-label.svelte";
import Menu from "./menubar-menu.svelte";
import Portal from "./menubar-portal.svelte";
import RadioGroup from "./menubar-radio-group.svelte";
import RadioItem from "./menubar-radio-item.svelte";
import Separator from "./menubar-separator.svelte";
import Shortcut from "./menubar-shortcut.svelte";
import Sub from "./menubar-sub.svelte";
import SubContent from "./menubar-sub-content.svelte";
import SubTrigger from "./menubar-sub-trigger.svelte";
import Trigger from "./menubar-trigger.svelte";
export {
CheckboxItem,
CheckboxItem as MenubarCheckboxItem,
Content,
Content as MenubarContent,
Group,
Group as MenubarGroup,
GroupHeading,
GroupHeading as MenubarGroupHeading,
Item,
Item as MenubarItem,
Label,
Label as MenubarLabel,
Menu,
Menu as MenubarMenu,
Portal,
Portal as MenubarPortal,
RadioGroup,
RadioGroup as MenubarRadioGroup,
RadioItem,
RadioItem as MenubarRadioItem,
Root,
//
Root as Menubar,
Separator,
Separator as MenubarSeparator,
Shortcut,
Shortcut as MenubarShortcut,
Sub,
Sub as MenubarSub,
SubContent,
SubContent as MenubarSubContent,
SubTrigger,
SubTrigger as MenubarSubTrigger,
Trigger,
Trigger as MenubarTrigger,
};

See the Elevation guide for the levels, roles and layered mode.

Flat by default. On Menubar, raised (+1) adds the bevel to the bar and floating (+2) a soft drop. The menus below it follow DropdownMenuContent (flat, or elevation on the content).

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Flat (0)

Raised (+1)

Flat (0)

Raised (+1)

Flat (0)

Raised (+1)

Prop Type Default
MenubarItem variant default | destructive default
MenubarItem inset boolean false
MenubarContent align start | center | end start
elevation "auto" | "sunken" | "flat" | "raised" | "floating" "auto"

Parts: Menubar, MenubarMenu, MenubarTrigger, MenubarContent, MenubarGroup, MenubarLabel, MenubarItem, MenubarCheckboxItem, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubTrigger, MenubarSubContent.

  • Menus reuse the Dropdown Menu recipe (data-[highlighted]); the open trigger stays highlighted.