Westy Design Systemv0.2.7
OverviewComponentsExamplesStorybook

Foundations

  • Typography
  • Colors
  • Spacing
  • Radius
  • Shadows
  • Icons

Components

  • Accordion
  • Alert
  • Alert Dialog
  • Aspect Ratio
  • Avatar
  • Badge
  • Breadcrumb
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Data Table
  • Date Picker
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Label
  • Menubar
  • Native Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Questionnaire
  • Radio Group
  • Resizable
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Sonner
  • Spinner
  • Switch
  • Table
  • Tabs
  • Textarea
  • Toggle
  • Toggle Group
  • Tooltip

Chat

  • Attachment
  • Bubble
  • Marker
  • Message
  • Message Scroller

Components

Context Menu

Displays a menu located at the pointer, triggered by a right click or a long press.

Playground

Right click hereLong press here
—dir
—modal
—disabled
Usage
ContextMenu wraps a ContextMenuTrigger — the surface that answers the right click (or a ~700ms long press on touch) — and a portaled ContextMenuContent that opens at the pointer position, so there are no side or align props to set. Related ContextMenuItems live in a ContextMenuGroup with a ContextMenuSeparator between groups, and a ContextMenuShortcut renders its hint at the end of the row — wiring the actual key handler is up to you. The panel is at least min-w-36 wide; give it an explicit width like w-52so it doesn't resize between menus.
Right click hereLong press here
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuGroup,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuTrigger,
} from "@workspace/ui/components/context-menu"

<ContextMenu>
  <ContextMenuTrigger className="flex aspect-video w-full max-w-xs items-center justify-center rounded-xl border border-dashed text-sm">
    Right click here
  </ContextMenuTrigger>
  <ContextMenuContent className="w-52">
    <ContextMenuGroup>
      <ContextMenuItem>
        Back <ContextMenuShortcut>⌘[</ContextMenuShortcut>
      </ContextMenuItem>
      <ContextMenuItem disabled>
        Forward <ContextMenuShortcut>⌘]</ContextMenuShortcut>
      </ContextMenuItem>
      <ContextMenuItem>
        Reload <ContextMenuShortcut>⌘R</ContextMenuShortcut>
      </ContextMenuItem>
    </ContextMenuGroup>
    <ContextMenuSeparator />
    <ContextMenuGroup>
      <ContextMenuItem>
        Save <ContextMenuShortcut>⌘S</ContextMenuShortcut>
      </ContextMenuItem>
      <ContextMenuItem>
        Save As… <ContextMenuShortcut>⇧⌘S</ContextMenuShortcut>
      </ContextMenuItem>
    </ContextMenuGroup>
  </ContextMenuContent>
</ContextMenu>
With icons
Drop a Phosphor icon before the item text — items lay out with gap-2 and size unclassed icons to size-4 automatically. Icons inherit the item color, including the accent foreground while the item is focused.
Right click hereLong press here
<ContextMenuContent className="w-52">
  <ContextMenuItem>
    <CopyIcon /> Copy
  </ContextMenuItem>
  <ContextMenuItem>
    <ScissorsIcon /> Cut
  </ContextMenuItem>
  <ContextMenuItem>
    <ClipboardTextIcon /> Paste
  </ContextMenuItem>
</ContextMenuContent>
Checkboxes
ContextMenuCheckboxItem is a controlled toggle: pass checked and onCheckedChange, and the check indicator renders at the end of the row. The menu closes on select by default — call event.preventDefault() in onSelect to keep it open while toggling several options.
Right click hereLong press here
const [showBookmarksBar, setShowBookmarksBar] = React.useState(true)
const [showFullUrls, setShowFullUrls] = React.useState(false)
const [showDeveloperTools, setShowDeveloperTools] = React.useState(false)

<ContextMenuContent className="w-52">
  <ContextMenuLabel>Appearance</ContextMenuLabel>
  <ContextMenuSeparator />
  <ContextMenuCheckboxItem
    checked={showBookmarksBar}
    onCheckedChange={setShowBookmarksBar}
  >
    Show Bookmarks Bar
  </ContextMenuCheckboxItem>
  <ContextMenuCheckboxItem
    checked={showFullUrls}
    onCheckedChange={setShowFullUrls}
    disabled
  >
    Show Full URLs
  </ContextMenuCheckboxItem>
  <ContextMenuCheckboxItem
    checked={showDeveloperTools}
    onCheckedChange={setShowDeveloperTools}
  >
    Show Developer Tools
  </ContextMenuCheckboxItem>
</ContextMenuContent>
Radio group
ContextMenuRadioGroup holds the selected value and reports changes through onValueChange; each ContextMenuRadioItem marks the active choice with the same end-of-row indicator.
Right click hereLong press here
const [person, setPerson] = React.useState("pedro")

<ContextMenuContent className="w-52">
  <ContextMenuLabel>People</ContextMenuLabel>
  <ContextMenuSeparator />
  <ContextMenuRadioGroup value={person} onValueChange={setPerson}>
    <ContextMenuRadioItem value="pedro">Pedro Duarte</ContextMenuRadioItem>
    <ContextMenuRadioItem value="colm">Colm Tuite</ContextMenuRadioItem>
  </ContextMenuRadioGroup>
</ContextMenuContent>
Submenu
Nest a ContextMenuSub with its own ContextMenuSubTrigger and ContextMenuSubContent for a second level. The sub trigger appends a caret automatically (mirrored in RTL), highlights while its panel is open, and opens on hover, Enter or the arrow key pointing into the submenu.
Right click hereLong press here
<ContextMenuContent className="w-52">
  <ContextMenuItem>
    Reload <ContextMenuShortcut>⌘R</ContextMenuShortcut>
  </ContextMenuItem>
  <ContextMenuSub>
    <ContextMenuSubTrigger>More Tools</ContextMenuSubTrigger>
    <ContextMenuSubContent className="w-44">
      <ContextMenuItem>Save Page…</ContextMenuItem>
      <ContextMenuItem>Create Shortcut…</ContextMenuItem>
      <ContextMenuItem>Name Window…</ContextMenuItem>
      <ContextMenuSeparator />
      <ContextMenuItem>Developer Tools</ContextMenuItem>
    </ContextMenuSubContent>
  </ContextMenuSub>
  <ContextMenuSeparator />
  <ContextMenuItem>
    View Source <ContextMenuShortcut>⌥⌘U</ContextMenuShortcut>
  </ContextMenuItem>
</ContextMenuContent>
Label and inset
When only some rows carry an icon, pass inset to the plain ones — it pads the start by the icon column (ps-8) so every label lines up. The prop exists on items, labels, checkbox and radio items, and sub triggers alike.
Right click hereLong press here
<ContextMenuContent className="w-52">
  <ContextMenuLabel inset>File</ContextMenuLabel>
  <ContextMenuItem>
    <DownloadSimpleIcon /> Download
  </ContextMenuItem>
  <ContextMenuItem inset>Rename</ContextMenuItem>
  <ContextMenuItem inset>Duplicate</ContextMenuItem>
</ContextMenuContent>
Destructive
variant="destructive" paints the item — icon included — with the destructive token and swaps the focus highlight for a translucent destructive wash. Reserve it for the irreversible row and keep a separator before it.
Right click hereLong press here
<ContextMenuContent className="w-52">
  <ContextMenuItem>
    <PencilSimpleIcon /> Edit
  </ContextMenuItem>
  <ContextMenuItem>
    <ShareNetworkIcon /> Share
  </ContextMenuItem>
  <ContextMenuSeparator />
  <ContextMenuItem variant="destructive">
    <TrashIcon /> Delete
  </ContextMenuItem>
</ContextMenuContent>
RTL
Pass dir="rtl" on the ContextMenu root. Items, indicators and shortcuts are laid out with logical properties (ps-8, pe-8, end-2, ms-auto), and the submenu caret flips via rtl:rotate-180 — so the whole menu mirrors without extra classes.
انقر بزر الفأرة الأيمن هنا
<ContextMenu dir="rtl">
  <ContextMenuTrigger className="…">انقر بزر الفأرة الأيمن هنا</ContextMenuTrigger>
  <ContextMenuContent className="w-52">
    <ContextMenuItem>
      رجوع <ContextMenuShortcut>⌘[</ContextMenuShortcut>
    </ContextMenuItem>
    <ContextMenuItem disabled>
      تقدم <ContextMenuShortcut>⌘]</ContextMenuShortcut>
    </ContextMenuItem>
    <ContextMenuItem>
      إعادة تحميل <ContextMenuShortcut>⌘R</ContextMenuShortcut>
    </ContextMenuItem>
    <ContextMenuSub>
      <ContextMenuSubTrigger>المزيد من الأدوات</ContextMenuSubTrigger>
      <ContextMenuSubContent className="w-44">
        <ContextMenuItem>حفظ الصفحة…</ContextMenuItem>
        <ContextMenuItem>أدوات المطور</ContextMenuItem>
      </ContextMenuSubContent>
    </ContextMenuSub>
  </ContextMenuContent>
</ContextMenu>
Accessibility
The Radix ContextMenu primitive implements the WAI-ARIA menu pattern: once open, arrow keys and typeahead move focus between items, Enter or Space activates, and Escape closes and returns focus to the trigger. Checkbox and radio items announce their state via aria-checked; disabled items are skipped. But the menu only opens from a right click or a long press — there is no keyboard gesture and no visible affordance — so label the trigger surface with what it does and keep every action reachable elsewhere too (a toolbar or a Dropdown Menu). Use pointer variants to match the hint to the input device.
<ContextMenuTrigger className="…">
  <span className="hidden pointer-fine:inline-block">Right click here</span>
  <span className="hidden pointer-coarse:inline-block">Long press here</span>
</ContextMenuTrigger>
API Reference
Fifteen parts wrap the Radix ContextMenu primitive with Westy styling. ContextMenu is the stateful root, ContextMenuTrigger the surface that listens for the right click, and ContextMenuContent handles the portal and positioning automatically — the panel opens at the pointer, so there are no side, align or asChild props here. ContextMenuPortal exists for advanced composition only. Every Radix prop passes through; see the Radix documentation for the full primitive API.
ComponentPropDefault
ContextMenuonOpenChange?: (open: boolean) => void—
ContextMenumodal?: booleantrue
ContextMenudir?: "ltr" | "rtl""ltr"
ContextMenuTriggerdisabled?: booleanfalse
ContextMenuContentalignOffset?: number0
ContextMenuContentavoidCollisions?: booleantrue
ContextMenuItemvariant?: "default" | "destructive""default"
ContextMenuIteminset?: boolean, disabled?: boolean, onSelect?: (event: Event) => void—
ContextMenuCheckboxItemchecked?: boolean, onCheckedChange?: (checked: boolean) => void—
ContextMenuRadioGroupvalue?: string, onValueChange?: (value: string) => void—
<ContextMenu data-slot="context-menu" />
<ContextMenuTrigger data-slot="context-menu-trigger" data-state />
<ContextMenuContent data-slot="context-menu-content" data-side data-align />
<ContextMenuGroup data-slot="context-menu-group" />
<ContextMenuLabel data-slot="context-menu-label" data-inset />
<ContextMenuItem data-slot="context-menu-item" data-variant data-inset />
<ContextMenuCheckboxItem data-slot="context-menu-checkbox-item" data-state />
<ContextMenuRadioGroup data-slot="context-menu-radio-group" />
<ContextMenuRadioItem data-slot="context-menu-radio-item" data-state />
<ContextMenuSeparator data-slot="context-menu-separator" />
<ContextMenuShortcut data-slot="context-menu-shortcut" />
<ContextMenuSub data-slot="context-menu-sub" />
<ContextMenuSubTrigger data-slot="context-menu-sub-trigger" data-open />
<ContextMenuSubContent data-slot="context-menu-sub-content" />

Tokens used

13 design tokens consumed by the Context Menu component.

  • --popover

    Popover

    bg-popover
  • --popover-foreground

    Popover Foreground

    text-popover-foreground
  • --accent

    Accent

    focus:bg-accentdata-open:bg-accent
  • --accent-foreground

    Accent Foreground

    focus:text-accent-foregrounddata-open:text-accent-foreground
  • --destructive

    Destructive

    data-[variant=destructive]:text-destructivedata-[variant=destructive]:focus:bg-destructive/10dark:data-[variant=destructive]:focus:bg-destructive/20
  • --muted-foreground

    Muted Foreground

    text-muted-foreground
  • --foreground

    Foreground

    ring-foreground/10
  • --border

    Border

    bg-borderborder
  • --radius-md

    Radius 0.8× (8px)

    rounded-md
  • --radius-sm

    Radius 0.6× (6px)

    rounded-sm
  • --text-sm

    Text Small (14px)

    text-sm
  • --text-xs

    Text Extra Small (12px)

    text-xs
  • --spacing

    Spacing unit (4px)

    p-1px-2py-1.5gap-2ps-8pe-8min-w-36size-4