Skip to content

Context Menu

Open actions for an item with right-click, long press or Shift+F10.

import { ContextMenu, ContextMenuContent, ContextMenuTrigger } from '@youj/react'

Installation & usage

npm i @youj/react

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Use a context menu as a shortcut to actions people can already reach another way: a file's rename and delete, a canvas's paste and zoom. It's easy to miss, and it's awkward on touch and for assistive technology, so give the same actions a visible control too. The file rows above pair each context menu with a ⋯ Dropdown Menu built from the same rows. Don't make it the only way to do anything.

import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger } from '@youj/react'

export function FileRow({ file }: { file: File }) {
  return (
    <ContextMenu>
      <ContextMenuTrigger className="flex items-center gap-3">
        <a href={file.url}>{file.name}</a>
      </ContextMenuTrigger>
      <ContextMenuContent>
        <ContextMenuItem onClick={() => rename(file)}>Rename</ContextMenuItem>
        <ContextMenuSeparator />
        <ContextMenuItem variant="danger" onClick={() => remove(file)}>
          Delete
        </ContextMenuItem>
      </ContextMenuContent>
    </ContextMenu>
  )
}

ContextMenuTrigger is the area that opens the menu. Put something focusable in it, like the file's link in the Demo story, so the keyboard can open the menu too. The rows are the Dropdown Menu's under other names (ContextMenuItem, ContextMenuCheckboxItem, ContextMenuSub…), so one set of rows can serve both menus, and they look and move the same.

Reference

Propscontainer, presentation (2)

Props

ContextMenuContent

ContextMenuContent props
PropType
containerHTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null>

Where the portal renders. Default: document.body.

–
presentationOverlayPresentation
'auto''sheet''popover'

auto (the default) is a popup at the pointer and a bottom sheet on touch; force either one.

default 'auto'

'auto'

Base UI places the popup at the pointer unless you pass side, align or their offsets. ContextMenu takes the root props (open, defaultOpen, onOpenChange, disabled). The rows take the same props as the Dropdown Menu's: variant and inset on items, checked on checkbox items, value on radio groups.

TouchOn a touch device, pressing and holding the trigger for half a second…

Touch

On a touch device, pressing and holding the trigger for half a second opens the menu as the same action sheet DropdownMenu uses: it rises from the bottom of the screen with a grab handle, rows are 48px and shortcut hints are hidden. Pull it down, tap outside it or press Escape to close it. Base UI's trigger handles the long press itself, because iOS Safari never sends a contextmenu event. If the finger moves first, the press is canceled, so a list of triggers still scrolls. It also turns off the browser's own long-press callout and text selection on the trigger.

A long press is easy to miss on a phone, so make sure the same actions are reachable from a visible button too.

presentation on ContextMenuContent chooses: auto (the default) is a menu at the pointer under a mouse or trackpad and a sheet on touch, sheet always uses the sheet, and popover keeps the menu at the finger on touch too.

MotionOpen, close, highlight… · reduced motion

Motion

Motion tokens
MomentTokens
Openscale .96 → 1 and fade, out of the point it opened at (yj-pop)
duration.fastease.enter

140ms · cubic-bezier(0.16, 1, 0.3, 1)

140mscubic-bezier(0.16, 1, 0.3, 1)
Close
duration.instantease.exit

80ms · cubic-bezier(0.4, 0, 1, 1)

80mscubic-bezier(0.4, 0, 1, 1)
Highlightone SlidingIndicator per popup glides between rows
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Checkdraws in when a row turns on
duration.slowease.emphasized

340ms · cubic-bezier(0.6, 0, 0.1, 1)

340mscubic-bezier(0.6, 0, 0.1, 1)
Keyboard open, Escape closeBase UI sets data-instant
no token

0ms

0ms

Reduced motion

The menu appears at once, the highlight jumps from row to row, and checks appear already drawn.

Because the popup's transform-origin is the pointer's position, the menu grows out of the spot you clicked rather than out of a corner.

Reduced motion

The menu appears in place at the pointer and closes at once, and the highlight moves without gliding. Everything else behaves the same.

AccessibilityKeyboard, focus

Accessibility

  • The menu is a menu with menuitem, menuitemcheckbox and menuitemradio rows, exactly like the dropdown menu, with the same keyboard support once open.
  • Shift+F10 and the Menu key open it from a focused element inside the trigger, just under that element. Browsers only send those keys as a contextmenu event on some platforms, so the trigger sends one itself.
  • On touch, a long press opens it.
  • Focus moves into the menu and returns to where it was when the menu closes.
KeyAction
Shift+F10 / MenuOpens the menu from the focused element inside the trigger.
↑ / ↓, Home / EndMove between rows. Letters jump by typeahead.
Enter / SpaceRuns the row, or toggles a checkbox or radio row.
→ / ←Opens or closes a submenu.
EscapeCloses the menu.