useMenuPress@astryxdesign/core v0.7.6 · useMenuPress

Usage

The press model of macOS and iOS menus, for any pointer: the row under the pointer when it is RELEASED is the row that acts, the highlight follows the pointer while it is held, a mouse opens the menu on press and can drag straight into it, and a finger held on the trigger opens it with the finger still down. The pointer is tracked at document level by pointerId, so a finger that slid off the row it landed on is still followed; the click the browser reports at the end of a touch, aimed at the row where the touch began, is swallowed so nothing acts twice. DropdownMenu, ContextMenu, DropdownMenuSubMenu, Selector and the menu bottom sheet already mount it; reach for it directly only when building a menu-like surface of your own.

ts
import {useMenuPress} from '@astryxdesign/core/hooks'

Best practices

GuidancePractices
Do

Pass a selector for ENABLED rows only, so a disabled row or a divider under the pointer clears the highlight instead of lighting up.

Do

Let the default highlight move focus in a menu; supply onHighlight only for a listbox that must keep focus on its combobox and highlight through aria-activedescendant.

Do

Declare touch-action on the menu root: pinch-zoom when its rows fit, pan-y pinch-zoom when it scrolls, so the browser — not the hook — decides when a finger is scrolling, and a two-finger pinch still zooms the page.

Don't

Act on a row from its own pointerdown or pointerup handler as well; the hook already activates the row under the release, and a second path acts twice.

Don't

Read a row click with detail 0 as a keyboard activation; the hook dispatches its pointer activation with detail 0 too. Use isMenuPressActivation() to tell them apart.

Do

Use the ref form, useMenuPress(menuRef, {triggerRef, …}): it listens on the menu and the trigger itself, so nothing is spread. Its press, the trigger's included, exists only while the menu element is mounted, so a menu that opens on a press of its trigger keeps its element mounted while closed. The options form, useMenuPress({menuRef, …}), is deprecated (DEP-0019) and is removed in 0.8.0; run astryx upgrade to move calls to the ref form.

Parameters

ParamTypeDescription
menuRefrequired
RefObject<HTMLElement | null>

The menu or listbox root — the surface whose rows a press picks from. The hook creates a press for the element behind the ref at commit, and a new one only for a new menu or document; any other change of the trigger, an option or a callback reaches the press through update() at commit. It destroys the press at unmount. While the element is unmounted there is no press, not even on the trigger.

optionsrequired

Configuration object.

options.itemSelectorrequired
string

Selector matching the ENABLED rows. A pointer over anything else inside the menu (a divider, a heading, a disabled row) highlights nothing.

options.triggerRef
RefObject<HTMLElement | null>

The control that opens the menu, when a press may start there. It gets the press only while the menu element is mounted, so a menu rendered only while open opens on the trigger's click alone.

options.onTriggerPress
(pointerType:
) => boolean

A mouse pressed the trigger, or a finger rested on it for the long-press delay: open the menu under the held pointer and return whether it opened. Return false when the press closed an open menu instead.

options.onHighlight
(row: HTMLElement | null) => void

Move the highlight; null clears it. Defaults to moving DOM focus with preventScroll onto the row, and onto the menu root when there is no row; after a pointer press that focus does not match :focus-visible. A picker that highlights through aria-activedescendant supplies its own.

options.onActivate
(row: HTMLElement, release: PointerEvent) => void

Act on the row under the release. Defaults to dispatching a click on the row that carries the release button and modifier keys.

options.onDismiss
() => void

A MOUSE was released outside the menu with nothing acting: close it. A finger released outside leaves the menu open, so this is never called then.

options.getScroller
() => HTMLElement | null

The element to scroll while a tracked pointer rests near its top or bottom edge. Defaults to the menu root when it overflows.

options.longPressDelayMs
number (default: 500)

How long a finger must rest on the trigger before the menu opens under it.

options.isEnabled
boolean (default: true)

Whether the model is live.

options.startBehavior
'menu' | 'items' (default: 'menu')

Where a press may start. 'items' takes only a press that lands on an item, with the main button, so other content in the menu keeps its own clicks and a Ctrl, Cmd or Shift mouse press on a link item opens it as the browser does.

Returns

FieldTypeDescription
menuProps{onPointerDown; "data-astryx-menu-press": ""}

Deprecated options form only, useMenuPress({menuRef, …}): spread onto the menu root. Claims presses that begin inside it and marks the root as carrying the press model.

triggerProps{onPointerDown; onContextMenu}

Deprecated options form only: spread onto the trigger. A mouse press opens the menu at once; a finger held for the delay opens it with the finger still down.

isTriggerClickFromPress() => boolean

Whether the click reaching the trigger belongs to the gesture that just pressed it. That press already opened or closed the menu, so the click must neither toggle nor reopen.

cancel() => void

End the gesture in flight with nothing acting, for when the menu closes under it. Nothing is spread: the press listens on the menu and its trigger itself and marks the menu root with data-astryx-menu-press. Without React, createMenuPress(menu, options) takes the same options with the trigger as an element (trigger) and returns {isTriggerClickFromPress, cancel, update, handleTriggerPointerDown, handleTriggerContextMenu, destroy}; destroy removes every listener it added.

handleTriggerPointerDown(event: PointerEvent | React.PointerEvent) => void

Ref form only: hands the press a trigger press-down that a handler forwards, such as a trigger that runs its own handler first. The press takes each event once.

handleTriggerContextMenu(event: MouseEvent | React.MouseEvent) => void

Ref form only: the same for the trigger's contextmenu, which a held finger raises as it opens the menu.

Use with shadcn

Already using the shadcn registry workflow? Install the real Astryx package and a local public re-export. Component implementation source stays in Astryx.

This install URL expires with the draft preview.
bash
npx shadcn@4.19.0 add https://astryx-ic4yqd5on-fbopensource.vercel.app/shadcn/hooks/use-menu-press.json