Context Menu
Primitive

Context Menu

Headless menu primitives that open at a pointer location or from the keyboard to present actions for a specific area.

Live behavior

Context Menu in motion

Interactive
Atom behavior · App-owned appearance
Preparing behavior…

Right-click the target or use the keyboard context-menu command.

waiting for input

When to Use

Use ContextMenu when actions belong to the exact thing a user right-clicked, such as a file, canvas object, or table row. Do not make it the only way to reach important actions because context menus are easy to miss. Use Menu or DropdownMenu when a visible button should open the actions, and Menubar for a persistent application command bar.

Features

The behavior Atom owns before your product adds appearance.

  • Opens at right-click coordinates or the keyboard trigger location.
  • Re-invokes at the latest right-click point and transfers an open menu directly between registered ContextMenu targets without exposing the browser menu.
  • Opens from a cancel-safe 700 ms touch/pen long press with a 10 px tolerance.
  • Supports controlled state, modal behavior, looping, and dismissal settings.
  • Includes actions, checkbox choices, radio choices, groups, separators, and nested menus.
  • Positions and collision-adjusts Content with Floating UI.
  • Supports pointer highlight, keyboard navigation, typeahead, and RTL submenus.
  • Keeps nested portalled menus inside parent modal focus scopes.

Import

tsx
import { ContextMenu } from "@flowstack-ui/atom";

Anatomy

tsx
<ContextMenu.Root>
  <ContextMenu.Trigger />
  <ContextMenu.Portal>
  <ContextMenu.Content>
    <ContextMenu.Arrow />
    <ContextMenu.Group>
      <ContextMenu.Label />
      <ContextMenu.Item />
      <ContextMenu.CheckboxItem><ContextMenu.ItemIndicator /></ContextMenu.CheckboxItem>
      <ContextMenu.RadioGroup>
        <ContextMenu.RadioItem />
      </ContextMenu.RadioGroup>
    </ContextMenu.Group>
    <ContextMenu.Separator />
    <ContextMenu.Sub>
      <ContextMenu.SubTrigger />
      <ContextMenu.SubContent>
        <ContextMenu.Item />
      </ContextMenu.SubContent>
    </ContextMenu.Sub>
  </ContextMenu.Content>
  </ContextMenu.Portal>
</ContextMenu.Root>

API Reference

Root

Owns Menu state and the point used to position Content. It renders no wrapper.

PropTypeDefault
openboolean-
defaultOpenbooleanfalse
onOpenChange(open: boolean) => void-
modalbooleantrue
closeOnSelectbooleantrue
loopbooleantrue
closeOnEscapebooleantrue

Trigger

Wraps the area that owns the menu. It renders a span with display: contents by default, opens on the context-menu gesture, and does not add button semantics to the wrapped area.

Touch and pen track one primary long press. Movement beyond 10 px, scrolling, early release, cancellation, a second pointer, disabled state, unmount, or a native contextmenu event cancels the fallback.

While a ContextMenu is open, another secondary click inside its Trigger updates the point anchor and keeps the custom menu active. A secondary click on another ContextMenu Trigger closes the previous root and opens the new one. Consumer onContextMenu handlers still run for the new invocation and may prevent Atom's open or reposition behavior.

PropTypeDefault
disabledbooleanfalse
asChildbooleanfalse
renderRenderProp-
Data attributeValues
[data-slot]"context-menu-trigger"
[data-state]"open" | "closed"
[data-disabled]Present when disabled
[data-pressed]Present only while a long press is pending

Content

Renders the portalled menu at Trigger's pointer or keyboard anchor, manages focus and highlight, locks scrolling when modal, and closes on outside click. The resolved explicit, Trigger, or provider direction is preserved on the portalled Content and nested SubContent DOM.

With a submenu open, an activation inside an ancestor menu but outside that submenu closes only the submenu. An activation outside every menu surface closes the complete ContextMenu tree in one activation.

PropTypeDefault
side"top" | "right" | "bottom" | "left""bottom"
align"start" | "center" | "end""start"
sideOffsetnumber4
loopbooleanRoot loop
ariaLabelstring-
onKeyDownCaptureKeyboardEventHandler-
ARIA attributeValues
role"menu"
aria-orientation"vertical"
aria-labelValue from ariaLabel
aria-labelledbyTrigger ID when ariaLabel is absent
Data attributeValues
[data-slot]"menu-content"
[data-state]"open" | "closed"
[data-side]Resolved placement side
[data-align]Resolved placement alignment
[data-positioned]Present after positioning

Group

Groups related menu entries as a div. Provide a native accessible label when the grouping needs to be announced.

ARIA attributeValues
role"group"
Data attributeValues
[data-slot]"menu-group"

Item

Represents one command. Its unique value supports highlight and typeahead; textValue supplies searchable text when children are not plain text.

PropTypeDefault
valuestringrequired
textValuestringText child or value
onSelect() => void-
disabledbooleanfalse
closeOnSelectbooleanRoot setting
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
role"menuitem"
aria-disabled"true" when disabled
Data attributeValues
[data-slot]"menu-item"
[data-highlighted]Present when highlighted
[data-disabled]Present when disabled
[data-value]Item value

CheckboxItem

Represents an independently toggleable menu choice. It stays open by default so several choices can be changed in one visit.

PropTypeDefault
valuestringrequired
textValuestringText child or value
checkedboolean | "indeterminate"false
onCheckedChange(checked: boolean) => void-
disabledbooleanfalse
closeOnSelectbooleanfalse
ARIA attributeValues
role"menuitemcheckbox"
aria-checkedCurrent checked state
aria-disabled"true" when disabled
Data attributeValues
[data-slot]"menu-checkbox-item"
[data-highlighted]Present when highlighted
[data-disabled]Present when disabled
[data-checked]Present when checked
[data-value]Item value

RadioGroup

Shares one controlled value with nested RadioItems and renders a semantic group.

PropTypeDefault
valuestring-
onValueChange(value: string) => void-
ARIA attributeValues
role"group"
Data attributeValues
[data-slot]"menu-radio-group"

RadioItem

Represents one mutually exclusive value inside RadioGroup. Selection does not close the menu unless closeOnSelect is enabled.

PropTypeDefault
valuestringrequired
textValuestringText child or value
disabledbooleanfalse
closeOnSelectbooleanfalse
ARIA attributeValues
role"menuitemradio"
aria-checkedWhether its value matches RadioGroup
aria-disabled"true" when disabled
Data attributeValues
[data-slot]"menu-radio-item"
[data-highlighted]Present when highlighted
[data-disabled]Present when disabled
[data-checked]Present when selected
[data-value]Item value

Separator

Marks a horizontal boundary between related groups of commands.

ARIA attributeValues
role"separator"
aria-orientation"horizontal"
Data attributeValues
[data-slot]"menu-separator"

Sub

Owns controlled or uncontrolled state for one nested menu and renders no wrapper.

PropTypeDefault
openboolean-
defaultOpenbooleanfalse
onOpenChange(open: boolean) => void-

SubTrigger

Registers a menu item that opens SubContent after intentional mouse movement and a hover delay, by click, or with the direction-aware submenu key. Merely positioning a menu beneath a stationary pointer does not open the submenu.

PropTypeDefault
valuestringrequired
textValuestringText child or value
disabledbooleanfalse
ARIA attributeValues
role"menuitem"
aria-haspopup"menu"
aria-expandedSub open state
aria-disabled"true" when disabled
Data attributeValues
[data-slot]"menu-sub-trigger"
[data-state]"open" | "closed"
[data-highlighted]Present when highlighted
[data-disabled]Present when disabled
[data-value]Trigger value

SubContent

Renders a separately portalled nested menu, positions it beside SubTrigger, and mirrors its side and open/close keys in RTL.

PropTypeDefault
sideOffsetnumber4
loopbooleantrue
ariaLabelstring-
ARIA attributeValues
role"menu"
aria-orientation"vertical"
aria-labelValue from ariaLabel
aria-labelledbySubTrigger ID when ariaLabel is absent
Data attributeValues
[data-slot]"menu-sub-content"
[data-state]"open" | "closed"
[data-side]Resolved placement side
[data-positioned]Present after positioning

The package also exports useContextMenuContext for advanced custom parts. It returns the current anchor point and its setter and must be used within Root.

Portal, Arrow, Label, and ItemIndicator use the shared Menu contract. CheckboxItem supports "indeterminate"; retained DOM parts accept refs, native props, asChild, and render; Content/SubContent expose the shared --atom-menu-* geometry variables.

Examples

File Actions

tsx
import { ContextMenu } from "@flowstack-ui/atom";

export function FileContextMenu() {
  return (
    <ContextMenu.Root>
      <ContextMenu.Trigger>
        <article tabIndex={0}>Quarterly report.pdf</article>
      </ContextMenu.Trigger>
      <ContextMenu.Content ariaLabel="File actions">
        <ContextMenu.Item value="open" onSelect={() => console.log("Open")}>
          Open
        </ContextMenu.Item>
        <ContextMenu.Item value="rename" onSelect={() => console.log("Rename")}>
          Rename
        </ContextMenu.Item>
        <ContextMenu.Separator />
        <ContextMenu.Item value="delete" onSelect={() => console.log("Delete")}>
          Delete
        </ContextMenu.Item>
      </ContextMenu.Content>
    </ContextMenu.Root>
  );
}

Persistent View Choices

tsx
import { useState } from "react";
import { ContextMenu } from "@flowstack-ui/atom";

export function CanvasContextMenu() {
  const [snap, setSnap] = useState(true);
  const [density, setDensity] = useState("comfortable");

  return (
    <ContextMenu.Root closeOnSelect={false}>
      <ContextMenu.Trigger>
        <section tabIndex={0}>Canvas</section>
      </ContextMenu.Trigger>
      <ContextMenu.Content ariaLabel="View options">
        <ContextMenu.CheckboxItem
          value="snap"
          checked={snap}
          onCheckedChange={setSnap}
        >
          Snap to grid
        </ContextMenu.CheckboxItem>
        <ContextMenu.RadioGroup value={density} onValueChange={setDensity}>
          <ContextMenu.RadioItem value="compact">Compact</ContextMenu.RadioItem>
          <ContextMenu.RadioItem value="comfortable">Comfortable</ContextMenu.RadioItem>
        </ContextMenu.RadioGroup>
      </ContextMenu.Content>
    </ContextMenu.Root>
  );
}

Accessibility

ContextMenu follows the WAI-ARIA Menu pattern. Trigger supports right click, keyboard invocation, and deliberate touch/pen long press. Content moves real item focus. Keep important actions available through a visible control as well.

KeyDescription
Shift+F10 / ContextMenuOpens at the keyboard anchor and highlights the first item.
ArrowDown / ArrowUpMoves through items, including disabled items, following loop.
Home / EndMoves to the first or last item.
Enter / SpaceActivates the focused item unless disabled.
Printable characterMoves by typeahead label.
ArrowRightOpens a submenu in LTR; closes it in RTL.
ArrowLeftCloses a submenu in LTR; opens it in RTL.
EscapeCloses the topmost submenu or menu.
Tab / Shift+TabCloses and exits after/before the invoking context.

Changelog

Unreleased

0.24.0

  • Added public Agent Knowledge for component selection, required composition, recurring mistakes, and validation.

0.20.7

  • Kept the custom menu active on repeated secondary clicks, repositioned it at the latest invocation point, and transferred it directly between registered ContextMenu targets instead of exposing the browser menu.
  • Corrected point-anchor wiring so Content positions from its pointer or keyboard coordinate rather than falling back to the viewport corner.
  • Inherited whole-tree outside dismissal when a submenu is open.

0.20.6

  • Inherited movement-gated submenu hover intent so opening a parent ContextMenu cannot also open a submenu that appears beneath a stationary pointer.

0.20.5

  • Inherited resolved direction on portalled Content and SubContent so logical row anatomy and submenu chevrons mirror with the trigger.
  • Inherited block-axis submenu collision fallbacks and narrow-viewport containment when neither inline side fits.

0.20.3

  • Inherited document-only overflow locking so sticky application chrome remains anchored while a modal ContextMenu is open.

0.12.0

  • Added a cancel-safe 700 ms touch/pen long press with 10 px movement tolerance, native-contextmenu de-duplication, temporary pressed state, and full cleanup.
  • Inherited real item focus, disabled-item navigation, owner-aware Tab exit, modal isolation, shared anatomy, mixed state, composition, and geometry.

0.3.4

  • Inherited corrected modal Menu scroll-lock compensation for documents that preserve their scrollbar gutter.

0.3.1

  • Inherited reliable Menu exit-presence cleanup for closed ContextMenu and submenu Content under global motion CSS.

0.2.0

  • Fixed Trigger so custom data-slot values override the default context-menu-trigger slot.
  • Inherited fixed submenu keyboard behavior under Direction.Provider dir="rtl" so ArrowLeft opens submenus, ArrowRight closes submenus, and submenu placement mirrors to the left side.
  • Fixed Trigger so its documented asChild and render composition props are implemented while preserving context-menu behavior.
  • Fixed Content so refs forward to the underlying shared menu content element.
  • Fixed pointer-open behavior so right-click opens without pre-highlighting the first item; keyboard context-menu opens still seed the first highlight.
  • Inherited the shared Menu typeahead behavior so a single-character search cycles from the current matching item while multi-character buffers still match exact prefixes.
  • Added shared dismissable layer Escape handling so ContextMenu closes before parent overlays when nested inside Dialog, Drawer, Modal, or Popover.
  • Registered shared Menu content with parent modal focus scopes so ContextMenu can remain a valid focus target inside Dialog, Drawer, and other modal primitives.
  • Added shared menu item parts to the ContextMenu namespace object.

0.1.0

  • Initial Atom release.