IX Surfaces

Star on GitHubFeedback
IX SurfacesRadixot-menu

Context menu

The same list of words, opened where you pressed and spreading from that point like ink.

Report an issue with Context menu

Example

List

Right-click here, or focus this and press Shift F10.

Leaders

Right-click the text, or focus it and press Shift F10.

Orbit

Right-click the photograph, or focus it and press Shift F10.

List, pointed at
Copy the link
Duplicate ⌘D
Archive
Leaders, pointed at
Cut ⌘X
Copy ⌘C
Add a margin note
Orbit, pointed at
Open
Set as the cover
Copy the photograph
Save to Halden
Remove it

Install

Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base, dropdown-menu, and popover along.

npx shadcn@latest add https://thedirectors.agency/ui/r/context-menu.json

Usage

context-menu-example.tsx
"use client"import * as React from "react"import {ContextMenu,ContextMenuCheckboxItem,ContextMenuContent,ContextMenuItem,ContextMenuSeparator,ContextMenuShortcut,ContextMenuSub,ContextMenuSubContent,ContextMenuSubTrigger,ContextMenuTrigger,} from "@/components/ui/context-menu"/** The area that answers a right-click: corner marks round a line of help. */function Area({ id, children }: { id: string; children: React.ReactNode }) {return (<ContextMenuTrigger asChild><divclassName="ot-corners"tabIndex={0}aria-describedby={id}style={{display: "grid",placeItems: "center",width: "min(19rem, 100%)",minHeight: "9rem",padding: "var(--ot-space-5)",textAlign: "center",fontSize: "var(--ot-pp)",color: "var(--ot-pencil)",cursor: "context-menu",}}><p id={id}>{children}</p></div></ContextMenuTrigger>)}function Notes() {const [pinned, setPinned] = React.useState(false)return (<ContextMenu><Area id="context-menu-help">Right-click here, or focus this and press Shift F10.</Area><ContextMenuContent aria-label="Spring notes"><ContextMenuItem>Copy the link</ContextMenuItem><ContextMenuItem>Duplicate <ContextMenuShortcut>⌘D</ContextMenuShortcut></ContextMenuItem><ContextMenuCheckboxItem checked={pinned} onCheckedChange={setPinned}>Pin to the top</ContextMenuCheckboxItem><ContextMenuSub><ContextMenuSubTrigger>Move to</ContextMenuSubTrigger><ContextMenuSubContent><ContextMenuItem>Halden</ContextMenuItem><ContextMenuItem>Northlight</ContextMenuItem></ContextMenuSubContent></ContextMenuSub><ContextMenuItem>Archive</ContextMenuItem><ContextMenuSeparator /><ContextMenuItem variant="destructive">Delete Spring notes for good</ContextMenuItem></ContextMenuContent></ContextMenu>)}/** Every word joined to its keys by leader dots, as on a contents page. */function Text() {return (<ContextMenu><Area id="context-menu-leaders-help">Right-click the text, or focus it and press Shift F10.</Area><ContextMenuContent variant="leaders" aria-label="Selected text"><ContextMenuItem>Cut <ContextMenuShortcut>⌘X</ContextMenuShortcut></ContextMenuItem><ContextMenuItem>Copy <ContextMenuShortcut>⌘C</ContextMenuShortcut></ContextMenuItem><ContextMenuItem>Paste <ContextMenuShortcut>⌘V</ContextMenuShortcut></ContextMenuItem><ContextMenuSeparator /><ContextMenuItem>Look up <ContextMenuShortcut>⌃⌘D</ContextMenuShortcut></ContextMenuItem><ContextMenuItem>Add a margin note</ContextMenuItem></ContextMenuContent></ContextMenu>)}/** The words stand on an arc bowed away from where you pressed. */function Photo() {return (<ContextMenu><Area id="context-menu-orbit-help">Right-click the photograph, or focus it and press Shift F10.</Area><ContextMenuContent variant="orbit" aria-label="Photograph"><ContextMenuItem>Open</ContextMenuItem><ContextMenuItem>Set as the cover</ContextMenuItem><ContextMenuItem>Copy the photograph</ContextMenuItem><ContextMenuItem>Save to Halden</ContextMenuItem><ContextMenuItem>Show the original</ContextMenuItem><ContextMenuItem variant="destructive">Remove it</ContextMenuItem></ContextMenuContent></ContextMenu>)}export default function Example() {return (<div className="flex w-full flex-wrap items-start gap-x-10 gap-y-10"><div className="grid gap-3"><span className="ot-label">List</span><Notes /></div><div className="grid gap-3"><span className="ot-label">Leaders</span><Text /></div><div className="grid gap-3"><span className="ot-label">Orbit</span><Photo /></div></div>)}/** A menu pinned open where it was pressed, one item pointed at. The docs' states row is inert, so this is the look alone. */function Pinned({ variant, children }: { variant: string; children: React.ReactNode }) {return (<div style={{ padding: "var(--ot-space-3) 0 0 var(--ot-space-3)" }}><div className="ot-pop ot-menu" data-at="point" data-variant={variant} data-state="open" style={{ "--ot-at-x": "-2px", "--ot-at-y": "-2px" } as React.CSSProperties}>{children}</div></div>)}

Contract

  • Underneath: Radix DropdownMenu, ContextMenu and Menubar.
  • Anatomy: <div class="ot-pop ot-menu" popover role="menu"> holding .ot-menu-label headings and <button class="ot-menu-item" role="menuitem"> (or menuitemcheckbox with aria-checked), each with optional .ot-menu-keys. data-variant="danger" for a destructive item. A menubar is <div class="ot-menubar" role="menubar"> of .ot-menubar-trigger buttons on a hairline; each menu is a ot-menu hung in that hairline. A context menu is an ot-menu with data-at="point", placed where the person pressed.
  • The item you point at is marked with the highlighter. Items arrive in turn. A menubar opens its hairline into a pocket under the open word: the menu rises over the rule, its paper hides the rule there, and its sides and foot carry the same line on, down, along and back up. The word and its menu are one shape, with no leader and no stroke. The menu unrolls down out of the line and rolls back up into it. Its words stand under the word, and the bar's words sit in from the rule's ends by a menu's own inset, so the first menu folds from the rule too. With no room below, it turns above the words in a whole frame. Focus on a word is the accent stroke laid on the rule under it. A context menu spreads from the point like ink (ot-spread); it opens toward the line's end and down from the point, back from it without room, and on a phone slides in to fit. A submenu opens beside its item; on a narrow screen, with room on neither side, it drops open under the item, in by the words' inset, the next level of an outline, and hangs from the item on the popover's leader (data-drop): the dot on the marked item's foot, the line dropping over a strip of paper. The item's arrow turns down while it is open. Shortcut keys (dir="ltr") read ⌘Z in either direction. Without a dir, each menu takes the direction of the page around its trigger or bar, so on a right-to-left page it hangs from the right end, submenus open to the left, and the arrow keys mirror.
  • variant on the list (DropdownMenuContent, ContextMenuContent; data-variant): list (the default) or leaders, and also marginalia on a dropdown menu or orbit on a context menu. On the Menubar root: pocket (the default), leaders or caption. Under every variant a context menu's point is pinned by a cross, the register mark of "the uncreative", over the menu's corner, and the menu spreads from it.
  • leaders: a book's contents page. Each word is joined to its keys (or a submenu's arrow) by a row of leader dots in rule-strong; a word with neither has none, and the words don't wrap. The highlighter stands down: the word you point at inks, and its dots ink from the word to its keys, passing through as the highlighter does. Submenus keep the leaders. A menubar set to leaders sets every menu so, in its pocket.
  • marginalia (dropdown menu): the small notes on hairline arrows of "It has to be design.". Each item's hint is a note (pp, graphite, at most 13em) hung beside the list at the height of the word you point at, on a hairline arrow pointing back at it; it glides from word to word, and a word without a hint leaves the margin empty. The list keeps its highlighter. With no room beside the list (under 220px) the note stands under it as its caption, without the arrow. The note is aria-hidden: each item carries its hint as its aria-description.
  • orbit (context menu): after WOVE. The words stand on an arc bowed away from the point, one dot on the arc for each; the one you point at inks and its dot lands as the accent. The arc is the panel's edge: the paper lies only outside it, and the panel's hairline and shadow stand down. Every row is one step high, and the words swing out to the arc as they arrive. Put items straight in the list (a group counts as one step); a separator is a step of silence with a short rule in it. In forced colours the arc gives way to a framed panel and the dots stay.
  • caption (menubar): the label at the far end of the SHAPES / GRADIENTS frame, and an editor's status line. The bar's far end keeps a small line of where you are, the path from the open word to the item you point at, parted by slashes (File / Export / As PDF); its last step inks and arrives as you move. At rest it shows the bar's aria-label in pencil. Below 40rem only the last step shows. It is aria-hidden, since the menus already name the path.
  • Forced colours: the highlighter gives way to the system's Highlight; leaders' dots and the marginalia arrow are CanvasText; orbit's pointed dot is Highlight. Reduced motion: the dots, the note and the caption move straight to their places.
  • Keyboard: Down and Up move, Home and End go to the ends, Enter runs, Escape closes, Tab closes and moves on. Right opens a submenu and Left closes it (mirrored right to left). In a menubar, Down opens, and Left and Right move between menus, open or closed. The context menu opens with the ContextMenu key or Shift+F10.

Props

Props of Context menu
PropTypeDefaultWhat it does
ContextMenuTriggerRadixThe area that answers a right-click, a long press, the ContextMenu key or Shift+F10. Give it tabIndex={0} so the keyboard can reach it.
ContextMenu dir"ltr" | "rtl"Without one, the menu takes the direction of the page around its trigger, so its arrow keys and submenus run the page's way.
ContextMenuContentRadixOpens at the pointer, toward the line's end and down: to the left on a right-to-left page. Without room it opens back from the point; on a phone, with room neither way, it slides in to fit. There is no leader line, since nothing hangs it from a trigger.
ContextMenuContent variant"list" | "leaders" | "orbit""list"How the list is set. A cross pins the point you pressed over every one. list: the word you point at takes the highlighter. leaders: each word is joined to its keys by leader dots, which ink from the word to its keys as you point at it. orbit: the words stand on an arc bowed away from the point, a dot on the arc for each; the one you point at inks and its dot lands as the accent. Put orbit's items straight in the list: a group counts as one step of the arc.
ContextMenuItem variant"default" | "destructive""default"A destructive item sits heavier and says so in its words. It is never red.
ContextMenuCheckboxItem / RadioItemRadixChecked, the sentence turns to the expression italic.
ContextMenuSubTriggerRadixThe only item with an arrow (→): it opens something. On a narrow screen its submenu drops open under it and the arrow turns down.

Craft

Where its move comes from

Where its move comes from
ComponentReferenceMove
Combobox, menu, commandWeingart letterThe highlighter marks what matches
Leaders menuA book's contents pageLeader dots join each word to its keys
Orbit context menuWOVEThe words stand on an arc bowed away from the point
Context menu point"the uncreative" register crossesA cross pins where you pressed

How it moves

How it moves
ComponentArticulationWhat moves
MenuArpeggioDot, leader, words in turn; a context menu spreads from the point
Leaders menuPass-throughThe dots of the word you point at ink from the word to its keys and leave the same way
Orbit context menuSpiccatoThe words swing out to the arc as they arrive; the dot you point at lands as the accent