ContextMenu
ContextMenu shows a menu of actions when the user right-clicks an area. Use it for actions on a specific item or region. For a menu opened by a button, use DropdownMenu.
'use client';
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from '@/components/nxds/components/context-menu/context-menu';
const triggerClass =
'nx:flex nx:h-[96px] nx:w-full nx:max-w-md nx:select-none nx:items-center nx:justify-center nx:rounded-md nx:border-default nx:border-dashed nx:border-border-default nx:typography-body-default nx:text-muted-foreground';
export default function ContextMenuDefault() {
return (
<ContextMenu>
<ContextMenuTrigger className={triggerClass}>
Right click here
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Back</ContextMenuItem>
<ContextMenuItem>Forward</ContextMenuItem>
<ContextMenuItem>Reload</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}
Installation
npx nxds-cli@latest add context-menu
Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.
Usage
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from '@/components/nxds/components/context-menu/context-menu';
Composition
ContextMenu
├── ContextMenuTrigger
└── ContextMenuContent
├── ContextMenuItem
│ └── ContextMenuShortcut
├── ContextMenuLabel
├── ContextMenuSeparator
├── ContextMenuGroup
│ └── ContextMenuItem
│ └── ContextMenuShortcut
├── ContextMenuCheckboxItem
├── ContextMenuRadioGroup
│ └── ContextMenuRadioItem
└── ContextMenuSub
├── ContextMenuSubTrigger
└── ContextMenuSubContent
├── ContextMenuItem
└── ContextMenuSeparator
Examples
With labels and separators
Split items into sections with ContextMenuLabel headings and ContextMenuSeparator dividers.
'use client';
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuTrigger,
} from '@/components/nxds/components/context-menu/context-menu';
const triggerClass =
'nx:flex nx:h-[96px] nx:w-full nx:max-w-md nx:select-none nx:items-center nx:justify-center nx:rounded-md nx:border-default nx:border-dashed nx:border-border-default nx:typography-body-default nx:text-muted-foreground';
export default function ContextMenuWithLabelsAndSeparators() {
return (
<ContextMenu>
<ContextMenuTrigger className={triggerClass}>
Right click here
</ContextMenuTrigger>
<ContextMenuContent className="nx:w-56">
<ContextMenuLabel>Actions</ContextMenuLabel>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem>
Back
<ContextMenuShortcut>⌘[</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>
Forward
<ContextMenuShortcut>⌘]</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>
Reload
<ContextMenuShortcut>⌘R</ContextMenuShortcut>
</ContextMenuItem>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuItem>
View Source
<ContextMenuShortcut>⌘U</ContextMenuShortcut>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}
With checkbox items
Use ContextMenuCheckboxItem for settings that toggle on and off.
'use client';
import * as React from 'react';
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuTrigger,
} from '@/components/nxds/components/context-menu/context-menu';
const triggerClass =
'nx:flex nx:h-[96px] nx:w-full nx:max-w-md nx:select-none nx:items-center nx:justify-center nx:rounded-md nx:border-default nx:border-dashed nx:border-border-default nx:typography-body-default nx:text-muted-foreground';
export default function ContextMenuWithCheckboxItems() {
const [showBookmarks, setShowBookmarks] = React.useState(true);
const [showFullUrls, setShowFullUrls] = React.useState(false);
return (
<ContextMenu>
<ContextMenuTrigger className={triggerClass}>
Right click here
</ContextMenuTrigger>
<ContextMenuContent className="nx:w-56">
<ContextMenuLabel>Appearance</ContextMenuLabel>
<ContextMenuSeparator />
<ContextMenuCheckboxItem
checked={showBookmarks}
onCheckedChange={setShowBookmarks}
>
Show Bookmarks
</ContextMenuCheckboxItem>
<ContextMenuCheckboxItem
checked={showFullUrls}
onCheckedChange={setShowFullUrls}
>
Show Full URLs
</ContextMenuCheckboxItem>
</ContextMenuContent>
</ContextMenu>
);
}
With radio items
Use ContextMenuRadioItem inside a ContextMenuRadioGroup to pick one option.
'use client';
import * as React from 'react';
import {
ContextMenu,
ContextMenuContent,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from '@/components/nxds/components/context-menu/context-menu';
const triggerClass =
'nx:flex nx:h-[96px] nx:w-full nx:max-w-md nx:select-none nx:items-center nx:justify-center nx:rounded-md nx:border-default nx:border-dashed nx:border-border-default nx:typography-body-default nx:text-muted-foreground';
export default function ContextMenuWithRadioItems() {
const [person, setPerson] = React.useState('pedro');
return (
<ContextMenu>
<ContextMenuTrigger className={triggerClass}>
Right click here
</ContextMenuTrigger>
<ContextMenuContent className="nx:w-56">
<ContextMenuLabel>People</ContextMenuLabel>
<ContextMenuSeparator />
<ContextMenuRadioGroup value={person} onValueChange={setPerson}>
<ContextMenuRadioItem value="pedro">
Pedro Duarte
</ContextMenuRadioItem>
<ContextMenuRadioItem value="colm">Colm Tuite</ContextMenuRadioItem>
</ContextMenuRadioGroup>
</ContextMenuContent>
</ContextMenu>
);
}
With sub menu
Nest a ContextMenuSub to open a second menu from an item.
'use client';
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from '@/components/nxds/components/context-menu/context-menu';
const triggerClass =
'nx:flex nx:h-[96px] nx:w-full nx:max-w-md nx:select-none nx:items-center nx:justify-center nx:rounded-md nx:border-default nx:border-dashed nx:border-border-default nx:typography-body-default nx:text-muted-foreground';
export default function ContextMenuWithSubMenu() {
return (
<ContextMenu>
<ContextMenuTrigger className={triggerClass}>
Right click here
</ContextMenuTrigger>
<ContextMenuContent className="nx:w-56">
<ContextMenuItem>
Back
<ContextMenuShortcut>⌘[</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>
Forward
<ContextMenuShortcut>⌘]</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuSub>
<ContextMenuSubTrigger>More Tools</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem>Save Page As...</ContextMenuItem>
<ContextMenuItem>Create Shortcut...</ContextMenuItem>
<ContextMenuItem>Name Window...</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem>Developer Tools</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuItem>
Reload
<ContextMenuShortcut>⌘R</ContextMenuShortcut>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}
With destructive item
Set variant="destructive" on an item for actions like Delete.
'use client';
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from '@/components/nxds/components/context-menu/context-menu';
const triggerClass =
'nx:flex nx:h-[96px] nx:w-full nx:max-w-md nx:select-none nx:items-center nx:justify-center nx:rounded-md nx:border-default nx:border-dashed nx:border-border-default nx:typography-body-default nx:text-muted-foreground';
export default function ContextMenuWithDestructiveItem() {
return (
<ContextMenu>
<ContextMenuTrigger className={triggerClass}>
Right click here
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Edit</ContextMenuItem>
<ContextMenuItem>Duplicate</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem variant="destructive">Delete</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}
Props
ContextMenu
ContextMenu adds no props of its own; it accepts the props of the element or primitive it wraps.
ContextMenuCheckboxItem
ContextMenuCheckboxItem adds no props of its own; it accepts the props of the element or primitive it wraps.
ContextMenuContent
ContextMenuContent adds no props of its own; it accepts the props of the element or primitive it wraps.
ContextMenuGroup
ContextMenuGroup adds no props of its own; it accepts the props of the element or primitive it wraps.
ContextMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | —No default documented | Indents the item to line up with items that have a leading icon or check. |
variant | "default" | "destructive" | null | "default" | —No description |
ContextMenuLabel
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | —No default documented | Indents the label to line up with items that have a leading icon or check. |
ContextMenuPortal
ContextMenuPortal adds no props of its own; it accepts the props of the element or primitive it wraps.
ContextMenuRadioGroup
ContextMenuRadioGroup adds no props of its own; it accepts the props of the element or primitive it wraps.
ContextMenuRadioItem
ContextMenuRadioItem adds no props of its own; it accepts the props of the element or primitive it wraps.
ContextMenuSeparator
ContextMenuSeparator adds no props of its own; it accepts the props of the element or primitive it wraps.
ContextMenuShortcut
ContextMenuShortcut adds no props of its own; it accepts the props of the element or primitive it wraps.
ContextMenuSub
ContextMenuSub adds no props of its own; it accepts the props of the element or primitive it wraps.
ContextMenuSubContent
ContextMenuSubContent adds no props of its own; it accepts the props of the element or primitive it wraps.
ContextMenuSubTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | —No default documented | Indents the trigger to line up with items that have a leading icon or check. |
ContextMenuTrigger
ContextMenuTrigger adds no props of its own; it accepts the props of the element or primitive it wraps.
ContextMenu shows a menu of actions when the user right-clicks an area. Use it for actions on a specific item or region. For a menu opened by a button, use DropdownMenu.
<script setup lang="ts">
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from '@/components/context-menu';
</script>
<template>
<ContextMenu>
<ContextMenuTrigger
class="nx:flex nx:h-[96px] nx:w-full nx:max-w-md nx:select-none nx:items-center nx:justify-center nx:rounded-md nx:border-default nx:border-dashed nx:border-border-default nx:typography-body-default nx:text-muted-foreground"
>Right click here</ContextMenuTrigger
>
<ContextMenuContent>
<ContextMenuItem>Back</ContextMenuItem>
<ContextMenuItem>Forward</ContextMenuItem>
<ContextMenuItem>Reload</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</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/context-menu/ContextMenuCheckboxItem.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { ContextMenuCheckboxItem, type ContextMenuCheckboxItemEmits, type ContextMenuCheckboxItemProps as RekaContextMenuCheckboxItemProps, ContextMenuItemIndicator, 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 ContextMenuCheckboxItemProps extends RekaContextMenuCheckboxItemProps { class?: HTMLAttributes['class']; } const props = defineProps<ContextMenuCheckboxItemProps>(); const emits = defineEmits<ContextMenuCheckboxItemEmits>(); const delegated = reactiveOmit(props, 'class'); const forwarded = useForwardPropsEmits(delegated, emits); </script> <template> <ContextMenuCheckboxItem v-bind="forwarded" data-slot="context-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" > <ContextMenuItemIndicator force-mount data-slot="context-menu-checkbox-indicator" > <IconCheck data-slot="context-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' ) " /> </ContextMenuItemIndicator> </span> <slot /> </ContextMenuCheckboxItem> </template>components/context-menu/ContextMenuContent.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { ContextMenuContent, type ContextMenuContentEmits, type ContextMenuContentProps as RekaContextMenuContentProps, ContextMenuPortal, useForwardPropsEmits, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; import { overlayFloatingTransitionClassName, popoverSurfaceClassName, } from '../overlay-layout/overlay-layout'; export interface ContextMenuContentProps extends RekaContextMenuContentProps { class?: HTMLAttributes['class']; } defineOptions({ inheritAttrs: false }); const props = defineProps<ContextMenuContentProps>(); const emits = defineEmits<ContextMenuContentEmits>(); const delegated = reactiveOmit(props, 'class'); const forwarded = useForwardPropsEmits(delegated, emits); </script> <template> <ContextMenuPortal> <ContextMenuContent v-bind="{ ...$attrs, ...forwarded }" data-slot="context-menu-content" :class=" cn( 'nx:z-popover nx:max-h-(--reka-context-menu-content-available-height)', 'nx:min-w-32 nx:overflow-x-hidden nx:overflow-y-auto', popoverSurfaceClassName, 'nx:p-1', overlayFloatingTransitionClassName, props.class ) " > <slot /> </ContextMenuContent> </ContextMenuPortal> </template>components/context-menu/ContextMenuItem.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { ContextMenuItem, type ContextMenuItemEmits, type ContextMenuItemProps as RekaContextMenuItemProps, useForwardPropsEmits, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; import { type ContextMenuItemVariants, contextMenuItemVariants } from '.'; export interface ContextMenuItemProps extends RekaContextMenuItemProps { variant?: ContextMenuItemVariants['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<ContextMenuItemProps>(), { variant: 'default', }); const emits = defineEmits<ContextMenuItemEmits>(); const delegated = reactiveOmit(props, 'class', 'inset', 'variant'); const forwarded = useForwardPropsEmits(delegated, emits); </script> <template> <ContextMenuItem v-bind="forwarded" data-slot="context-menu-item" :data-inset="inset || undefined" :data-variant="variant" :class=" cn(contextMenuItemVariants({ variant }), inset && 'nx:pl-8', props.class) " > <slot /> </ContextMenuItem> </template>components/context-menu/ContextMenuLabel.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { ContextMenuLabel, type ContextMenuLabelProps as RekaContextMenuLabelProps, useForwardProps, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; export interface ContextMenuLabelProps extends RekaContextMenuLabelProps { /** * Indents the label to line up with items that have a leading icon or check. */ inset?: boolean; class?: HTMLAttributes['class']; } const props = defineProps<ContextMenuLabelProps>(); const delegated = reactiveOmit(props, 'class', 'inset'); const forwarded = useForwardProps(delegated); </script> <template> <ContextMenuLabel v-bind="forwarded" data-slot="context-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 /> </ContextMenuLabel> </template>components/context-menu/ContextMenuRadioItem.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { ContextMenuItemIndicator, ContextMenuRadioItem, type ContextMenuRadioItemEmits, type ContextMenuRadioItemProps as RekaContextMenuRadioItemProps, 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 ContextMenuRadioItemProps extends RekaContextMenuRadioItemProps { class?: HTMLAttributes['class']; } const props = defineProps<ContextMenuRadioItemProps>(); const emits = defineEmits<ContextMenuRadioItemEmits>(); const delegated = reactiveOmit(props, 'class'); const forwarded = useForwardPropsEmits(delegated, emits); </script> <template> <ContextMenuRadioItem v-bind="forwarded" data-slot="context-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" > <ContextMenuItemIndicator force-mount data-slot="context-menu-radio-indicator" > <IconCircleFilled data-slot="context-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' ) " /> </ContextMenuItemIndicator> </span> <slot /> </ContextMenuRadioItem> </template>components/context-menu/ContextMenuSeparator.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { ContextMenuSeparator, type ContextMenuSeparatorProps as RekaContextMenuSeparatorProps, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; export interface ContextMenuSeparatorProps extends RekaContextMenuSeparatorProps { class?: HTMLAttributes['class']; } const props = defineProps<ContextMenuSeparatorProps>(); const delegated = reactiveOmit(props, 'class'); </script> <template> <ContextMenuSeparator v-bind="delegated" data-slot="context-menu-separator" :class=" cn('nx:-mx-1 nx:my-1 nx:h-px nx:bg-border-default-alpha', props.class) " /> </template>components/context-menu/ContextMenuShortcut.vue <script setup lang="ts"> import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; export interface ContextMenuShortcutProps { class?: HTMLAttributes['class']; } const props = defineProps<ContextMenuShortcutProps>(); </script> <template> <span data-slot="context-menu-shortcut" :class=" cn( 'nx:ml-auto nx:typography-shortcut nx:text-muted-foreground', props.class ) " > <slot /> </span> </template>components/context-menu/ContextMenuSubContent.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { ContextMenuSubContent, type ContextMenuSubContentEmits, type ContextMenuSubContentProps as RekaContextMenuSubContentProps, useForwardPropsEmits, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; import { overlayFloatingTransitionClassName, popoverSurfaceClassName, } from '../overlay-layout/overlay-layout'; export interface ContextMenuSubContentProps extends RekaContextMenuSubContentProps { class?: HTMLAttributes['class']; } const props = defineProps<ContextMenuSubContentProps>(); const emits = defineEmits<ContextMenuSubContentEmits>(); const delegated = reactiveOmit(props, 'class'); const forwarded = useForwardPropsEmits(delegated, emits); </script> <template> <ContextMenuSubContent v-bind="forwarded" data-slot="context-menu-sub-content" :class=" cn( 'nx:z-popover nx:min-w-32 nx:overflow-hidden', popoverSurfaceClassName, 'nx:p-1', overlayFloatingTransitionClassName, props.class ) " > <slot /> </ContextMenuSubContent> </template>components/context-menu/ContextMenuSubTrigger.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { ContextMenuSubTrigger, type ContextMenuSubTriggerProps as RekaContextMenuSubTriggerProps, useForwardProps, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { IconChevronRight } from '../../lib/icons'; import { cn } from '../../lib/utils'; export interface ContextMenuSubTriggerProps extends RekaContextMenuSubTriggerProps { /** * Indents the trigger to line up with items that have a leading icon or check. */ inset?: boolean; class?: HTMLAttributes['class']; } const props = defineProps<ContextMenuSubTriggerProps>(); const delegated = reactiveOmit(props, 'class', 'inset'); const forwarded = useForwardProps(delegated); </script> <template> <ContextMenuSubTrigger v-bind="forwarded" data-slot="context-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" /> </ContextMenuSubTrigger> </template>components/context-menu/ContextMenuTrigger.vue <script setup lang="ts"> import { ContextMenuTrigger, type ContextMenuTriggerProps as RekaContextMenuTriggerProps, } from 'reka-ui'; export type ContextMenuTriggerProps = RekaContextMenuTriggerProps; const props = defineProps<ContextMenuTriggerProps>(); </script> <template> <ContextMenuTrigger v-bind="props" data-slot="context-menu-trigger"> <slot /> </ContextMenuTrigger> </template>components/context-menu/index.ts import { cva, type VariantProps } from 'class-variance-authority'; export { default as ContextMenuCheckboxItem, type ContextMenuCheckboxItemProps, } from './ContextMenuCheckboxItem.vue'; export { default as ContextMenuContent, type ContextMenuContentProps, } from './ContextMenuContent.vue'; export { default as ContextMenuItem, type ContextMenuItemProps, } from './ContextMenuItem.vue'; export { default as ContextMenuLabel, type ContextMenuLabelProps, } from './ContextMenuLabel.vue'; export { default as ContextMenuRadioItem, type ContextMenuRadioItemProps, } from './ContextMenuRadioItem.vue'; export { default as ContextMenuSeparator, type ContextMenuSeparatorProps, } from './ContextMenuSeparator.vue'; export { default as ContextMenuShortcut, type ContextMenuShortcutProps, } from './ContextMenuShortcut.vue'; export { default as ContextMenuSubContent, type ContextMenuSubContentProps, } from './ContextMenuSubContent.vue'; export { default as ContextMenuSubTrigger, type ContextMenuSubTriggerProps, } from './ContextMenuSubTrigger.vue'; export { default as ContextMenuTrigger, type ContextMenuTriggerProps, } from './ContextMenuTrigger.vue'; export { ContextMenuRoot as ContextMenu, ContextMenuGroup, type ContextMenuGroupProps, ContextMenuPortal, type ContextMenuPortalProps, type ContextMenuRootProps as ContextMenuProps, ContextMenuRadioGroup, type ContextMenuRadioGroupProps, ContextMenuSub, type ContextMenuSubProps, } from 'reka-ui'; export const contextMenuItemVariants = 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 ContextMenuItemVariants = VariantProps< typeof contextMenuItemVariants >;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 { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger } from '@/components/context-menu';
Examples
With labels and separators
Split items into sections with ContextMenuLabel headings and ContextMenuSeparator dividers.
<script setup lang="ts">
import {
ContextMenu,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuTrigger,
} from '@/components/context-menu';
</script>
<template>
<ContextMenu>
<ContextMenuTrigger
class="nx:flex nx:h-[96px] nx:w-full nx:max-w-md nx:select-none nx:items-center nx:justify-center nx:rounded-md nx:border-default nx:border-dashed nx:border-border-default nx:typography-body-default nx:text-muted-foreground"
>Right click here</ContextMenuTrigger
>
<ContextMenuContent class="nx:w-56">
<ContextMenuLabel>Actions</ContextMenuLabel>
<ContextMenuSeparator />
<ContextMenuGroup>
<ContextMenuItem
>Back<ContextMenuShortcut>⌘[</ContextMenuShortcut></ContextMenuItem
>
<ContextMenuItem
>Forward<ContextMenuShortcut>⌘]</ContextMenuShortcut></ContextMenuItem
>
<ContextMenuItem
>Reload<ContextMenuShortcut>⌘R</ContextMenuShortcut></ContextMenuItem
>
</ContextMenuGroup>
<ContextMenuSeparator />
<ContextMenuItem
>View Source<ContextMenuShortcut
>⌘U</ContextMenuShortcut
></ContextMenuItem
>
</ContextMenuContent>
</ContextMenu>
</template>
With checkbox items
Use ContextMenuCheckboxItem for settings that toggle on and off.
<script setup lang="ts">
import { ref } from 'vue';
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuTrigger,
} from '@/components/context-menu';
const showBookmarks = ref(true);
const showFullUrls = ref(false);
</script>
<template>
<ContextMenu>
<ContextMenuTrigger
class="nx:flex nx:h-[96px] nx:w-full nx:max-w-md nx:select-none nx:items-center nx:justify-center nx:rounded-md nx:border-default nx:border-dashed nx:border-border-default nx:typography-body-default nx:text-muted-foreground"
>Right click here</ContextMenuTrigger
>
<ContextMenuContent class="nx:w-56">
<ContextMenuLabel>Appearance</ContextMenuLabel>
<ContextMenuSeparator />
<ContextMenuCheckboxItem v-model="showBookmarks"
>Show Bookmarks</ContextMenuCheckboxItem
>
<ContextMenuCheckboxItem v-model="showFullUrls"
>Show Full URLs</ContextMenuCheckboxItem
>
</ContextMenuContent>
</ContextMenu>
</template>
With radio items
Use ContextMenuRadioItem inside a ContextMenuRadioGroup to pick one option.
<script setup lang="ts">
import { ref } from 'vue';
import {
ContextMenu,
ContextMenuContent,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from '@/components/context-menu';
const person = ref('pedro');
</script>
<template>
<ContextMenu>
<ContextMenuTrigger
class="nx:flex nx:h-[96px] nx:w-full nx:max-w-md nx:select-none nx:items-center nx:justify-center nx:rounded-md nx:border-default nx:border-dashed nx:border-border-default nx:typography-body-default nx:text-muted-foreground"
>Right click here</ContextMenuTrigger
>
<ContextMenuContent class="nx:w-56">
<ContextMenuLabel>People</ContextMenuLabel>
<ContextMenuSeparator />
<ContextMenuRadioGroup v-model="person">
<ContextMenuRadioItem value="pedro">Pedro Duarte</ContextMenuRadioItem>
<ContextMenuRadioItem value="colm">Colm Tuite</ContextMenuRadioItem>
</ContextMenuRadioGroup>
</ContextMenuContent>
</ContextMenu>
</template>
With sub menu
Nest a ContextMenuSub to open a second menu from an item.
<script setup lang="ts">
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from '@/components/context-menu';
</script>
<template>
<ContextMenu>
<ContextMenuTrigger
class="nx:flex nx:h-[96px] nx:w-full nx:max-w-md nx:select-none nx:items-center nx:justify-center nx:rounded-md nx:border-default nx:border-dashed nx:border-border-default nx:typography-body-default nx:text-muted-foreground"
>Right click here</ContextMenuTrigger
>
<ContextMenuContent class="nx:w-56">
<ContextMenuItem
>Back<ContextMenuShortcut>⌘[</ContextMenuShortcut></ContextMenuItem
>
<ContextMenuItem
>Forward<ContextMenuShortcut>⌘]</ContextMenuShortcut></ContextMenuItem
>
<ContextMenuSeparator />
<ContextMenuSub>
<ContextMenuSubTrigger>More Tools</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem>Save Page As...</ContextMenuItem>
<ContextMenuItem>Create Shortcut...</ContextMenuItem>
<ContextMenuItem>Name Window...</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem>Developer Tools</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuItem
>Reload<ContextMenuShortcut>⌘R</ContextMenuShortcut></ContextMenuItem
>
</ContextMenuContent>
</ContextMenu>
</template>
With destructive item
Set variant="destructive" on an item for actions like Delete.
<script setup lang="ts">
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from '@/components/context-menu';
</script>
<template>
<ContextMenu>
<ContextMenuTrigger
class="nx:flex nx:h-[96px] nx:w-full nx:max-w-md nx:select-none nx:items-center nx:justify-center nx:rounded-md nx:border-default nx:border-dashed nx:border-border-default nx:typography-body-default nx:text-muted-foreground"
>Right click here</ContextMenuTrigger
>
<ContextMenuContent>
<ContextMenuItem>Edit</ContextMenuItem>
<ContextMenuItem>Duplicate</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem variant="destructive">Delete</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</template>
Props
ContextMenuCheckboxItem
ContextMenuCheckboxItem adds no props of its own; it accepts the props of the element or primitive it wraps.
ContextMenuContent
ContextMenuContent adds no props of its own; it accepts the props of the element or primitive it wraps.
ContextMenuItem
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | —No default documented | Indents the item to line up with items that have a leading icon or check. |
variant | "default" | "destructive" | null | "default" | —No description |
ContextMenuLabel
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | —No default documented | Indents the label to line up with items that have a leading icon or check. |
ContextMenuRadioItem
ContextMenuRadioItem adds no props of its own; it accepts the props of the element or primitive it wraps.
ContextMenuSeparator
ContextMenuSeparator adds no props of its own; it accepts the props of the element or primitive it wraps.
ContextMenuShortcut
ContextMenuShortcut adds no props of its own; it accepts the props of the element or primitive it wraps.
ContextMenuSubContent
ContextMenuSubContent adds no props of its own; it accepts the props of the element or primitive it wraps.
ContextMenuSubTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
inset | boolean | —No default documented | Indents the trigger to line up with items that have a leading icon or check. |
ContextMenuTrigger
ContextMenuTrigger adds no props of its own; it accepts the props of the element or primitive it wraps.