Skip to main content
Nexus Design System
GitHub

DropdownMenu

DropdownMenu opens a list of actions from a button. Use it for a short list of actions or options. For picking a form value, use Select.

'use client';

import { Button } from '@/components/nxds/components/button/button';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuTrigger,
} from '@/components/nxds/components/dropdown-menu/dropdown-menu';

export default function DropdownMenuWithLabelsAndSeparators() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline">My Account</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent className="nx:w-56">
        <DropdownMenuLabel>My Account</DropdownMenuLabel>
        <DropdownMenuSeparator />
        <DropdownMenuGroup>
          <DropdownMenuItem>
            Profile
            <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
          </DropdownMenuItem>
          <DropdownMenuItem>
            Billing
            <DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
          </DropdownMenuItem>
          <DropdownMenuItem>
            Settings
            <DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
          </DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuItem>
          Logout
          <DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

This example also needs: Button.

Installation

npx nxds-cli@latest add dropdown-menu

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

Usage

import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
} from '@/components/nxds/components/dropdown-menu/dropdown-menu';

Composition

DropdownMenu
├── DropdownMenuTrigger
└── DropdownMenuContent
    ├── DropdownMenuLabel
    ├── DropdownMenuSeparator
    ├── DropdownMenuGroup
    │   └── DropdownMenuItem
    │       └── DropdownMenuShortcut
    ├── DropdownMenuItem
    │   └── DropdownMenuShortcut
    ├── DropdownMenuCheckboxItem
    ├── DropdownMenuRadioGroup
    │   └── DropdownMenuRadioItem
    └── DropdownMenuSub
        ├── DropdownMenuSubTrigger
        └── DropdownMenuSubContent
            ├── DropdownMenuItem
            └── DropdownMenuSeparator

Examples

With checkbox items

Use DropdownMenuCheckboxItem for settings that toggle on and off.

'use client';

import * as React from 'react';

import { Button } from '@/components/nxds/components/button/button';
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from '@/components/nxds/components/dropdown-menu/dropdown-menu';

export default function DropdownMenuWithCheckboxItems() {
  const [showStatusBar, setShowStatusBar] = React.useState(true);
  const [showActivityBar, setShowActivityBar] = React.useState(false);
  const [showPanel, setShowPanel] = React.useState(false);

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline">View Options</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent className="nx:w-56">
        <DropdownMenuLabel>Appearance</DropdownMenuLabel>
        <DropdownMenuSeparator />
        <DropdownMenuCheckboxItem
          checked={showStatusBar}
          onCheckedChange={setShowStatusBar}
        >
          Status Bar
        </DropdownMenuCheckboxItem>
        <DropdownMenuCheckboxItem
          checked={showActivityBar}
          onCheckedChange={setShowActivityBar}
        >
          Activity Bar
        </DropdownMenuCheckboxItem>
        <DropdownMenuCheckboxItem
          checked={showPanel}
          onCheckedChange={setShowPanel}
        >
          Panel
        </DropdownMenuCheckboxItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

With radio items

Use DropdownMenuRadioItem inside a DropdownMenuRadioGroup to pick one option.

'use client';

import * as React from 'react';

import { Button } from '@/components/nxds/components/button/button';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from '@/components/nxds/components/dropdown-menu/dropdown-menu';

export default function DropdownMenuWithRadioItems() {
  const [position, setPosition] = React.useState('bottom');

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline">Panel Position</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent className="nx:w-56">
        <DropdownMenuLabel>Panel Position</DropdownMenuLabel>
        <DropdownMenuSeparator />
        <DropdownMenuRadioGroup value={position} onValueChange={setPosition}>
          <DropdownMenuRadioItem value="top">Top</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="bottom">Bottom</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="right">Right</DropdownMenuRadioItem>
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

With sub menu

Nest a DropdownMenuSub to open a second menu from an item.

'use client';

import { Button } from '@/components/nxds/components/button/button';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
} from '@/components/nxds/components/dropdown-menu/dropdown-menu';

export default function DropdownMenuWithSubMenu() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline">Open Menu</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent className="nx:w-56">
        <DropdownMenuItem>
          New Tab
          <DropdownMenuShortcut>⌘T</DropdownMenuShortcut>
        </DropdownMenuItem>
        <DropdownMenuItem>
          New Window
          <DropdownMenuShortcut>⌘N</DropdownMenuShortcut>
        </DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuSub>
          <DropdownMenuSubTrigger>More Tools</DropdownMenuSubTrigger>
          <DropdownMenuSubContent>
            <DropdownMenuItem>Save Page As...</DropdownMenuItem>
            <DropdownMenuItem>Create Shortcut...</DropdownMenuItem>
            <DropdownMenuItem>Name Window...</DropdownMenuItem>
            <DropdownMenuSeparator />
            <DropdownMenuItem>Developer Tools</DropdownMenuItem>
          </DropdownMenuSubContent>
        </DropdownMenuSub>
        <DropdownMenuSeparator />
        <DropdownMenuItem>
          Exit
          <DropdownMenuShortcut>⌘Q</DropdownMenuShortcut>
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

Props

DropdownMenu

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

DropdownMenuCheckboxItem

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

DropdownMenuContent

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

DropdownMenuGroup

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

DropdownMenuItem

DropdownMenuItem props
PropTypeDefaultDescription
insetbooleanNo default documented

Indents the item to line up with items that have a leading icon or check.

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

DropdownMenuLabel

DropdownMenuLabel props
PropTypeDefaultDescription
insetbooleanNo default documented

Indents the label to line up with items that have a leading icon or check.

DropdownMenuPortal

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

DropdownMenuRadioGroup

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

DropdownMenuRadioItem

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

DropdownMenuSeparator

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

DropdownMenuShortcut

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

DropdownMenuSub

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

DropdownMenuSubContent

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

DropdownMenuSubTrigger

DropdownMenuSubTrigger props
PropTypeDefaultDescription
insetbooleanNo default documented

Indents the trigger to line up with items that have a leading icon or check.

DropdownMenuTrigger

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

DropdownMenu opens a list of actions from a button. Use it for a short list of actions or options. For picking a form value, use Select.

<script setup lang="ts">
import { Button } from '@/components/button';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuTrigger,
} from '@/components/dropdown-menu';
</script>

<template>
  <DropdownMenu>
    <DropdownMenuTrigger as-child>
      <Button variant="outline">My Account</Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent class="nx:w-56">
      <DropdownMenuLabel>My Account</DropdownMenuLabel>
      <DropdownMenuSeparator />
      <DropdownMenuGroup>
        <DropdownMenuItem
          >Profile<DropdownMenuShortcut
            >⇧⌘P</DropdownMenuShortcut
          ></DropdownMenuItem
        >
        <DropdownMenuItem
          >Billing<DropdownMenuShortcut
            >⌘B</DropdownMenuShortcut
          ></DropdownMenuItem
        >
        <DropdownMenuItem
          >Settings<DropdownMenuShortcut
            >⌘S</DropdownMenuShortcut
          ></DropdownMenuItem
        >
      </DropdownMenuGroup>
      <DropdownMenuSeparator />
      <DropdownMenuItem
        >Logout<DropdownMenuShortcut
          >⇧⌘Q</DropdownMenuShortcut
        ></DropdownMenuItem
      >
    </DropdownMenuContent>
  </DropdownMenu>
</template>

This example also needs: Button.

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/dropdown-menu/DropdownMenuCheckboxItem.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      DropdownMenuCheckboxItem,
      type DropdownMenuCheckboxItemEmits,
      type DropdownMenuCheckboxItemProps as RekaDropdownMenuCheckboxItemProps,
      DropdownMenuItemIndicator,
      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 DropdownMenuCheckboxItemProps extends RekaDropdownMenuCheckboxItemProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<DropdownMenuCheckboxItemProps>();
    const emits = defineEmits<DropdownMenuCheckboxItemEmits>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <DropdownMenuCheckboxItem
        v-bind="forwarded"
        data-slot="dropdown-menu-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"
        >
          <DropdownMenuItemIndicator
            force-mount
            data-slot="dropdown-menu-checkbox-indicator"
          >
            <IconCheck
              data-slot="dropdown-menu-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'
                )
              "
            />
          </DropdownMenuItemIndicator>
        </span>
        <slot />
      </DropdownMenuCheckboxItem>
    </template>
    
    components/dropdown-menu/DropdownMenuContent.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      DropdownMenuContent,
      type DropdownMenuContentEmits,
      type DropdownMenuContentProps as RekaDropdownMenuContentProps,
      DropdownMenuPortal,
      useForwardPropsEmits,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    import {
      overlayFloatingTransitionClassName,
      popoverSurfaceClassName,
    } from '../overlay-layout/overlay-layout';
    
    export interface DropdownMenuContentProps extends RekaDropdownMenuContentProps {
      class?: HTMLAttributes['class'];
    }
    
    defineOptions({ inheritAttrs: false });
    
    const props = withDefaults(defineProps<DropdownMenuContentProps>(), {
      sideOffset: 4,
    });
    const emits = defineEmits<DropdownMenuContentEmits>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <DropdownMenuPortal>
        <DropdownMenuContent
          v-bind="{ ...$attrs, ...forwarded }"
          data-slot="dropdown-menu-content"
          :class="
            cn(
              'nx:z-popover nx:max-h-(--reka-dropdown-menu-content-available-height)',
              'nx:min-w-32 nx:overflow-x-hidden nx:overflow-y-auto',
              popoverSurfaceClassName,
              'nx:p-1',
              overlayFloatingTransitionClassName,
              props.class
            )
          "
        >
          <slot />
        </DropdownMenuContent>
      </DropdownMenuPortal>
    </template>
    
    components/dropdown-menu/DropdownMenuItem.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      DropdownMenuItem,
      type DropdownMenuItemEmits,
      type DropdownMenuItemProps as RekaDropdownMenuItemProps,
      useForwardPropsEmits,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    import { type DropdownMenuItemVariants, dropdownMenuItemVariants } from '.';
    
    export interface DropdownMenuItemProps extends RekaDropdownMenuItemProps {
      variant?: DropdownMenuItemVariants['variant'];
      /**
       * Indents the item to line up with items that have a leading icon or check.
       */
      inset?: boolean;
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<DropdownMenuItemProps>(), {
      variant: 'default',
    });
    const emits = defineEmits<DropdownMenuItemEmits>();
    
    const delegated = reactiveOmit(props, 'class', 'inset', 'variant');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <DropdownMenuItem
        v-bind="forwarded"
        data-slot="dropdown-menu-item"
        :data-inset="inset || undefined"
        :data-variant="variant"
        :class="
          cn(dropdownMenuItemVariants({ variant }), inset && 'nx:pl-8', props.class)
        "
      >
        <slot />
      </DropdownMenuItem>
    </template>
    
    components/dropdown-menu/DropdownMenuLabel.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      DropdownMenuLabel,
      type DropdownMenuLabelProps as RekaDropdownMenuLabelProps,
      useForwardProps,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface DropdownMenuLabelProps extends RekaDropdownMenuLabelProps {
      /**
       * Indents the label to line up with items that have a leading icon or check.
       */
      inset?: boolean;
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<DropdownMenuLabelProps>();
    
    const delegated = reactiveOmit(props, 'class', 'inset');
    const forwarded = useForwardProps(delegated);
    </script>
    
    <template>
      <DropdownMenuLabel
        v-bind="forwarded"
        data-slot="dropdown-menu-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 />
      </DropdownMenuLabel>
    </template>
    
    components/dropdown-menu/DropdownMenuRadioItem.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      DropdownMenuItemIndicator,
      DropdownMenuRadioItem,
      type DropdownMenuRadioItemEmits,
      type DropdownMenuRadioItemProps as RekaDropdownMenuRadioItemProps,
      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 DropdownMenuRadioItemProps extends RekaDropdownMenuRadioItemProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<DropdownMenuRadioItemProps>();
    const emits = defineEmits<DropdownMenuRadioItemEmits>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <DropdownMenuRadioItem
        v-bind="forwarded"
        data-slot="dropdown-menu-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"
        >
          <DropdownMenuItemIndicator
            force-mount
            data-slot="dropdown-menu-radio-indicator"
          >
            <IconCircleFilled
              data-slot="dropdown-menu-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'
                )
              "
            />
          </DropdownMenuItemIndicator>
        </span>
        <slot />
      </DropdownMenuRadioItem>
    </template>
    
    components/dropdown-menu/DropdownMenuSeparator.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      DropdownMenuSeparator,
      type DropdownMenuSeparatorProps as RekaDropdownMenuSeparatorProps,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface DropdownMenuSeparatorProps extends RekaDropdownMenuSeparatorProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<DropdownMenuSeparatorProps>();
    
    const delegated = reactiveOmit(props, 'class');
    </script>
    
    <template>
      <DropdownMenuSeparator
        v-bind="delegated"
        data-slot="dropdown-menu-separator"
        :class="
          cn('nx:-mx-1 nx:my-1 nx:h-px nx:bg-border-default-alpha', props.class)
        "
      />
    </template>
    
    components/dropdown-menu/DropdownMenuShortcut.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface DropdownMenuShortcutProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<DropdownMenuShortcutProps>();
    </script>
    
    <template>
      <span
        data-slot="dropdown-menu-shortcut"
        :class="
          cn(
            'nx:ml-auto nx:typography-shortcut nx:text-muted-foreground',
            props.class
          )
        "
      >
        <slot />
      </span>
    </template>
    
    components/dropdown-menu/DropdownMenuSubContent.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      DropdownMenuSubContent,
      type DropdownMenuSubContentEmits,
      type DropdownMenuSubContentProps as RekaDropdownMenuSubContentProps,
      useForwardPropsEmits,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    import {
      overlayFloatingTransitionClassName,
      popoverSurfaceClassName,
    } from '../overlay-layout/overlay-layout';
    
    export interface DropdownMenuSubContentProps extends RekaDropdownMenuSubContentProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<DropdownMenuSubContentProps>();
    const emits = defineEmits<DropdownMenuSubContentEmits>();
    
    const delegated = reactiveOmit(props, 'class');
    const forwarded = useForwardPropsEmits(delegated, emits);
    </script>
    
    <template>
      <DropdownMenuSubContent
        v-bind="forwarded"
        data-slot="dropdown-menu-sub-content"
        :class="
          cn(
            'nx:z-popover nx:min-w-32 nx:overflow-hidden',
            popoverSurfaceClassName,
            'nx:p-1',
            overlayFloatingTransitionClassName,
            props.class
          )
        "
      >
        <slot />
      </DropdownMenuSubContent>
    </template>
    
    components/dropdown-menu/DropdownMenuSubTrigger.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import {
      DropdownMenuSubTrigger,
      type DropdownMenuSubTriggerProps as RekaDropdownMenuSubTriggerProps,
      useForwardProps,
    } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { IconChevronRight } from '../../lib/icons';
    import { cn } from '../../lib/utils';
    
    export interface DropdownMenuSubTriggerProps extends RekaDropdownMenuSubTriggerProps {
      /**
       * Indents the trigger to line up with items that have a leading icon or check.
       */
      inset?: boolean;
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<DropdownMenuSubTriggerProps>();
    
    const delegated = reactiveOmit(props, 'class', 'inset');
    const forwarded = useForwardProps(delegated);
    </script>
    
    <template>
      <DropdownMenuSubTrigger
        v-bind="forwarded"
        data-slot="dropdown-menu-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" />
      </DropdownMenuSubTrigger>
    </template>
    
    components/dropdown-menu/index.ts
    import { cva, type VariantProps } from 'class-variance-authority';
    
    export {
      default as DropdownMenuCheckboxItem,
      type DropdownMenuCheckboxItemProps,
    } from './DropdownMenuCheckboxItem.vue';
    export {
      default as DropdownMenuContent,
      type DropdownMenuContentProps,
    } from './DropdownMenuContent.vue';
    export {
      default as DropdownMenuItem,
      type DropdownMenuItemProps,
    } from './DropdownMenuItem.vue';
    export {
      default as DropdownMenuLabel,
      type DropdownMenuLabelProps,
    } from './DropdownMenuLabel.vue';
    export {
      default as DropdownMenuRadioItem,
      type DropdownMenuRadioItemProps,
    } from './DropdownMenuRadioItem.vue';
    export {
      default as DropdownMenuSeparator,
      type DropdownMenuSeparatorProps,
    } from './DropdownMenuSeparator.vue';
    export {
      default as DropdownMenuShortcut,
      type DropdownMenuShortcutProps,
    } from './DropdownMenuShortcut.vue';
    export {
      default as DropdownMenuSubContent,
      type DropdownMenuSubContentProps,
    } from './DropdownMenuSubContent.vue';
    export {
      default as DropdownMenuSubTrigger,
      type DropdownMenuSubTriggerProps,
    } from './DropdownMenuSubTrigger.vue';
    export {
      DropdownMenuRoot as DropdownMenu,
      DropdownMenuGroup,
      type DropdownMenuGroupProps,
      DropdownMenuPortal,
      type DropdownMenuPortalProps,
      type DropdownMenuRootProps as DropdownMenuProps,
      DropdownMenuRadioGroup,
      type DropdownMenuRadioGroupProps,
      DropdownMenuSub,
      type DropdownMenuSubProps,
      DropdownMenuTrigger,
      type DropdownMenuTriggerProps,
    } from 'reka-ui';
    
    export const dropdownMenuItemVariants = 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 DropdownMenuItemVariants = VariantProps<
      typeof dropdownMenuItemVariants
    >;
    
    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 { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '@/components/dropdown-menu';

Examples

With checkbox items

Use DropdownMenuCheckboxItem for settings that toggle on and off.

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

import { Button } from '@/components/button';
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from '@/components/dropdown-menu';

const showStatusBar = ref(true);
const showActivityBar = ref(false);
const showPanel = ref(false);
</script>

<template>
  <DropdownMenu>
    <DropdownMenuTrigger as-child>
      <Button variant="outline">View Options</Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent class="nx:w-56">
      <DropdownMenuLabel>Appearance</DropdownMenuLabel>
      <DropdownMenuSeparator />
      <DropdownMenuCheckboxItem v-model="showStatusBar"
        >Status Bar</DropdownMenuCheckboxItem
      >
      <DropdownMenuCheckboxItem v-model="showActivityBar"
        >Activity Bar</DropdownMenuCheckboxItem
      >
      <DropdownMenuCheckboxItem v-model="showPanel"
        >Panel</DropdownMenuCheckboxItem
      >
    </DropdownMenuContent>
  </DropdownMenu>
</template>

With radio items

Use DropdownMenuRadioItem inside a DropdownMenuRadioGroup to pick one option.

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

import { Button } from '@/components/button';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from '@/components/dropdown-menu';

const position = ref('bottom');
</script>

<template>
  <DropdownMenu>
    <DropdownMenuTrigger as-child>
      <Button variant="outline">Panel Position</Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent class="nx:w-56">
      <DropdownMenuLabel>Panel Position</DropdownMenuLabel>
      <DropdownMenuSeparator />
      <DropdownMenuRadioGroup v-model="position">
        <DropdownMenuRadioItem value="top">Top</DropdownMenuRadioItem>
        <DropdownMenuRadioItem value="bottom">Bottom</DropdownMenuRadioItem>
        <DropdownMenuRadioItem value="right">Right</DropdownMenuRadioItem>
      </DropdownMenuRadioGroup>
    </DropdownMenuContent>
  </DropdownMenu>
</template>

With sub menu

Nest a DropdownMenuSub to open a second menu from an item.

<script setup lang="ts">
import { Button } from '@/components/button';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
} from '@/components/dropdown-menu';
</script>

<template>
  <DropdownMenu>
    <DropdownMenuTrigger as-child>
      <Button variant="outline">Open Menu</Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent class="nx:w-56">
      <DropdownMenuItem
        >New Tab<DropdownMenuShortcut
          >⌘T</DropdownMenuShortcut
        ></DropdownMenuItem
      >
      <DropdownMenuItem
        >New Window<DropdownMenuShortcut
          >⌘N</DropdownMenuShortcut
        ></DropdownMenuItem
      >
      <DropdownMenuSeparator />
      <DropdownMenuSub>
        <DropdownMenuSubTrigger>More Tools</DropdownMenuSubTrigger>
        <DropdownMenuSubContent>
          <DropdownMenuItem>Save Page As...</DropdownMenuItem>
          <DropdownMenuItem>Create Shortcut...</DropdownMenuItem>
          <DropdownMenuItem>Name Window...</DropdownMenuItem>
          <DropdownMenuSeparator />
          <DropdownMenuItem>Developer Tools</DropdownMenuItem>
        </DropdownMenuSubContent>
      </DropdownMenuSub>
      <DropdownMenuSeparator />
      <DropdownMenuItem
        >Exit<DropdownMenuShortcut>⌘Q</DropdownMenuShortcut></DropdownMenuItem
      >
    </DropdownMenuContent>
  </DropdownMenu>
</template>

Props

DropdownMenuCheckboxItem

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

DropdownMenuContent

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

DropdownMenuItem

DropdownMenuItem props
PropTypeDefaultDescription
insetbooleanNo default documented

Indents the item to line up with items that have a leading icon or check.

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

DropdownMenuLabel

DropdownMenuLabel props
PropTypeDefaultDescription
insetbooleanNo default documented

Indents the label to line up with items that have a leading icon or check.

DropdownMenuRadioItem

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

DropdownMenuSeparator

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

DropdownMenuShortcut

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

DropdownMenuSubContent

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

DropdownMenuSubTrigger

DropdownMenuSubTrigger props
PropTypeDefaultDescription
insetbooleanNo default documented

Indents the trigger to line up with items that have a leading icon or check.