Use anything you like in the default slot of the ContextMenu, and right-click on it to display the menu.
<script setup lang="ts">
const items = ref([
[
{
label: 'Appearance',
children: [
{
label: 'System',
icon: 'i-lucide-monitor'
},
{
label: 'Light',
icon: 'i-lucide-sun'
},
{
label: 'Dark',
icon: 'i-lucide-moon'
}
]
}
],
[
{
label: 'Show Sidebar',
kbds: ['meta', 's']
},
{
label: 'Show Toolbar',
kbds: ['shift', 'meta', 'd']
},
{
label: 'Collapse Pinned Tabs',
disabled: true
}
],
[
{
label: 'Refresh the Page'
},
{
label: 'Clear Cookies and Refresh'
},
{
label: 'Clear Cache and Refresh'
},
{
type: 'separator'
},
{
label: 'Developer',
children: [
[
{
label: 'View Source',
kbds: ['meta', 'shift', 'u']
},
{
label: 'Developer Tools',
kbds: ['option', 'meta', 'i']
},
{
label: 'Inspect Elements',
kbds: ['option', 'meta', 'c']
}
],
[
{
label: 'JavaScript Console',
kbds: ['option', 'meta', 'j']
}
]
]
}
]
])
</script>
<template>
<UContextMenu :items="items">
<div
class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72"
>
Right click here
</div>
</UContextMenu>
</template>
Use the items prop as an array of objects with the following properties:
label?: stringicon?: stringavatar?: AvatarPropskbds?: string[] | KbdProps[]type?: "link" | "label" | "separator" | "checkbox"color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"checked?: booleandisabled?: booleanslot?: stringonSelect?: (e: Event) => voidonUpdateChecked?: (checked: boolean) => voidchildren?: ContextMenuItem[] | ContextMenuItem[][]class?: anyui?: { item?: ClassNameValue, label?: ClassNameValue, separator?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLabel?: ClassNameValue, itemLabelExternalIcon?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue, itemTrailingKbds?: ClassNameValue, itemTrailingKbdsSize?: ClassNameValue }You can pass any property from the Link component such as to, target, etc.
<script setup lang="ts">
import type { ContextMenuItem } from '@nuxt/ui'
const items = ref<ContextMenuItem[][]>([
[
{
label: 'Appearance',
children: [
{
label: 'System',
icon: 'i-lucide-monitor'
},
{
label: 'Light',
icon: 'i-lucide-sun'
},
{
label: 'Dark',
icon: 'i-lucide-moon'
}
]
}
],
[
{
label: 'Show Sidebar',
kbds: ['meta', 's']
},
{
label: 'Show Toolbar',
kbds: ['shift', 'meta', 'd']
},
{
label: 'Collapse Pinned Tabs',
disabled: true
}
],
[
{
label: 'Refresh the Page'
},
{
label: 'Clear Cookies and Refresh'
},
{
label: 'Clear Cache and Refresh'
},
{
type: 'separator'
},
{
label: 'Developer',
children: [
[
{
label: 'View Source',
kbds: ['meta', 'shift', 'u']
},
{
label: 'Developer Tools',
kbds: ['option', 'meta', 'i']
},
{
label: 'Inspect Elements',
kbds: ['option', 'meta', 'c']
}
],
[
{
label: 'JavaScript Console',
kbds: ['option', 'meta', 'j']
}
]
]
}
]
])
</script>
<template>
<UContextMenu
:items="items"
:ui="{
content: 'w-48'
}"
>
<div
class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72"
>
Right click here
</div>
</UContextMenu>
</template>
items prop to create separated groups of items.children array of objects with the same properties as the items prop to create a nested menu which can be controlled using the open, defaultOpen and content properties.Use the size prop to change the size of the ContextMenu.
<script setup lang="ts">
import type { ContextMenuItem } from '@nuxt/ui'
const items = ref<ContextMenuItem[]>([
{
label: 'System',
icon: 'i-lucide-monitor'
},
{
label: 'Light',
icon: 'i-lucide-sun'
},
{
label: 'Dark',
icon: 'i-lucide-moon'
}
])
</script>
<template>
<UContextMenu
size="xl"
:items="items"
:ui="{
content: 'w-48'
}"
>
<div
class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72"
>
Right click here
</div>
</UContextMenu>
</template>
Use the modal prop to control whether the ContextMenu blocks interaction with outside content. Defaults to true.
<script setup lang="ts">
import type { ContextMenuItem } from '@nuxt/ui'
const items = ref<ContextMenuItem[]>([
{
label: 'System',
icon: 'i-lucide-monitor'
},
{
label: 'Light',
icon: 'i-lucide-sun'
},
{
label: 'Dark',
icon: 'i-lucide-moon'
}
])
</script>
<template>
<UContextMenu
:modal="false"
:items="items"
:ui="{
content: 'w-48'
}"
>
<div
class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72"
>
Right click here
</div>
</UContextMenu>
</template>
Use the disabled prop to disable the ContextMenu.
<script setup lang="ts">
import type { ContextMenuItem } from '@nuxt/ui'
const items = ref<ContextMenuItem[]>([
{
label: 'System',
icon: 'i-lucide-monitor'
},
{
label: 'Light',
icon: 'i-lucide-sun'
},
{
label: 'Dark',
icon: 'i-lucide-moon'
}
])
</script>
<template>
<UContextMenu
disabled
:items="items"
:ui="{
content: 'w-48'
}"
>
<div
class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72"
>
Right click here
</div>
</UContextMenu>
</template>
You can use the type property with checkbox and use the checked / onUpdateChecked properties to control the checked state of the item.
<script setup lang="ts">
import type { ContextMenuItem } from '@nuxt/ui'
const showSidebar = ref(true)
const showToolbar = ref(false)
const items = computed<ContextMenuItem[]>(() => [{
label: 'View',
type: 'label' as const
}, {
type: 'separator' as const
}, {
label: 'Show Sidebar',
type: 'checkbox' as const,
checked: showSidebar.value,
onUpdateChecked(checked: boolean) {
showSidebar.value = checked
},
onSelect(e: Event) {
e.preventDefault()
}
}, {
label: 'Show Toolbar',
type: 'checkbox' as const,
checked: showToolbar.value,
onUpdateChecked(checked: boolean) {
showToolbar.value = checked
}
}, {
label: 'Collapse Pinned Tabs',
type: 'checkbox' as const,
disabled: true
}])
</script>
<template>
<UContextMenu :items="items" :ui="{ content: 'w-48' }">
<div class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72">
Right click here
</div>
</UContextMenu>
</template>
checked state of items, it's recommended to wrap your items array inside a computed.You can use the color property to highlight certain items with a color.
<script setup lang="ts">
import type { ContextMenuItem } from '@nuxt/ui'
const items: ContextMenuItem[][] = [
[
{
label: 'View',
icon: 'i-lucide-eye'
},
{
label: 'Copy',
icon: 'i-lucide-copy'
},
{
label: 'Edit',
icon: 'i-lucide-pencil'
}
],
[
{
label: 'Delete',
color: 'error' as const,
icon: 'i-lucide-trash'
}
]
]
</script>
<template>
<UContextMenu :items="items" :ui="{ content: 'w-48' }">
<div class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72">
Right click here
</div>
</UContextMenu>
</template>
Use the slot property to customize a specific item.
You will have access to the following slots:
#{{ item.slot }}#{{ item.slot }}-leading#{{ item.slot }}-label#{{ item.slot }}-trailing<script setup lang="ts">
import type { ContextMenuItem } from '@nuxt/ui'
const loading = ref(true)
const items = [
{
label: 'Refresh the Page',
slot: 'refresh' as const
},
{
label: 'Clear Cookies and Refresh'
},
{
label: 'Clear Cache and Refresh'
}
] satisfies ContextMenuItem[]
</script>
<template>
<UContextMenu :items="items" :ui="{ content: 'w-48' }">
<div class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72">
Right click here
</div>
<template #refresh-label>
{{ loading ? 'Refreshing...' : 'Refresh the Page' }}
</template>
<template #refresh-trailing>
<UIcon v-if="loading" name="i-lucide-loader-circle" class="shrink-0 size-5 text-primary animate-spin" />
</template>
</UContextMenu>
</template>
#item, #item-leading, #item-label and #item-trailing slots to customize all items.Use the extractShortcuts utility to automatically define shortcuts from menu items with a kbds property. It recursively extracts shortcuts and returns an object compatible with defineShortcuts.
<script setup lang="ts">
const items = [
[{
label: 'Show Sidebar',
kbds: ['meta', 'S'],
onSelect() {
console.log('Show Sidebar clicked')
}
}, {
label: 'Show Toolbar',
kbds: ['shift', 'meta', 'D'],
onSelect() {
console.log('Show Toolbar clicked')
}
}, {
label: 'Collapse Pinned Tabs',
disabled: true
}], [{
label: 'Refresh the Page'
}, {
label: 'Clear Cookies and Refresh'
}, {
label: 'Clear Cache and Refresh'
}, {
type: 'separator' as const
}, {
label: 'Developer',
children: [[{
label: 'View Source',
kbds: ['option', 'meta', 'U'],
onSelect() {
console.log('View Source clicked')
}
}, {
label: 'Developer Tools',
kbds: ['option', 'meta', 'I'],
onSelect() {
console.log('Developer Tools clicked')
}
}], [{
label: 'Inspect Elements',
kbds: ['option', 'meta', 'C'],
onSelect() {
console.log('Inspect Elements clicked')
}
}], [{
label: 'JavaScript Console',
kbds: ['option', 'meta', 'J'],
onSelect() {
console.log('JavaScript Console clicked')
}
}]]
}]
]
defineShortcuts(extractShortcuts(items))
</script>
select function of the corresponding item.| Prop | Default | Type |
|---|---|---|
size | 'md' | "sm" | "md" | "xs" | "lg" | "xl" |
items | ContextMenuItem[] | ContextMenuItem[][]
| |
checkedIcon | appConfig.ui.icons.check | anyThe icon displayed when an item is checked. |
loadingIcon | appConfig.ui.icons.loading | anyThe icon displayed when an item is loading. |
externalIcon | true | anyThe icon displayed when the item is an external link.
Set to |
content | ContextMenuContentProps & Partial<EmitsToProps<MenuContentEmits>>The content of the menu.
| |
portal | true | string | false | true | HTMLElementRender the menu in a portal. |
labelKey | 'label' | string | numberThe key used to get the label from the item. |
descriptionKey | 'description' | string | numberThe key used to get the description from the item. |
disabled | boolean | |
modal | true | boolean The modality of the dropdown menu. When set to |
pressOpenDelay | 700 | numberThe duration from when the trigger is pressed until the menu opens. |
ui | { content?: ClassNameValue; viewport?: ClassNameValue; group?: ClassNameValue; label?: ClassNameValue; separator?: ClassNameValue; item?: ClassNameValue; itemLeadingIcon?: ClassNameValue; itemLeadingAvatar?: ClassNameValue; itemLeadingAvatarSize?: ClassNameValue; itemTrailing?: ClassNameValue; itemTrailingIcon?: ClassNameValue; itemTrailingKbds?: ClassNameValue; itemTrailingKbdsSize?: ClassNameValue; itemWrapper?: ClassNameValue; itemLabel?: ClassNameValue; itemDescription?: ClassNameValue; itemLabelExternalIcon?: ClassNameValue; } |
| Slot | Type |
|---|---|
default | {} |
item | { item: ContextMenuItem; active?: boolean | undefined; index: number; ui: object; } |
item-leading | { item: ContextMenuItem; active?: boolean | undefined; index: number; ui: object; } |
item-label | { item: ContextMenuItem; active?: boolean | undefined; index: number; } |
item-description | { item: ContextMenuItem; active?: boolean | undefined; index: number; } |
item-trailing | { item: ContextMenuItem; active?: boolean | undefined; index: number; ui: object; } |
content-top | { sub: boolean; } |
content-bottom | { sub: boolean; } |
| Event | Type |
|---|---|
update:open | [payload: boolean] |
export default defineAppConfig({
ui: {
contextMenu: {
slots: {
content: 'min-w-32 bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-context-menu-content-transform-origin) flex flex-col',
viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
group: 'p-1 isolate',
label: 'w-full flex items-center font-semibold text-highlighted',
separator: '-mx-1 my-1 h-px bg-border',
item: 'group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75',
itemLeadingIcon: 'shrink-0',
itemLeadingAvatar: 'shrink-0',
itemLeadingAvatarSize: '',
itemTrailing: 'ms-auto inline-flex gap-1.5 items-center',
itemTrailingIcon: 'shrink-0',
itemTrailingKbds: 'hidden lg:inline-flex items-center shrink-0',
itemTrailingKbdsSize: '',
itemWrapper: 'flex-1 flex flex-col text-start min-w-0',
itemLabel: 'truncate',
itemDescription: 'truncate text-muted',
itemLabelExternalIcon: 'inline-block size-3 align-top text-dimmed'
},
variants: {
color: {
primary: '',
secondary: '',
success: '',
info: '',
warning: '',
error: '',
neutral: ''
},
active: {
true: {
item: 'text-highlighted before:bg-elevated',
itemLeadingIcon: 'text-default'
},
false: {
item: [
'text-default data-highlighted:text-highlighted data-[state=open]:text-highlighted data-highlighted:before:bg-elevated/50 data-[state=open]:before:bg-elevated/50',
'transition-colors before:transition-colors'
],
itemLeadingIcon: [
'text-dimmed group-data-highlighted:text-default group-data-[state=open]:text-default',
'transition-colors'
]
}
},
loading: {
true: {
itemLeadingIcon: 'animate-spin'
}
},
size: {
xs: {
label: 'p-1 text-xs gap-1',
item: 'p-1 text-xs gap-1',
itemLeadingIcon: 'size-4',
itemLeadingAvatarSize: '3xs',
itemTrailingIcon: 'size-4',
itemTrailingKbds: 'gap-0.5',
itemTrailingKbdsSize: 'sm'
},
sm: {
label: 'p-1.5 text-xs gap-1.5',
item: 'p-1.5 text-xs gap-1.5',
itemLeadingIcon: 'size-4',
itemLeadingAvatarSize: '3xs',
itemTrailingIcon: 'size-4',
itemTrailingKbds: 'gap-0.5',
itemTrailingKbdsSize: 'sm'
},
md: {
label: 'p-1.5 text-sm gap-1.5',
item: 'p-1.5 text-sm gap-1.5',
itemLeadingIcon: 'size-5',
itemLeadingAvatarSize: '2xs',
itemTrailingIcon: 'size-5',
itemTrailingKbds: 'gap-0.5',
itemTrailingKbdsSize: 'md'
},
lg: {
label: 'p-2 text-sm gap-2',
item: 'p-2 text-sm gap-2',
itemLeadingIcon: 'size-5',
itemLeadingAvatarSize: '2xs',
itemTrailingIcon: 'size-5',
itemTrailingKbds: 'gap-1',
itemTrailingKbdsSize: 'md'
},
xl: {
label: 'p-2 text-base gap-2',
item: 'p-2 text-base gap-2',
itemLeadingIcon: 'size-6',
itemLeadingAvatarSize: 'xs',
itemTrailingIcon: 'size-6',
itemTrailingKbds: 'gap-1',
itemTrailingKbdsSize: 'lg'
}
}
},
compoundVariants: [
{
color: 'primary',
active: false,
class: {
item: 'text-primary data-highlighted:text-primary data-highlighted:before:bg-primary/10 data-[state=open]:before:bg-primary/10',
itemLeadingIcon: 'text-primary/75 group-data-highlighted:text-primary group-data-[state=open]:text-primary'
}
},
{
color: 'primary',
active: true,
class: {
item: 'text-primary before:bg-primary/10',
itemLeadingIcon: 'text-primary'
}
}
],
defaultVariants: {
size: 'md'
}
}
}
})
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
contextMenu: {
slots: {
content: 'min-w-32 bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-context-menu-content-transform-origin) flex flex-col',
viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
group: 'p-1 isolate',
label: 'w-full flex items-center font-semibold text-highlighted',
separator: '-mx-1 my-1 h-px bg-border',
item: 'group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75',
itemLeadingIcon: 'shrink-0',
itemLeadingAvatar: 'shrink-0',
itemLeadingAvatarSize: '',
itemTrailing: 'ms-auto inline-flex gap-1.5 items-center',
itemTrailingIcon: 'shrink-0',
itemTrailingKbds: 'hidden lg:inline-flex items-center shrink-0',
itemTrailingKbdsSize: '',
itemWrapper: 'flex-1 flex flex-col text-start min-w-0',
itemLabel: 'truncate',
itemDescription: 'truncate text-muted',
itemLabelExternalIcon: 'inline-block size-3 align-top text-dimmed'
},
variants: {
color: {
primary: '',
secondary: '',
success: '',
info: '',
warning: '',
error: '',
neutral: ''
},
active: {
true: {
item: 'text-highlighted before:bg-elevated',
itemLeadingIcon: 'text-default'
},
false: {
item: [
'text-default data-highlighted:text-highlighted data-[state=open]:text-highlighted data-highlighted:before:bg-elevated/50 data-[state=open]:before:bg-elevated/50',
'transition-colors before:transition-colors'
],
itemLeadingIcon: [
'text-dimmed group-data-highlighted:text-default group-data-[state=open]:text-default',
'transition-colors'
]
}
},
loading: {
true: {
itemLeadingIcon: 'animate-spin'
}
},
size: {
xs: {
label: 'p-1 text-xs gap-1',
item: 'p-1 text-xs gap-1',
itemLeadingIcon: 'size-4',
itemLeadingAvatarSize: '3xs',
itemTrailingIcon: 'size-4',
itemTrailingKbds: 'gap-0.5',
itemTrailingKbdsSize: 'sm'
},
sm: {
label: 'p-1.5 text-xs gap-1.5',
item: 'p-1.5 text-xs gap-1.5',
itemLeadingIcon: 'size-4',
itemLeadingAvatarSize: '3xs',
itemTrailingIcon: 'size-4',
itemTrailingKbds: 'gap-0.5',
itemTrailingKbdsSize: 'sm'
},
md: {
label: 'p-1.5 text-sm gap-1.5',
item: 'p-1.5 text-sm gap-1.5',
itemLeadingIcon: 'size-5',
itemLeadingAvatarSize: '2xs',
itemTrailingIcon: 'size-5',
itemTrailingKbds: 'gap-0.5',
itemTrailingKbdsSize: 'md'
},
lg: {
label: 'p-2 text-sm gap-2',
item: 'p-2 text-sm gap-2',
itemLeadingIcon: 'size-5',
itemLeadingAvatarSize: '2xs',
itemTrailingIcon: 'size-5',
itemTrailingKbds: 'gap-1',
itemTrailingKbdsSize: 'md'
},
xl: {
label: 'p-2 text-base gap-2',
item: 'p-2 text-base gap-2',
itemLeadingIcon: 'size-6',
itemLeadingAvatarSize: 'xs',
itemTrailingIcon: 'size-6',
itemTrailingKbds: 'gap-1',
itemTrailingKbdsSize: 'lg'
}
}
},
compoundVariants: [
{
color: 'primary',
active: false,
class: {
item: 'text-primary data-highlighted:text-primary data-highlighted:before:bg-primary/10 data-[state=open]:before:bg-primary/10',
itemLeadingIcon: 'text-primary/75 group-data-highlighted:text-primary group-data-[state=open]:text-primary'
}
},
{
color: 'primary',
active: true,
class: {
item: 'text-primary before:bg-primary/10',
itemLeadingIcon: 'text-primary'
}
}
],
defaultVariants: {
size: 'md'
}
}
}
})
]
})