liqui
Components

Context Menu

A right-click menu with submenus, checkbox and radio items, on a keep-mounted glass popup.

Right-click anywhere here

Installation

npx shadcn@latest add https://liqui.design/r/context-menu.json

Installing more than one? Register the namespace once in components.json and drop the URLs:

components.json
{
  "registries": {
    "@liqui-design": "https://liqui.design/r/{name}.json"
  }
}
npx shadcn@latest add @liqui-design/context-menu

Usage

import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuTrigger,
} from '@/components/ui/context-menu';
<ContextMenu>
  <ContextMenuTrigger render={<div>Right-click me</div>} />
  <ContextMenuContent>
    <ContextMenuItem>
      New Folder
      <ContextMenuShortcut>⇧⌘N</ContextMenuShortcut>
    </ContextMenuItem>
    <ContextMenuSeparator />
    <ContextMenuItem variant="danger">Move to Trash</ContextMenuItem>
  </ContextMenuContent>
</ContextMenu>

The full anatomy also ships ContextMenuGroup, ContextMenuGroupLabel, ContextMenuCheckboxItem, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSub, ContextMenuSubTrigger and ContextMenuSubContent.

Notes

The portal stays mounted

ContextMenuContent renders through <Portal keepMounted>. A closed menu stays in the DOM, so its filter and already-decoded displacement map survive close and the next open is instant.

Without it, every open pays the feImage decode window again, and the menu appears as flat frosted blur for ~150ms before refraction arrives. That reads as lag even though nothing is slow.

display: none → shown restarts any CSS animation still attached to an element. The refraction fade-in is therefore cold-gated inside @liqui-design/glass and removed once it has played — otherwise a keep-mounted popup would re-fade on every single open, which looks worse than the problem it solves.

A submenu is a narrower box, so the popup defaults would over-bend it:

<LiquiGlass {...POPUP_GLASS} refraction={110} bezel={20} />

Same principle as the checkbox — bezel is a fraction of the box, so smaller boxes need smaller numbers.

Trigger area

ContextMenuTrigger takes render and becomes your element. Give it a real box with dimensions; a zero-height trigger has nothing to right-click.

On this page