Components

Context Menu

Displays a menu to the user — such as a set of actions or functions — triggered by right-clicking.

Right click here

Source Code

You will need to install the Vue 3 Context Menu package

yarn add @imengyu/vue3-context-menu

Copy the following code into your project.

<template>
  <ClientOnly>
    <ContextMenu v-model:show="show" :options="localOptions">
      <template
        #itemRender="{ disabled, label, icon, showRightArrow, onClick, onMouseEnter, shortcut }"
      >
        <button
          class="flex w-full cursor-pointer items-center justify-between rounded px-2 py-1.5 text-left text-sm hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50"
          @click="onClick"
          :disabled="disabled"
          @mouseenter="onMouseEnter"
        >
          <div class="flex grow items-center gap-3">
            <Icon v-if="icon" :name="icon" />
            <span>{{ label }}</span>
          </div>
          <span v-if="showRightArrow"
            ><Icon name="heroicons:chevron-right" class="h-4 w-4 text-muted-foreground"
          /></span>
          <span v-else-if="shortcut" class="text-xs text-muted-foreground">{{ shortcut }}</span>
        </button>
      </template>

      <template v-for="(item, i) in items" :key="i">
        <ContextMenuItem v-if="!item.children" v-bind="item as any" />
        <ContextMenuGroup v-else-if="item.children" v-bind="item as any">
          <ContextMenuItem
            v-for="(child, k) in item.children"
            :key="`child-${k}`"
            v-bind="child as any"
          />
        </ContextMenuGroup>
        <ContextMenuSeparator class="!bg-popover !p-1.5 after:!bg-border" v-if="item.divided" />
      </template>
    </ContextMenu>
  </ClientOnly>
</template>

<script setup lang="ts">
  import {
    ContextMenu,
    ContextMenuGroup,
    ContextMenuSeparator,
    ContextMenuItem,
    type MenuOptions,
    type MenuItem,
  } from "@imengyu/vue3-context-menu";

  const { x, y } = useMouse();
  const { y: windowY } = useWindowScroll();

  const props = withDefaults(
    defineProps<{
      modelValue?: boolean;
      config?: Omit<MenuOptions, "x" | "y" | "items">;
      items?: MenuItem[];
    }>(),
    {
      modelValue: false,
    }
  );

  const localOptions = computed(() => {
    const top = unref(y) - unref(windowY);
    const left = unref(x);
    return {
      zIndex: 1000,
      x: left,
      y: top,
      closeWhenScroll: false,
      ...props.config,
      customClass: "my-context-menu",
    };
  });
  const emit = defineEmits<{
    "update:modelValue": [any];
  }>();

  const show = computed({
    get() {
      return props.modelValue;
    },
    set(value) {
      emit("update:modelValue", value);
    },
  });
</script>
<style src="@imengyu/vue3-context-menu/lib/vue3-context-menu.css"></style>

<style>
  .my-context-menu {
    @apply !min-w-[250px] !rounded-md border !bg-popover p-1 text-popover-foreground !shadow transition-opacity duration-200;
  }
</style>