Context Menu

Components

Context Menu New · experimental

shadcn's Context Menu with goo. The menu blooms out of the spot you right-clicked, the highlight is a blob that glides between items, and submenus bloom out of their item.

Right-click here (or long-press)
Right-click the area (long-press on touch), then move between items.

Customize

Preset
Speed 1.00×
Colour

GoopContextMenu wraps the same reka-ui Context Menu primitive as shadcn-vue's ContextMenu, with the same API. Right-click and long-press, keyboard navigation, typeahead, focus handling, and positioning work the same. Swap the imports and you're done.

Installation

Nuxt

pnpm add @goopui/nuxt
nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@goopui/nuxt'],
})

The module auto-imports every GoopContextMenu* part, adds goop.css, and points Tailwind at the component classes. It needs Tailwind CSS v4 and the shadcn tokens (--popover, --popover-foreground, --accent, --destructive, --ring, --radius).

Vue

pnpm add @goopui/vue
main.css
@import "tailwindcss";
@import "@goopui/vue/goop.css";

Usage

<script setup lang="ts">
// Nuxt auto-imports these. In plain Vue, import them:
import {
  GoopContextMenu,
  GoopContextMenuContent,
  GoopContextMenuItem,
  GoopContextMenuShortcut,
  GoopContextMenuSub,
  GoopContextMenuSubContent,
  GoopContextMenuSubTrigger,
  GoopContextMenuTrigger,
} from '@goopui/vue'
</script>

<template>
  <GoopContextMenu>
    <GoopContextMenuTrigger class="flex h-44 items-center justify-center rounded-lg border border-dashed">
      Right-click here
    </GoopContextMenuTrigger>
    <GoopContextMenuContent class="w-52">
      <GoopContextMenuItem>Back <GoopContextMenuShortcut>⌘[</GoopContextMenuShortcut></GoopContextMenuItem>
      <GoopContextMenuItem>Reload <GoopContextMenuShortcut>⌘R</GoopContextMenuShortcut></GoopContextMenuItem>
      <GoopContextMenuSub>
        <GoopContextMenuSubTrigger>More tools</GoopContextMenuSubTrigger>
        <GoopContextMenuSubContent class="w-44">
          <GoopContextMenuItem>Save page…</GoopContextMenuItem>
          <GoopContextMenuItem>Developer tools</GoopContextMenuItem>
        </GoopContextMenuSubContent>
      </GoopContextMenuSub>
    </GoopContextMenuContent>
  </GoopContextMenu>
</template>

The parts mirror shadcn-vue's: GoopContextMenu, Trigger, Content, Group, Label, Item (inset, variant="destructive"), CheckboxItem, RadioGroup, RadioItem, Separator, Shortcut, Sub, SubTrigger, SubContent, and Portal, each prefixed with GoopContextMenu. The goo lives on GoopContextMenuContent and GoopContextMenuSubContent.

Behavior

  • Bloom from the pointer. A context menu has no button to grow out of, so it grows out of the spot you clicked: the menu pools from a droplet at the pointer, spreads with a lobed edge, overshoots, and wobbles to rest (wobble, openSlosh). Then it splashes drops all around (openDrips). A menu that flips to the left of or above the pointer near the screen edge grows from its corner nearest the pointer.
  • Highlight. The highlighted item's background is a goo blob. It glides between items with a stretch and a neck, overshoots when it lands, and follows the keyboard as well as the pointer (highlight, momentum, wobble). Its swell leans toward the pointer with a little lag (followPointer).
  • Select. Choosing an item squishes the blob (pop), then the menu closes once the pop has played. event.preventDefault() in your @select still keeps it open.
  • Submenus. A submenu blooms out of its parent item and has its own highlight (submenus). Engine props you leave unset on GoopContextMenuSubContent are inherited from the parent menu.
  • Close. The menu pulls in, wobbles, and shrinks back to the point it grew from (closeSlosh).
  • Content stays sharp. The menu's background, ring, and shadow are drawn on an aria-hidden surface behind the items, so the text is never filtered.

Bloom origin

bloomFrom picks where the bloom starts: 'pointer' (the default: the right-click or long-press point; a submenu blooms from its item) or 'center' (the panel's own center, like Dropdown Menu).

<GoopContextMenuContent>...</GoopContextMenuContent>                      <!-- from the pointer -->
<GoopContextMenuContent bloom-from="center">...</GoopContextMenuContent>  <!-- from its center -->

Surface color

The trigger is an area, not a button, so the menu defaults to surface="popover": shadcn's popover tokens. 'trigger' takes the area's colors, and any CSS color works too. Submenus reuse their parent's color.

Turning parts off

Every effect is its own boolean, and they all default to on:

<GoopContextMenuContent :highlight="false">...</GoopContextMenuContent>   <!-- shadcn's flat highlight -->
<GoopContextMenuContent :pop="false">...</GoopContextMenuContent>         <!-- closes right away -->
<GoopContextMenuContent :submenus="false">...</GoopContextMenuContent>    <!-- plain shadcn submenus -->
<GoopContextMenuContent :open-drips="false">...</GoopContextMenuContent>  <!-- no splash -->

:goo="false" renders exactly shadcn's ContextMenuContent, with its own enter and exit animation.

Driving it from code

GoopContextMenuContent and GoopContextMenuSubContent expose their engine handle as handle and the highlight handle as highlight (null before they mount, or when that part is off). The menu only exists while open.

Props

The root, trigger, items, labels, groups, separators, and submenu parts take shadcn-vue's props. GoopContextMenuContent and GoopContextMenuSubContent take reka-ui's props, class, and these goo props.

GoopContextMenuContent goo props
PropTypeDefaultNotes
goobooleantrueMaster switch. false renders plain shadcn ContextMenu parts.
bloomFrom'pointer' | 'center''pointer'Where the bloom starts. 'pointer': the right-click or long-press point (a submenu: its parent item). 'center': the panel's own center. Close shrinks back to the same spot.
bloomStart'single' | 'merge''single''single': one droplet swells. 'merge': a few drops fuse.
wobblebooleantrueJelly overshoot: the open overshoot and settle, and the highlight overshoot.
momentumbooleantrueThe highlight's stretch and neck while it travels.
fadebooleantrueThe items fade in once the goo settles.
openSloshbooleantrueOn open, every edge overshoots, rebounds, and wobbles.
openDripsbooleantrueOn open, drops splash off all around.
openDripAmountnumber1Splash drop count, 0 to 3.
openDripSizenumber1Splash drop size, 0.3 to 2.5.
openDripFlingnumber1How far the drops are flung, 0 to 3.
openDripVarietynumber0.7Mix of globs and specks, 0 to 1.
sloshVarietynumber1How differently each side bulges, 0 to 1. 0 is an even, centred bulge.
closeSloshbooleantrueOn close, the menu pulls in and wobbles before it shrinks back.
highlightbooleantrueThe item highlight is a goo blob that glides between items and follows the keyboard. false gives shadcn's flat background.
popbooleantrueThe highlight squishes when an item is selected, before the menu closes.
followPointerbooleantrueThe highlight's swell follows the pointer with a little lag. Off, or from the keyboard, it stays centred.
submenusbooleantrueSubmenus bloom out of their item. false gives plain shadcn submenus.
surface'popover' | 'trigger' | string'popover'The menu's color. 'popover' uses shadcn's popover tokens. 'trigger' takes the trigger area's colors. Any CSS color works. Submenus reuse their parent's.
speednumber1Animation speed multiplier.
preset'subtle' | 'playful' | 'wild'app config, then playfulThe overall feel.
renderer'svg' | 'canvas''svg'How the goo is drawn.

Accessibility

Keyboard navigation, typeahead, focus handling, and ARIA attributes come from reka-ui. The goo surface and the highlight blob are aria-hidden and can't be clicked, and the items are never filtered. Under reduced motion the menu opens and closes plainly: it keeps its goo shape and color, but the bloom, slosh, and splash are off, the highlight snaps between items, and there's no pop. See Accessibility & reduced motion.