Skip to main content
Nexus Design System
GitHub

Menubar

Menubar shows a row of menus that stays on screen, like a desktop app. Use it for app-level commands grouped under File, Edit and View. For one menu on a button, use DropdownMenu.

'use client';

import {
  Menubar,
  MenubarContent,
  MenubarItem,
  MenubarMenu,
  MenubarSeparator,
  MenubarShortcut,
  MenubarTrigger,
} from '@/components/nxds/components/menubar/menubar';

export default function MenubarDefault() {
  return (
    <Menubar>
      <MenubarMenu>
        <MenubarTrigger>File</MenubarTrigger>
        <MenubarContent>
          <MenubarItem>
            New Tab
            <MenubarShortcut>⌘T</MenubarShortcut>
          </MenubarItem>
          <MenubarItem>
            New Window
            <MenubarShortcut>⌘N</MenubarShortcut>
          </MenubarItem>
          <MenubarItem>New Incognito Window</MenubarItem>
          <MenubarSeparator />
          <MenubarItem>Print</MenubarItem>
        </MenubarContent>
      </MenubarMenu>
      <MenubarMenu>
        <MenubarTrigger>Edit</MenubarTrigger>
        <MenubarContent>
          <MenubarItem>Undo</MenubarItem>
          <MenubarItem>Redo</MenubarItem>
        </MenubarContent>
      </MenubarMenu>
      <MenubarMenu>
        <MenubarTrigger>View</MenubarTrigger>
        <MenubarContent>
          <MenubarItem>Reload</MenubarItem>
          <MenubarItem>Toggle Fullscreen</MenubarItem>
        </MenubarContent>
      </MenubarMenu>
    </Menubar>
  );
}

Installation

npx nxds-cli@latest add menubar

Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.

Usage

import {
  Menubar,
  MenubarCheckboxItem,
  MenubarContent,
  MenubarItem,
  MenubarLabel,
  MenubarMenu,
  MenubarRadioGroup,
  MenubarRadioItem,
  MenubarSeparator,
  MenubarShortcut,
  MenubarSub,
  MenubarSubContent,
  MenubarSubTrigger,
  MenubarTrigger,
} from '@/components/nxds/components/menubar/menubar';

Composition

Menubar
└── MenubarMenu
    ├── MenubarTrigger
    └── MenubarContent
        ├── MenubarItem
        │   └── MenubarShortcut
        ├── MenubarSeparator
        ├── MenubarLabel
        ├── MenubarCheckboxItem
        ├── MenubarRadioGroup
        │   └── MenubarRadioItem
        └── MenubarSub
            ├── MenubarSubTrigger
            └── MenubarSubContent
                └── MenubarItem

Examples

With checkbox items

Toggle settings on and off with checkbox items.

'use client';

import * as React from 'react';

import {
  Menubar,
  MenubarCheckboxItem,
  MenubarContent,
  MenubarLabel,
  MenubarMenu,
  MenubarSeparator,
  MenubarTrigger,
} from '@/components/nxds/components/menubar/menubar';

export default function MenubarWithCheckboxItems() {
  const [showBookmarks, setShowBookmarks] = React.useState(true);
  const [showFullUrls, setShowFullUrls] = React.useState(false);

  return (
    <Menubar>
      <MenubarMenu>
        <MenubarTrigger>View</MenubarTrigger>
        <MenubarContent>
          <MenubarLabel>Appearance</MenubarLabel>
          <MenubarSeparator />
          <MenubarCheckboxItem
            checked={showBookmarks}
            onCheckedChange={setShowBookmarks}
          >
            Show Bookmarks
          </MenubarCheckboxItem>
          <MenubarCheckboxItem
            checked={showFullUrls}
            onCheckedChange={setShowFullUrls}
          >
            Show Full URLs
          </MenubarCheckboxItem>
        </MenubarContent>
      </MenubarMenu>
    </Menubar>
  );
}

With radio items

Pick one option from a group with radio items.

'use client';

import * as React from 'react';

import {
  Menubar,
  MenubarContent,
  MenubarLabel,
  MenubarMenu,
  MenubarRadioGroup,
  MenubarRadioItem,
  MenubarSeparator,
  MenubarTrigger,
} from '@/components/nxds/components/menubar/menubar';

export default function MenubarWithRadioItems() {
  const [profile, setProfile] = React.useState('benoit');

  return (
    <Menubar>
      <MenubarMenu>
        <MenubarTrigger>Profiles</MenubarTrigger>
        <MenubarContent>
          <MenubarLabel>People</MenubarLabel>
          <MenubarSeparator />
          <MenubarRadioGroup value={profile} onValueChange={setProfile}>
            <MenubarRadioItem value="benoit">Benoit</MenubarRadioItem>
            <MenubarRadioItem value="luis">Luis</MenubarRadioItem>
          </MenubarRadioGroup>
        </MenubarContent>
      </MenubarMenu>
    </Menubar>
  );
}

With sub menu

Nest related commands in a submenu that opens from an item.

'use client';

import {
  Menubar,
  MenubarContent,
  MenubarItem,
  MenubarMenu,
  MenubarSeparator,
  MenubarSub,
  MenubarSubContent,
  MenubarSubTrigger,
  MenubarTrigger,
} from '@/components/nxds/components/menubar/menubar';

export default function MenubarWithSubMenu() {
  return (
    <Menubar>
      <MenubarMenu>
        <MenubarTrigger>File</MenubarTrigger>
        <MenubarContent>
          <MenubarItem>New Tab</MenubarItem>
          <MenubarSeparator />
          <MenubarSub>
            <MenubarSubTrigger>Share</MenubarSubTrigger>
            <MenubarSubContent>
              <MenubarItem>Email Link</MenubarItem>
              <MenubarItem>Messages</MenubarItem>
              <MenubarItem>Notes</MenubarItem>
            </MenubarSubContent>
          </MenubarSub>
          <MenubarSeparator />
          <MenubarItem>Print</MenubarItem>
        </MenubarContent>
      </MenubarMenu>
    </Menubar>
  );
}

With inset items

Indent items and labels with inset to line up with items that have icons.

'use client';

import {
  Menubar,
  MenubarContent,
  MenubarItem,
  MenubarLabel,
  MenubarMenu,
  MenubarSeparator,
  MenubarTrigger,
} from '@/components/nxds/components/menubar/menubar';

export default function MenubarWithInsetItems() {
  return (
    <Menubar>
      <MenubarMenu>
        <MenubarTrigger>View</MenubarTrigger>
        <MenubarContent>
          <MenubarLabel inset>Appearance</MenubarLabel>
          <MenubarSeparator />
          <MenubarItem inset>Reload</MenubarItem>
          <MenubarItem inset>Force Reload</MenubarItem>
          <MenubarItem inset>Toggle Fullscreen</MenubarItem>
        </MenubarContent>
      </MenubarMenu>
    </Menubar>
  );
}

With disabled items

Show unavailable commands as disabled items that cannot be selected.

'use client';

import {
  Menubar,
  MenubarContent,
  MenubarItem,
  MenubarMenu,
  MenubarSeparator,
  MenubarTrigger,
} from '@/components/nxds/components/menubar/menubar';

export default function MenubarWithDisabledItems() {
  return (
    <Menubar>
      <MenubarMenu>
        <MenubarTrigger>Edit</MenubarTrigger>
        <MenubarContent>
          <MenubarItem>Undo</MenubarItem>
          <MenubarItem>Redo</MenubarItem>
          <MenubarSeparator />
          <MenubarItem disabled>Cut</MenubarItem>
          <MenubarItem disabled>Copy</MenubarItem>
          <MenubarItem>Paste</MenubarItem>
        </MenubarContent>
      </MenubarMenu>
    </Menubar>
  );
}

Props

Menubar

Menubar adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarCheckboxItem

MenubarCheckboxItem adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarContent

MenubarContent adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarGroup

MenubarGroup adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarItem

MenubarItem props
PropTypeDefaultDescription
insetbooleanNo default documented

Indents the item to align with items that have a leading icon or checkbox.

variant"default" | "destructive" | null"default"No description

MenubarLabel

MenubarLabel props
PropTypeDefaultDescription
insetbooleanNo default documented

Indents the label to align with items that have a leading icon or checkbox.

MenubarMenu

MenubarMenu adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarPortal

MenubarPortal adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarRadioGroup

MenubarRadioGroup adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarRadioItem

MenubarRadioItem adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarSeparator

MenubarSeparator adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarShortcut

MenubarShortcut adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarSub

MenubarSub adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarSubContent

MenubarSubContent adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarSubTrigger

MenubarSubTrigger props
PropTypeDefaultDescription
insetbooleanNo default documented

Indents the trigger to align with items that have a leading icon or checkbox.

MenubarTrigger

MenubarTrigger adds no props of its own; it accepts the props of the element or primitive it wraps.

Menubar shows a row of menus that stays on screen, like a desktop app. Use it for app-level commands grouped under File, Edit and View. For one menu on a button, use DropdownMenu.

<script setup lang="ts">
import {
  Menubar,
  MenubarContent,
  MenubarItem,
  MenubarMenu,
  MenubarSeparator,
  MenubarShortcut,
  MenubarTrigger,
} from '@/components/menubar';
</script>

<template>
  <Menubar>
    <MenubarMenu>
      <MenubarTrigger>File</MenubarTrigger>
      <MenubarContent>
        <MenubarItem>New Tab<MenubarShortcut>⌘T</MenubarShortcut> </MenubarItem>
        <MenubarItem
          >New Window<MenubarShortcut>⌘N</MenubarShortcut>
        </MenubarItem>
        <MenubarItem>New Incognito Window</MenubarItem>
        <MenubarSeparator />
        <MenubarItem>Print</MenubarItem>
      </MenubarContent>
    </MenubarMenu>
    <MenubarMenu>
      <MenubarTrigger>Edit</MenubarTrigger>
      <MenubarContent>
        <MenubarItem>Undo</MenubarItem>
        <MenubarItem>Redo</MenubarItem>
      </MenubarContent>
    </MenubarMenu>
    <MenubarMenu>
      <MenubarTrigger>View</MenubarTrigger>
      <MenubarContent>
        <MenubarItem>Reload</MenubarItem>
        <MenubarItem>Toggle Fullscreen</MenubarItem>
      </MenubarContent>
    </MenubarMenu>
  </Menubar>
</template>

Installation

  1. Install the following dependencies:

    npm install @phosphor-icons/vue@^2.2.1 @vueuse/core@^14.1.0 class-variance-authority@^0.7.1 reka-ui@^2.11.0
    
  2. Copy and paste the following code into your project.

    components/menubar/Menubar.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      MenubarRoot,
      type MenubarRootEmits,
      type MenubarRootProps,
      useForwardPropsEmits,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface MenubarProps extends MenubarRootProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<MenubarProps>();
    const emits = defineEmits<MenubarRootEmits>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <MenubarRoot
        v-bind="forwarded"
        data-slot="menubar"
        :class="
          cn(
            'nx:flex nx:items-center nx:gap-1 nx:rounded-md nx:border-default nx:border-border-default nx:bg-container nx:p-1 nx:shadow-xs',
            props.class
          )
        "
      >
        <slot />
      </MenubarRoot>
    </template>
    
    components/menubar/MenubarCheckboxItem.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      MenubarCheckboxItem,
      type MenubarCheckboxItemEmits,
      type MenubarCheckboxItemProps as RekaMenubarCheckboxItemProps,
      MenubarItemIndicator,
      useForwardPropsEmits,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { IconCheck } from '../../lib/icons';
    import { selectionIndicatorMotionClassName } from '../../lib/motion';
    import { cn } from '../../lib/utils';
    
    export interface MenubarCheckboxItemProps extends RekaMenubarCheckboxItemProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<MenubarCheckboxItemProps>();
    const emits = defineEmits<MenubarCheckboxItemEmits>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <MenubarCheckboxItem
        v-bind="forwarded"
        data-slot="menubar-checkbox-item"
        :class="
          cn(
            'nx:group nx:relative nx:flex nx:cursor-default nx:select-none nx:items-center',
            'nx:rounded-sm nx:py-1.5 nx:pl-8 nx:pr-2 nx:typography-body-default nx:outline-none',
            'nx:transition-colors',
            'nx:focus:bg-popover-hover nx:focus:text-popover-foreground',
            'nx:data-disabled:pointer-events-none nx:data-disabled:text-disabled-foreground',
            props.class
          )
        "
      >
        <span
          class="nx:pointer-events-none nx:absolute nx:left-2 nx:flex nx:size-3.5 nx:items-center nx:justify-center"
        >
          <MenubarItemIndicator force-mount data-slot="menubar-checkbox-indicator">
            <IconCheck
              data-slot="menubar-checkbox-indicator-icon"
              aria-hidden="true"
              :class="
                cn(
                  'nx:size-4',
                  selectionIndicatorMotionClassName,
                  'nx:group-data-[state=checked]:scale-100 nx:group-data-[state=checked]:opacity-100',
                  'nx:group-data-[state=indeterminate]:scale-100 nx:group-data-[state=indeterminate]:opacity-100'
                )
              "
            />
          </MenubarItemIndicator>
        </span>
        <slot />
      </MenubarCheckboxItem>
    </template>
    
    components/menubar/MenubarContent.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      MenubarContent,
      type MenubarContentEmits,
      type MenubarContentProps as RekaMenubarContentProps,
      MenubarPortal,
      useForwardPropsEmits,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    import {
      overlayFloatingTransitionClassName,
      popoverSurfaceClassName,
    } from '../overlay-layout/overlay-layout';
    
    export interface MenubarContentProps extends RekaMenubarContentProps {
      class?: HTMLAttributes['class'];
    }
    
    defineOptions({ inheritAttrs: false });
    
    const props = withDefaults(defineProps<MenubarContentProps>(), {
      align: 'start',
      alignOffset: -4,
      sideOffset: 8,
    });
    const emits = defineEmits<MenubarContentEmits>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <MenubarPortal>
        <MenubarContent
          v-bind="{ ...$attrs, ...forwarded }"
          data-slot="menubar-content"
          :class="
            cn(
              'nx:z-popover nx:min-w-48 nx:overflow-hidden',
              popoverSurfaceClassName,
              'nx:p-1',
              overlayFloatingTransitionClassName,
              props.class
            )
          "
        >
          <slot />
        </MenubarContent>
      </MenubarPortal>
    </template>
    
    components/menubar/MenubarItem.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      MenubarItem,
      type MenubarItemEmits,
      type MenubarItemProps as RekaMenubarItemProps,
      useForwardPropsEmits,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    import { type MenubarItemVariants, menubarItemVariants } from '.';
    
    export interface MenubarItemProps extends RekaMenubarItemProps {
      variant?: MenubarItemVariants['variant'];
      /**
       * Indents the item to align with items that have a leading icon or checkbox.
       */
      inset?: boolean;
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<MenubarItemProps>(), {
      variant: 'default',
    });
    const emits = defineEmits<MenubarItemEmits>();
    
    const delegated = reactiveOmit(props, 'class', 'inset', 'variant');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <MenubarItem
        v-bind="forwarded"
        data-slot="menubar-item"
        :data-inset="inset || undefined"
        :data-variant="variant"
        :class="
          cn(menubarItemVariants({ variant }), inset && 'nx:pl-8', props.class)
        "
      >
        <slot />
      </MenubarItem>
    </template>
    
    components/menubar/MenubarLabel.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      MenubarLabel,
      type MenubarLabelProps as RekaMenubarLabelProps,
      useForwardProps,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface MenubarLabelProps extends RekaMenubarLabelProps {
      /**
       * Indents the label to align with items that have a leading icon or checkbox.
       */
      inset?: boolean;
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<MenubarLabelProps>();
    
    const delegated = reactiveOmit(props, 'class', 'inset');
    const forwarded = useForwardProps(delegated);
    </script>
    
    <template>
      <MenubarLabel
        v-bind="forwarded"
        data-slot="menubar-label"
        :data-inset="inset || undefined"
        :class="
          cn(
            'nx:px-2 nx:py-1.5 nx:typography-label-default',
            inset && 'nx:pl-8',
            props.class
          )
        "
      >
        <slot />
      </MenubarLabel>
    </template>
    
    components/menubar/MenubarRadioItem.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      MenubarItemIndicator,
      MenubarRadioItem,
      type MenubarRadioItemEmits,
      type MenubarRadioItemProps as RekaMenubarRadioItemProps,
      useForwardPropsEmits,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { IconCircleFilled } from '../../lib/icons';
    import { selectionIndicatorMotionClassName } from '../../lib/motion';
    import { cn } from '../../lib/utils';
    
    export interface MenubarRadioItemProps extends RekaMenubarRadioItemProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<MenubarRadioItemProps>();
    const emits = defineEmits<MenubarRadioItemEmits>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <MenubarRadioItem
        v-bind="forwarded"
        data-slot="menubar-radio-item"
        :class="
          cn(
            'nx:group nx:relative nx:flex nx:cursor-default nx:select-none nx:items-center',
            'nx:rounded-sm nx:py-1.5 nx:pl-8 nx:pr-2 nx:typography-body-default nx:outline-none',
            'nx:transition-colors',
            'nx:focus:bg-popover-hover nx:focus:text-popover-foreground',
            'nx:data-disabled:pointer-events-none nx:data-disabled:text-disabled-foreground',
            props.class
          )
        "
      >
        <span
          class="nx:pointer-events-none nx:absolute nx:left-2 nx:flex nx:size-3.5 nx:items-center nx:justify-center"
        >
          <MenubarItemIndicator force-mount data-slot="menubar-radio-indicator">
            <IconCircleFilled
              data-slot="menubar-radio-indicator-icon"
              aria-hidden="true"
              :class="
                cn(
                  'nx:size-2',
                  selectionIndicatorMotionClassName,
                  'nx:group-data-[state=checked]:scale-100 nx:group-data-[state=checked]:opacity-100'
                )
              "
            />
          </MenubarItemIndicator>
        </span>
        <slot />
      </MenubarRadioItem>
    </template>
    
    components/menubar/MenubarSeparator.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      MenubarSeparator,
      type MenubarSeparatorProps as RekaMenubarSeparatorProps,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface MenubarSeparatorProps extends RekaMenubarSeparatorProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<MenubarSeparatorProps>();
    
    const delegated = reactiveOmit(props, 'class');
    </script>
    
    <template>
      <MenubarSeparator
        v-bind="delegated"
        data-slot="menubar-separator"
        :class="
          cn('nx:-mx-1 nx:my-1 nx:h-px nx:bg-border-default-alpha', props.class)
        "
      />
    </template>
    
    components/menubar/MenubarShortcut.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface MenubarShortcutProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<MenubarShortcutProps>();
    </script>
    
    <template>
      <span
        data-slot="menubar-shortcut"
        :class="
          cn(
            'nx:ml-auto nx:typography-shortcut nx:text-muted-foreground',
            props.class
          )
        "
      >
        <slot />
      </span>
    </template>
    
    components/menubar/MenubarSubContent.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      MenubarSubContent,
      type MenubarSubContentEmits,
      type MenubarSubContentProps as RekaMenubarSubContentProps,
      useForwardPropsEmits,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    import {
      overlayFloatingTransitionClassName,
      popoverSurfaceClassName,
    } from '../overlay-layout/overlay-layout';
    
    export interface MenubarSubContentProps extends RekaMenubarSubContentProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<MenubarSubContentProps>();
    const emits = defineEmits<MenubarSubContentEmits>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <MenubarSubContent
        v-bind="forwarded"
        data-slot="menubar-sub-content"
        :class="
          cn(
            'nx:z-popover nx:min-w-32 nx:overflow-hidden',
            popoverSurfaceClassName,
            'nx:p-1',
            overlayFloatingTransitionClassName,
            props.class
          )
        "
      >
        <slot />
      </MenubarSubContent>
    </template>
    
    components/menubar/MenubarSubTrigger.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      MenubarSubTrigger,
      type MenubarSubTriggerProps as RekaMenubarSubTriggerProps,
      useForwardProps,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { IconChevronRight } from '../../lib/icons';
    import { cn } from '../../lib/utils';
    
    export interface MenubarSubTriggerProps extends RekaMenubarSubTriggerProps {
      /**
       * Indents the trigger to align with items that have a leading icon or checkbox.
       */
      inset?: boolean;
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<MenubarSubTriggerProps>();
    
    const delegated = reactiveOmit(props, 'class', 'inset');
    const forwarded = useForwardProps(delegated);
    </script>
    
    <template>
      <MenubarSubTrigger
        v-bind="forwarded"
        data-slot="menubar-sub-trigger"
        :data-inset="inset || undefined"
        :class="
          cn(
            'nx:flex nx:cursor-default nx:select-none nx:items-center nx:gap-2',
            'nx:rounded-sm nx:px-2 nx:py-1.5 nx:typography-body-default nx:outline-none',
            'nx:focus:bg-popover-hover nx:focus:text-popover-foreground',
            'nx:data-[state=open]:bg-popover-hover nx:data-[state=open]:text-popover-foreground',
            'nx:[&_svg]:pointer-events-none nx:[&_svg]:size-4 nx:[&_svg]:shrink-0',
            inset && 'nx:pl-8',
            props.class
          )
        "
      >
        <slot />
        <IconChevronRight class="nx:ml-auto nx:size-4" />
      </MenubarSubTrigger>
    </template>
    
    components/menubar/MenubarTrigger.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      MenubarTrigger,
      type MenubarTriggerProps as RekaMenubarTriggerProps,
      useForwardProps,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface MenubarTriggerProps extends RekaMenubarTriggerProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<MenubarTriggerProps>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardProps(delegated);
    </script>
    
    <template>
      <MenubarTrigger
        v-bind="forwarded"
        data-slot="menubar-trigger"
        :class="
          cn(
            'nx:flex nx:select-none nx:items-center nx:rounded-sm nx:border-0 nx:bg-transparent nx:px-2 nx:py-1 nx:text-inherit',
            'nx:typography-label-default nx:outline-none',
            'nx:focus:bg-container-hover nx:focus:text-foreground',
            'nx:data-[state=open]:bg-container-hover nx:data-[state=open]:text-foreground',
            props.class
          )
        "
      >
        <slot />
      </MenubarTrigger>
    </template>
    
    components/menubar/index.ts
    import { cva, type VariantProps } from 'class-variance-authority';
    
    export { default as Menubar, type MenubarProps } from './Menubar.vue';
    export {
      default as MenubarCheckboxItem,
      type MenubarCheckboxItemProps,
    } from './MenubarCheckboxItem.vue';
    export {
      default as MenubarContent,
      type MenubarContentProps,
    } from './MenubarContent.vue';
    export {
      default as MenubarItem,
      type MenubarItemProps,
    } from './MenubarItem.vue';
    export {
      default as MenubarLabel,
      type MenubarLabelProps,
    } from './MenubarLabel.vue';
    export {
      default as MenubarRadioItem,
      type MenubarRadioItemProps,
    } from './MenubarRadioItem.vue';
    export {
      default as MenubarSeparator,
      type MenubarSeparatorProps,
    } from './MenubarSeparator.vue';
    export {
      default as MenubarShortcut,
      type MenubarShortcutProps,
    } from './MenubarShortcut.vue';
    export {
      default as MenubarSubContent,
      type MenubarSubContentProps,
    } from './MenubarSubContent.vue';
    export {
      default as MenubarSubTrigger,
      type MenubarSubTriggerProps,
    } from './MenubarSubTrigger.vue';
    export {
      default as MenubarTrigger,
      type MenubarTriggerProps,
    } from './MenubarTrigger.vue';
    export {
      MenubarGroup,
      type MenubarGroupProps,
      MenubarMenu,
      type MenubarMenuProps,
      MenubarPortal,
      type MenubarPortalProps,
      MenubarRadioGroup,
      type MenubarRadioGroupProps,
      MenubarSub,
      type MenubarSubProps,
    } from 'reka-ui';
    
    export const menubarItemVariants = cva(
      'nx:relative nx:flex nx:cursor-default nx:select-none nx:items-center nx:gap-2 nx:rounded-sm nx:px-2 nx:py-1.5 nx:typography-body-default nx:outline-none nx:transition-colors nx:focus:bg-popover-hover nx:focus:text-popover-foreground nx:data-disabled:pointer-events-none nx:data-disabled:text-disabled-foreground nx:[&_svg]:pointer-events-none nx:[&_svg]:size-4 nx:[&_svg]:shrink-0',
      {
        variants: {
          variant: {
            default: '',
            destructive:
              'nx:text-error-subtle-foreground nx:focus:bg-error-background nx:focus:text-error-foreground',
          },
        },
        defaultVariants: {
          variant: 'default',
        },
      }
    );
    
    export type MenubarItemVariants = VariantProps<typeof menubarItemVariants>;
    
    components/overlay-layout/overlay-layout.ts
    import { cva, type VariantProps } from 'class-variance-authority';
    
    /**
     * Popover-family surfaces share the translucent/blurred recipe; Tooltip uses a
     * solid carve-out because its small, brief text appears over arbitrary content.
     */
    export const popoverSurfaceClassName = [
      'nx:rounded-md',
      'nx:border-default',
      'nx:border-border-default',
      'nx:bg-popover-alpha',
      'nx:backdrop-blur-md',
      'nx:text-popover-foreground',
      'nx:typography-body-default',
      'nx:shadow-lg',
    ].join(' ');
    
    export const tooltipSurfaceClassName = [
      'nx:rounded-md',
      'nx:border-default',
      'nx:border-border-default',
      'nx:bg-popover',
      'nx:text-popover-foreground',
      'nx:typography-body-default',
      'nx:shadow-lg',
    ].join(' ');
    
    export const navigationMenuInlinePopoverSurfaceClassName = [
      'nx:group-data-[viewport=false]/navigation-menu:rounded-md',
      'nx:group-data-[viewport=false]/navigation-menu:border-default',
      'nx:group-data-[viewport=false]/navigation-menu:border-border-default',
      'nx:group-data-[viewport=false]/navigation-menu:bg-popover-alpha',
      'nx:group-data-[viewport=false]/navigation-menu:backdrop-blur-md',
      'nx:group-data-[viewport=false]/navigation-menu:text-popover-foreground',
      'nx:group-data-[viewport=false]/navigation-menu:shadow-lg',
    ].join(' ');
    
    export const navigationMenuInlinePopoverTransitionClassName = [
      'nx:group-data-[viewport=false]/navigation-menu:transition-[opacity,scale]',
      'nx:group-data-[viewport=false]/navigation-menu:duration-fast',
      'nx:group-data-[viewport=false]/navigation-menu:ease-move',
      'nx:group-data-[viewport=false]/navigation-menu:data-[state=closed]:scale-95',
      'nx:group-data-[viewport=false]/navigation-menu:data-[state=closed]:opacity-0',
      'nx:group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-overlay-presence-exit',
    ].join(' ');
    
    // `animate-overlay-presence-exit` is a non-visual "presence bridge": Reka's
    // Presence only waits on `animationName`, not `transitionend`, so this inert
    // animation keeps a closing overlay mounted while the opacity/scale/translate
    // TRANSITIONS below run the visible exit. It animates an unread custom property (see
    // generateMotionUtilitiesCSS in @nxds/core), never a transitioned property, so it
    // cannot override those transitions.
    const overlayPresenceExitClassName =
      'nx:data-[state=closed]:animate-overlay-presence-exit';
    
    export const overlayFloatingTransitionClassName = [
      'nx:transition-[opacity,scale]',
      'nx:duration-fast',
      'nx:ease-move',
      'nx:data-[state=closed]:scale-95',
      'nx:data-[state=closed]:opacity-0',
      overlayPresenceExitClassName,
    ].join(' ');
    
    const overlayContentTransitionClassName = [
      'nx:transition-[opacity,scale]',
      'nx:duration-default',
      'nx:ease-move',
      'nx:data-[state=closed]:scale-95',
      'nx:data-[state=closed]:opacity-0',
      overlayPresenceExitClassName,
    ].join(' ');
    
    export const overlayScrimTransitionClassName = [
      'nx:transition-opacity',
      'nx:duration-default',
      'nx:ease-move',
      'nx:data-[state=closed]:opacity-0',
      overlayPresenceExitClassName,
    ].join(' ');
    
    export const overlayPanelTransitionClassName = [
      'nx:transition-[translate]',
      'nx:duration-slow',
      'nx:ease-move',
      overlayPresenceExitClassName,
    ].join(' ');
    
    export const overlayContentVariants = cva(
      [
        // Centred with margins, not a translate: a transform would make this the
        // containing block for fixed-position popups a dialog holds.
        'nx:fixed nx:inset-0 nx:z-modal nx:m-auto nx:flex nx:h-fit nx:w-full nx:max-w-lg nx:flex-col',
        'nx:max-h-[calc(100svh-2rem)] nx:overflow-hidden',
        'nx:gap-4 nx:border-default nx:border-border-default nx:bg-container nx:py-6 nx:shadow-lg',
        overlayContentTransitionClassName,
        'nx:sm:rounded-lg',
      ].join(' ')
    );
    
    export const overlayScrimVariants = cva(
      [
        'nx:fixed nx:inset-0 nx:z-modal nx:bg-overlay',
        overlayScrimTransitionClassName,
      ].join(' ')
    );
    
    export const overlayHeaderVariants = cva(
      'nx:flex nx:shrink-0 nx:flex-col nx:gap-1 nx:px-6',
      {
        variants: {
          variant: {
            default: 'nx:text-center nx:sm:text-left',
            center: 'nx:items-center nx:text-center',
          },
        },
        defaultVariants: {
          variant: 'default',
        },
      }
    );
    
    export const overlayBodyClassName = [
      'nx:min-h-0',
      'nx:overflow-y-auto',
      'nx:px-6',
      'nx:focus-visible:outline-2',
      'nx:focus-visible:outline-focus-default',
      'nx:focus-visible:[outline-offset:-2px]',
    ].join(' ');
    
    export const overlayFooterVariants = cva(
      'nx:flex nx:shrink-0 nx:gap-2 nx:px-6',
      {
        variants: {
          orientation: {
            horizontal:
              'nx:flex-col nx:sm:flex-row nx:sm:items-center nx:sm:justify-end',
            vertical: 'nx:flex-col nx:items-stretch nx:*:w-full',
          },
        },
        defaultVariants: {
          orientation: 'horizontal',
        },
      }
    );
    
    export const overlayCloseButtonClassName = [
      'nx:absolute nx:right-6 nx:top-6 nx:rounded-sm nx:border-0 nx:bg-transparent nx:p-1 nx:text-muted-foreground-subtle',
      'nx:transition-control',
      'nx:hover:bg-container-hover nx:hover:text-foreground',
      'nx:focus-visible:bg-container-hover nx:focus-visible:text-foreground',
      'nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default',
      'nx:disabled:pointer-events-none',
    ].join(' ');
    
    export type OverlayHeaderVariants = VariantProps<typeof overlayHeaderVariants>;
    
    export type OverlayVariant = NonNullable<
      VariantProps<typeof overlayHeaderVariants>['variant']
    >;
    
    export type OverlayButtonOrientation = NonNullable<
      VariantProps<typeof overlayFooterVariants>['orientation']
    >;
    
    export type OverlayLayoutContextValue = {
      variant: OverlayVariant;
      buttonOrientation: OverlayButtonOrientation;
    };
    
    export const defaultOverlayLayout: OverlayLayoutContextValue = {
      variant: 'default',
      buttonOrientation: 'horizontal',
    };
    
    export function resolveOverlayButtonOrientation(
      variant: OverlayVariant
    ): OverlayButtonOrientation {
      return variant === 'center' ? 'vertical' : 'horizontal';
    }
    
    lib/icons.ts
    /**
     * Internal Icons
     *
     * This barrel file re-exports icons used internally by the design system.
     * These are hardcoded icons for component states (loading, validation, navigation).
     *
     * User-facing icons should be passed via props by consumers.
     *
     * This set draws Phosphor. `nxds init --icons` installs the same names drawn
     * by another library.
     */
    import { PhCircle } from '@phosphor-icons/vue';
    import { type FunctionalComponent, h } from 'vue';
    
    // Loading states
    export { PhCircleNotch as IconLoader2 } from '@phosphor-icons/vue';
    
    // Navigation
    export {
      PhArrowDown as IconArrowDown,
      PhArrowLeft as IconArrowLeft,
      PhArrowRight as IconArrowRight,
      PhArrowUp as IconArrowUp,
      PhCaretDown as IconChevronDown,
      PhCaretLeft as IconChevronLeft,
      PhCaretRight as IconChevronRight,
      PhCaretUp as IconChevronUp,
      PhDotsThree as IconDots,
      PhSidebarSimple as IconLayoutSidebar,
      PhCaretUpDown as IconSelector,
    } from '@phosphor-icons/vue';
    
    // Actions
    export {
      PhCheck as IconCheck,
      PhCopy as IconCopy,
      PhMinus as IconMinus,
      PhPencilSimple as IconPencil,
      PhPlus as IconPlus,
      PhX as IconX,
    } from '@phosphor-icons/vue';
    
    export const IconCircleFilled: FunctionalComponent = (_props, { attrs }) =>
      h(PhCircle, { weight: 'fill', ...attrs });
    
    // Filter field types
    export {
      PhCalendarBlank as IconCalendar,
      PhHash as IconHash,
      PhTextAa as IconLetterCase,
      PhListBullets as IconList,
    } from '@phosphor-icons/vue';
    
    // Search / input
    export { PhMagnifyingGlass as IconSearch } from '@phosphor-icons/vue';
    
    // Alerts/Status
    export {
      PhWarningCircle as IconAlertCircle,
      PhWarning as IconAlertTriangle,
      PhCheckCircle as IconCircleCheck,
      PhInfo as IconInfoCircle,
    } from '@phosphor-icons/vue';
    
    // Layout
    export {
      PhDotsSixVertical as IconGripVertical,
      PhPalette as IconPalette,
    } from '@phosphor-icons/vue';
    
    lib/motion.ts
    export const selectionIndicatorMotionClassName =
      'nx:scale-50 nx:opacity-0 nx:transition-[opacity,scale] nx:duration-fast nx:ease-enter';
    
  3. Keep each file at the path shown above it.

    Paths are relative to packages/vue/src/. Keep the same folders under your @/ root, and the imports between files work without edits.

Usage

import { Menubar, MenubarCheckboxItem, MenubarContent, MenubarItem, MenubarLabel, MenubarMenu, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger } from '@/components/menubar';

Examples

With checkbox items

Toggle settings on and off with checkbox items.

<script setup lang="ts">
import { ref } from 'vue';

import {
  Menubar,
  MenubarCheckboxItem,
  MenubarContent,
  MenubarLabel,
  MenubarMenu,
  MenubarSeparator,
  MenubarTrigger,
} from '@/components/menubar';

const showBookmarks = ref(true);
const showFullUrls = ref(false);
</script>

<template>
  <Menubar>
    <MenubarMenu>
      <MenubarTrigger>View</MenubarTrigger>
      <MenubarContent>
        <MenubarLabel>Appearance</MenubarLabel>
        <MenubarSeparator />
        <MenubarCheckboxItem v-model="showBookmarks"
          >Show Bookmarks</MenubarCheckboxItem
        >
        <MenubarCheckboxItem v-model="showFullUrls"
          >Show Full URLs</MenubarCheckboxItem
        >
      </MenubarContent>
    </MenubarMenu>
  </Menubar>
</template>

With radio items

Pick one option from a group with radio items.

<script setup lang="ts">
import { ref } from 'vue';

import {
  Menubar,
  MenubarContent,
  MenubarLabel,
  MenubarMenu,
  MenubarRadioGroup,
  MenubarRadioItem,
  MenubarSeparator,
  MenubarTrigger,
} from '@/components/menubar';

const profile = ref('benoit');
</script>

<template>
  <Menubar>
    <MenubarMenu>
      <MenubarTrigger>Profiles</MenubarTrigger>
      <MenubarContent>
        <MenubarLabel>People</MenubarLabel>
        <MenubarSeparator />
        <MenubarRadioGroup v-model="profile">
          <MenubarRadioItem value="benoit">Benoit</MenubarRadioItem>
          <MenubarRadioItem value="luis">Luis</MenubarRadioItem>
        </MenubarRadioGroup>
      </MenubarContent>
    </MenubarMenu>
  </Menubar>
</template>

With sub menu

Nest related commands in a submenu that opens from an item.

<script setup lang="ts">
import {
  Menubar,
  MenubarContent,
  MenubarItem,
  MenubarMenu,
  MenubarSeparator,
  MenubarSub,
  MenubarSubContent,
  MenubarSubTrigger,
  MenubarTrigger,
} from '@/components/menubar';
</script>

<template>
  <Menubar>
    <MenubarMenu>
      <MenubarTrigger>File</MenubarTrigger>
      <MenubarContent>
        <MenubarItem>New Tab</MenubarItem>
        <MenubarSeparator />
        <MenubarSub>
          <MenubarSubTrigger>Share</MenubarSubTrigger>
          <MenubarSubContent>
            <MenubarItem>Email Link</MenubarItem>
            <MenubarItem>Messages</MenubarItem>
            <MenubarItem>Notes</MenubarItem>
          </MenubarSubContent>
        </MenubarSub>
        <MenubarSeparator />
        <MenubarItem>Print</MenubarItem>
      </MenubarContent>
    </MenubarMenu>
  </Menubar>
</template>

With inset items

Indent items and labels with inset to line up with items that have icons.

<script setup lang="ts">
import {
  Menubar,
  MenubarContent,
  MenubarItem,
  MenubarLabel,
  MenubarMenu,
  MenubarSeparator,
  MenubarTrigger,
} from '@/components/menubar';
</script>

<template>
  <Menubar>
    <MenubarMenu>
      <MenubarTrigger>View</MenubarTrigger>
      <MenubarContent>
        <MenubarLabel inset>Appearance</MenubarLabel>
        <MenubarSeparator />
        <MenubarItem inset>Reload</MenubarItem>
        <MenubarItem inset>Force Reload</MenubarItem>
        <MenubarItem inset>Toggle Fullscreen</MenubarItem>
      </MenubarContent>
    </MenubarMenu>
  </Menubar>
</template>

With disabled items

Show unavailable commands as disabled items that cannot be selected.

<script setup lang="ts">
import {
  Menubar,
  MenubarContent,
  MenubarItem,
  MenubarMenu,
  MenubarSeparator,
  MenubarTrigger,
} from '@/components/menubar';
</script>

<template>
  <Menubar>
    <MenubarMenu>
      <MenubarTrigger>Edit</MenubarTrigger>
      <MenubarContent>
        <MenubarItem>Undo</MenubarItem>
        <MenubarItem>Redo</MenubarItem>
        <MenubarSeparator />
        <MenubarItem disabled>Cut</MenubarItem>
        <MenubarItem disabled>Copy</MenubarItem>
        <MenubarItem>Paste</MenubarItem>
      </MenubarContent>
    </MenubarMenu>
  </Menubar>
</template>

Props

Menubar

Menubar adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarCheckboxItem

MenubarCheckboxItem adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarContent

MenubarContent adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarItem

MenubarItem props
PropTypeDefaultDescription
insetbooleanNo default documented

Indents the item to align with items that have a leading icon or checkbox.

variant"default" | "destructive" | null"default"No description

MenubarLabel

MenubarLabel props
PropTypeDefaultDescription
insetbooleanNo default documented

Indents the label to align with items that have a leading icon or checkbox.

MenubarRadioItem

MenubarRadioItem adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarSeparator

MenubarSeparator adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarShortcut

MenubarShortcut adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarSubContent

MenubarSubContent adds no props of its own; it accepts the props of the element or primitive it wraps.

MenubarSubTrigger

MenubarSubTrigger props
PropTypeDefaultDescription
insetbooleanNo default documented

Indents the trigger to align with items that have a leading icon or checkbox.

MenubarTrigger

MenubarTrigger adds no props of its own; it accepts the props of the element or primitive it wraps.