Beta, expect API change, and bugs. Hit one? Tell us.

Skip to content

Context Menu

A right-click menu.

Right click here
<script setup lang="ts">
import { ContextMenu } from '@shardsui/vue/context-menu'
</script>

<template>
  <ContextMenu.Root>
    <ContextMenu.Trigger
      class="flex h-48 w-60 items-center justify-center rounded-md border border-dashed border-gray-300 text-sm font-normal text-gray-600 select-none"
    >
      Right click here
    </ContextMenu.Trigger>
    <ContextMenu.Portal>
      <ContextMenu.Positioner class="outline-hidden">
        <ContextMenu.Popup
          class="origin-(--transform-origin) rounded-md bg-gray-50 py-1 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-opacity duration-100 ease-out data-ending-style:opacity-0"
        >
          <ContextMenu.Item
            class="flex py-2 pr-8 pl-4 text-sm/4 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
            >Open</ContextMenu.Item
          >
          <ContextMenu.Item
            class="flex py-2 pr-8 pl-4 text-sm/4 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
            >Rename</ContextMenu.Item
          >
          <ContextMenu.Separator class="m-1 h-px bg-gray-200" />
          <ContextMenu.Item
            class="flex py-2 pr-8 pl-4 text-sm/4 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
            >Delete</ContextMenu.Item
          >
        </ContextMenu.Popup>
      </ContextMenu.Positioner>
    </ContextMenu.Portal>
  </ContextMenu.Root>
</template>

Anatomy

<script setup>
import { ContextMenu } from '@shardsui/vue/context-menu'
</script>

<template>
  <ContextMenu.Root>
    <ContextMenu.Trigger />
    <ContextMenu.Portal>
      <ContextMenu.Backdrop />
      <ContextMenu.Positioner>
        <ContextMenu.Popup>
          <ContextMenu.Arrow />
          <ContextMenu.Item />
          <ContextMenu.LinkItem />
          <ContextMenu.Separator />
          <ContextMenu.SubmenuRoot>
            <ContextMenu.SubmenuTrigger />
          </ContextMenu.SubmenuRoot>
          <ContextMenu.Group>
            <ContextMenu.GroupLabel />
          </ContextMenu.Group>
          <ContextMenu.RadioGroup>
            <ContextMenu.RadioItem>
              <ContextMenu.RadioItemIndicator />
            </ContextMenu.RadioItem>
          </ContextMenu.RadioGroup>
          <ContextMenu.CheckboxItem>
            <ContextMenu.CheckboxItemIndicator />
          </ContextMenu.CheckboxItem>
        </ContextMenu.Popup>
      </ContextMenu.Positioner>
    </ContextMenu.Portal>
  </ContextMenu.Root>
</template>

Most parts (Backdrop, Popup, Item, etc.) are re-exported from the Menu component, so they share the same props and data attributes. Positioner is the exception: its align, offset, arrowPadding and positionMethod defaults follow the pointer instead of a trigger element, and are documented below.

Usage guidelines

  • Use context menus as an enhancement: they may be undiscoverable on touch devices or with assistive technology, so always pair them with a visible control for the same actions.

Examples

The Menu page has more demos, and most of its patterns carry over to the context menu.

Using with Menu

This card exposes actions through a visible menu button and reuses them in the context menu for right-click and long-press users.

Station Hofplein

JPG, 2.4 MB

<script setup lang="ts">
import { ContextMenu } from '@shardsui/vue/context-menu'
import { Menu } from '@shardsui/vue/menu'

const actions = ['Preview', 'Download', 'Copy link', 'Rename']
</script>

<template>
  <div
    class="group relative w-64 overflow-hidden rounded-md bg-gray-50 text-left text-gray-900 outline-1 outline-gray-200 select-none"
  >
    <ContextMenu.Root>
      <ContextMenu.Trigger>
        <div class="h-32 w-full bg-linear-to-br from-gray-200 to-gray-400"></div>
        <div class="p-2">
          <p class="text-sm/5">Station Hofplein</p>
          <p class="text-xs/4 text-gray-500">JPG, 2.4 MB</p>
        </div>
      </ContextMenu.Trigger>
      <ContextMenu.Portal>
        <ContextMenu.Positioner class="outline-hidden">
          <ContextMenu.Popup
            class="origin-(--transform-origin) rounded-md bg-gray-50 py-1 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[transform,scale,opacity] duration-100 ease-out data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0"
          >
            <ContextMenu.Item
              v-for="action in actions"
              :key="action"
              class="flex py-2 pr-8 pl-4 text-sm/4 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
              >{{ action }}</ContextMenu.Item
            >
            <ContextMenu.Separator class="m-1 h-px bg-gray-200" />
            <ContextMenu.Item
              class="flex py-2 pr-8 pl-4 text-sm/4 text-red-700 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
              >Delete</ContextMenu.Item
            >
          </ContextMenu.Popup>
        </ContextMenu.Positioner>
      </ContextMenu.Portal>
    </ContextMenu.Root>

    <Menu.Root>
      <Menu.Trigger
        aria-label="Image actions"
        class="absolute top-2 right-2 flex size-8 items-center justify-center rounded-md bg-gray-50 text-gray-900 opacity-0 outline-1 outline-gray-200 select-none group-hover:opacity-100 hover:bg-gray-100 focus-visible:opacity-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 data-popup-open:bg-gray-100 data-popup-open:opacity-100 any-pointer-coarse:opacity-100"
      >
        <svg class="size-4" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
          <path
            d="M9.5 13c0 .8284-.67157 1.5-1.5 1.5s-1.5-.6716-1.5-1.5.67157-1.5 1.5-1.5 1.5.6716 1.5 1.5m0-5c0 .82843-.67157 1.5-1.5 1.5S6.5 8.82843 6.5 8 7.17157 6.5 8 6.5s1.5.67157 1.5 1.5m0-5c0 .82843-.67157 1.5-1.5 1.5S6.5 3.82843 6.5 3 7.17157 1.5 8 1.5s1.5.67157 1.5 1.5"
          />
        </svg>
      </Menu.Trigger>
      <Menu.Portal>
        <Menu.Positioner class="outline-hidden" align="end" :side-offset="8">
          <Menu.Popup
            class="origin-(--transform-origin) rounded-md bg-gray-50 py-1 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[transform,scale,opacity] duration-100 ease-out data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0"
          >
            <Menu.Item
              v-for="action in actions"
              :key="action"
              class="flex py-2 pr-8 pl-4 text-sm/4 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
              >{{ action }}</Menu.Item
            >
            <Menu.Separator class="m-1 h-px bg-gray-200" />
            <Menu.Item
              class="flex py-2 pr-8 pl-4 text-sm/4 text-red-700 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
              >Delete</Menu.Item
            >
          </Menu.Popup>
        </Menu.Positioner>
      </Menu.Portal>
    </Menu.Root>
  </div>
</template>

Nested menu

Nest <ContextMenu.SubmenuRoot> inside the popup to create a submenu, and use <ContextMenu.SubmenuTrigger> for the menu item that opens the nested menu.

Right click here
<script setup lang="ts">
import { ContextMenu } from '@shardsui/vue/context-menu'

const folders = ['Design', 'Engineering', 'Marketing']

function submenuOffset({ side }: { side: string }) {
  return side === 'top' || side === 'bottom' ? 4 : -4
}
</script>

<template>
  <ContextMenu.Root>
    <ContextMenu.Trigger
      class="flex h-48 w-60 items-center justify-center rounded-md border border-dashed border-gray-300 text-sm font-normal text-gray-600 select-none"
    >
      Right click here
    </ContextMenu.Trigger>
    <ContextMenu.Portal>
      <ContextMenu.Positioner class="outline-hidden">
        <ContextMenu.Popup
          class="origin-(--transform-origin) rounded-md bg-gray-50 py-1 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-opacity duration-100 ease-out data-ending-style:opacity-0"
        >
          <ContextMenu.Item
            class="flex py-2 pr-6 pl-4 text-sm/4 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
            >Open</ContextMenu.Item
          >

          <ContextMenu.SubmenuRoot>
            <ContextMenu.SubmenuTrigger
              class="flex items-center justify-between gap-4 py-2 pr-2 pl-4 text-sm/4 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900 data-popup-open:relative data-popup-open:z-0 data-popup-open:before:absolute data-popup-open:before:inset-x-1 data-popup-open:before:inset-y-0 data-popup-open:before:z-[-1] data-popup-open:before:rounded-xs data-popup-open:before:bg-gray-100 data-highlighted:data-popup-open:before:bg-gray-900"
            >
              Move to folder
              <svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                <path
                  d="M9.5 18.25L15.75 12L9.5 5.75"
                  stroke="currentColor"
                  stroke-width="1.5"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
              </svg>
            </ContextMenu.SubmenuTrigger>
            <ContextMenu.Portal>
              <ContextMenu.Positioner
                class="outline-hidden"
                :align-offset="submenuOffset"
                :side-offset="submenuOffset"
              >
                <ContextMenu.Popup
                  class="origin-(--transform-origin) rounded-md bg-gray-50 py-1 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[transform,scale,opacity] duration-100 ease-out data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0"
                >
                  <ContextMenu.Item
                    v-for="folder in folders"
                    :key="folder"
                    class="flex py-2 pr-6 pl-4 text-sm/4 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
                    >{{ folder }}</ContextMenu.Item
                  >
                </ContextMenu.Popup>
              </ContextMenu.Positioner>
            </ContextMenu.Portal>
          </ContextMenu.SubmenuRoot>

          <ContextMenu.Separator class="m-1 h-px bg-gray-200" />

          <ContextMenu.Item
            class="flex py-2 pr-6 pl-4 text-sm/4 outline-hidden select-none data-highlighted:relative data-highlighted:z-0 data-highlighted:text-gray-50 data-highlighted:before:absolute data-highlighted:before:inset-x-1 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] data-highlighted:before:rounded-sm data-highlighted:before:bg-gray-900"
            >Delete</ContextMenu.Item
          >
        </ContextMenu.Popup>
      </ContextMenu.Positioner>
    </ContextMenu.Portal>
  </ContextMenu.Root>
</template>

API reference

Root

Opens on right click or long press. Doesn't render its own HTML element.

PropTypeDefault

Trigger

An area that opens the menu on right click or long press. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-popup-openPresent when the context menu is open.
data-pressedPresent when the menu is open.

Positioner

Positions the context menu popup at the press point. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-sideWhich side of the anchor the popup is on.
data-alignHow the popup is aligned relative to the side.
data-anchor-hiddenPresent when the anchor is hidden.
data-instantPresent when animations should be instant.
data-nestedPresent when the menu is a submenu.
CSS VariableDescription
--available-widthAvailable width between the anchor and the viewport edge.
--available-heightAvailable height between the anchor and the viewport edge.
--anchor-widthWidth of the press point (0, or 10 after a long press).
--anchor-heightHeight of the press point (0, or 10 after a long press).
--transform-originTransform origin for scale animations.

Other parts

Portal, Backdrop, Popup, Arrow, Item, LinkItem, Group, GroupLabel, RadioGroup, RadioItem, RadioItemIndicator, CheckboxItem, CheckboxItemIndicator, SubmenuRoot, SubmenuTrigger, Separator — see Menu for their props and data attributes.