Components
A menu to display actions when right-clicking on an element.

Usage

Use anything you like in the default slot of the ContextMenu, and right-click on it to display the menu.

Items

Use the items prop as an array of objects with the following properties:

  • label?: string
  • icon?: string
  • avatar?: AvatarProps
  • kbds?: string[] | KbdProps[]
  • type?: "link" | "label" | "separator"
  • disabled?: boolean
  • class?: any
  • slot?: string
  • select?(e: Event): void

You can also pass any property from the Link component such as to, target, etc.

Right click here
<script setup lang="ts">
const items = ref([
  [
    {
      label: 'Appearance',
      children: [
        {
          label: 'System',
          icon: 'i-heroicons-computer-desktop'
        },
        {
          label: 'Light',
          icon: 'i-heroicons-sun'
        },
        {
          label: 'Dark',
          icon: 'i-heroicons-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" class="w-48">
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-gray-300 dark:border-gray-700 text-sm aspect-video w-72"
    >
      Right click here
    </div>
  </UContextMenu>
</template>
You can pass an array of arrays to the items prop to create separated groups of items.
Each item can take a 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.

Size

Use the size prop to change the size of the ContextMenu.

Right click here
<script setup lang="ts">
const items = ref([
  {
    label: 'System',
    icon: 'i-heroicons-computer-desktop'
  },
  {
    label: 'Light',
    icon: 'i-heroicons-sun'
  },
  {
    label: 'Dark',
    icon: 'i-heroicons-moon'
  }
])
</script>

<template>
  <UContextMenu size="xl" :items="items" class="w-48">
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-gray-300 dark:border-gray-700 text-sm aspect-video w-72"
    >
      Right click here
    </div>
  </UContextMenu>
</template>

Disabled

Use the disabled prop to disable the ContextMenu.

Right click here
<script setup lang="ts">
const items = ref([
  {
    label: 'System',
    icon: 'i-heroicons-computer-desktop'
  },
  {
    label: 'Light',
    icon: 'i-heroicons-sun'
  },
  {
    label: 'Dark',
    icon: 'i-heroicons-moon'
  }
])
</script>

<template>
  <UContextMenu disabled :items="items" class="w-48">
    <div
      class="flex items-center justify-center rounded-md border border-dashed border-gray-300 dark:border-gray-700 text-sm aspect-video w-72"
    >
      Right click here
    </div>
  </UContextMenu>
</template>

Examples

With custom slot

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
Right click here
<script setup lang="ts">
const loading = ref(true)

const items = [{
  label: 'Refresh the Page',
  slot: 'refresh'
}, {
  label: 'Clear Cookies and Refresh'
}, {
  label: 'Clear Cache and Refresh'
}]
</script>

<template>
  <UContextMenu :items="items" class="w-48">
    <div class="flex items-center justify-center rounded-md border border-dashed border-gray-300 dark:border-gray-700 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-heroicons-arrow-path-20-solid" class="shrink-0 size-5 text-primary-500 dark:text-primary-400 animate-spin" />
    </template>
  </UContextMenu>
</template>
You can also use the #item, #item-leading, #item-label and #item-trailing slots to customize all items.

Extract shortcuts

When you have some items with kbds property (displaying some Kbd), you can easily make them work with the defineShortcuts composable.

Inside the defineShortcuts composable, there is an extractShortcuts utility that will extract the shortcuts recursively from the items and return an object that you can pass to defineShortcuts. It will automatically call the select function of the item when the shortcut is pressed.

<script setup lang="ts">
const items = [
  [{
    label: 'Show Sidebar',
    kbds: ['meta', 'S'],
    select() {
      console.log('Show Sidebar clicked')
    }
  }, {
    label: 'Show Toolbar',
    kbds: ['shift', 'meta', 'D'],
    select() {
      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'],
      select() {
        console.log('View Source clicked')
      }
    }, {
      label: 'Developer Tools',
      kbds: ['option', 'meta', 'I'],
      select() {
        console.log('Developer Tools clicked')
      }
    }], [{
      label: 'Inspect Elements',
      kbds: ['option', 'meta', 'C'],
      select() {
        console.log('Inspect Elements clicked')
      }
    }], [{
      label: 'JavaScript Console',
      kbds: ['option', 'meta', 'J'],
      select() {
        console.log('JavaScript Console clicked')
      }
    }]]
  }]
]

defineShortcuts(extractShortcuts(items))
</script>
In this example, S, ⇧ D, ⌥ U, ⌥ I, ⌥ C and ⌥ J would trigger the select function of the corresponding item.

API

Props

Prop Default Type
size

md

"md" | "xs" | "sm" | "lg" | "xl"

items

ContextMenuItem[] | ContextMenuItem[][]

content

Omit<ContextMenuContentProps, "asChild" | "as" | "forceMount">

The content of the menu.

portal

true

boolean

Render the menu in a portal.

modal

true

boolean

The modality of the dropdown menu.

When set to true, interaction with outside elements will be disabled and only menu content will be visible to screen readers.

disabled

boolean

When true, the context menu would not open when right-clicking.

Note that this will also restore the native context menu.

ui

Partial<{ content: string; group: string; label: string; separator: string; item: string; itemLeadingIcon: string; itemLeadingAvatar: string; itemLeadingAvatarSize: string; itemTrailing: string; itemTrailingIcon: string; itemTrailingKbds: string; itemTrailingKbdsSize: string; itemLabel: string; itemLabelExternalIcon...

Slots

Slot Type
default

{}

item

{ item: ContextMenuItem; active?: boolean | undefined; index: number; }

item-leading

{ item: ContextMenuItem; active?: boolean | undefined; index: number; }

item-label

{ item: ContextMenuItem; active?: boolean | undefined; index: number; }

item-trailing

{ item: ContextMenuItem; active?: boolean | undefined; index: number; }

Emits

Event Type
update:open

[payload: boolean]

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    contextMenu: {
      slots: {
        content: 'min-w-32 bg-white dark:bg-gray-900 shadow-lg rounded-md ring ring-gray-200 dark:ring-gray-800 divide-y divide-gray-200 dark:divide-gray-800 overflow-y-auto scroll-py-1 data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in]',
        group: 'p-1 isolate',
        label: 'w-full flex items-center font-semibold text-gray-900 dark:text-white',
        separator: '-mx-1 my-1 h-px bg-gray-200 dark:bg-gray-800',
        item: 'group relative w-full flex items-center 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',
        itemTrailingIcon: 'shrink-0',
        itemTrailingKbds: 'hidden lg:inline-flex items-center shrink-0',
        itemTrailingKbdsSize: '',
        itemLabel: 'truncate',
        itemLabelExternalIcon: 'size-3 align-top text-gray-400 dark:text-gray-500'
      },
      variants: {
        active: {
          true: {
            item: 'text-gray-900 dark:text-white before:bg-gray-100 dark:before:bg-gray-800',
            itemLeadingIcon: 'text-gray-700 dark:text-gray-200'
          },
          false: {
            item: [
              'text-gray-700 dark:text-gray-200 data-highlighted:text-gray-900 dark:data-highlighted:text-white data-[state=open]:text-gray-900 dark:data-[state=open]:text-white data-highlighted:before:bg-gray-50 dark:data-highlighted:before:bg-gray-800/50 data-[state=open]:before:bg-gray-50 dark:data-[state=open]:before:bg-gray-800/50',
              'transition-colors before:transition-colors'
            ],
            itemLeadingIcon: [
              'text-gray-400 dark:text-gray-500 group-data-highlighted:text-gray-700 dark:group-data-highlighted:text-gray-200 group-data-[state=open]:text-gray-700 dark:group-data-[state=open]:text-gray-200',
              'transition-colors'
            ]
          }
        },
        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'
          }
        }
      },
      defaultVariants: {
        size: 'md'
      }
    }
  }
})