@geee-be/react-quick-menu
v0.1.5
Published
Headless, accessible React hierarchical quick-menu component
Maintainers
Readme
@geee-be/react-quick-menu
A headless, accessible, data-driven hierarchical popup menu for React. Items are navigated by typing their unique key sequences — no mouse required.
Features
- Keyboard-driven: type a key sequence to filter and select items
- Hierarchical: items can have children; navigation stacks parent items into a breadcrumb
- Accessible: built on
@radix-ui/react-dialog— modal dialog semantics, focus trap, ARIA attributes - Global hotkey: open the menu with any hotkey via
@tanstack/react-hotkeys - Headless: zero styles — render items however you like via render-prop children
- Fully typed: strict TypeScript throughout
Installation
pnpm add @geee-be/react-quick-menuKey Sequence Logic
Given items with these keySequence values:
| Item | keySequence | Minimum unique prefix |
|-------|------------|----------------------|
| Hello | hello | h |
| There | there | th |
| Food | food | f |
| Truck | truck | tr |
Typing t filters to There and Truck. Typing r then selects Truck (matching unique prefix tr).
If the typed characters match no items, an audible beep plays and the character is discarded. Backspace removes the last typed character. Escape closes the menu.
When a parent item (one with children) is selected, its children are shown and the parent appears in the breadcrumb. The selected item IDs from root to the chosen leaf are returned via onSelect.
Usage
import * as QuickMenu from '@geee-be/react-quick-menu';
const items: QuickMenu.QuickMenuItem[] = [
{ id: 'file', label: 'File', keySequence: 'file', children: [
{ id: 'file-save', label: 'Save', keySequence: 'save' },
{ id: 'file-open', label: 'Open', keySequence: 'open' },
]},
{ id: 'help', label: 'Help', keySequence: 'help' },
];
function App() {
const handleSelect = (ids: string[]) => {
console.log('Selected path:', ids);
// e.g. ['file', 'file-save']
};
return (
<QuickMenu.Root
items={items}
onSelect={handleSelect}
hotkey="/"
stackPosition="above"
>
<QuickMenu.Trigger>Open Menu</QuickMenu.Trigger>
<QuickMenu.Content title="Quick Menu">
{/* Breadcrumb of selected ancestors */}
<QuickMenu.Breadcrumb
render={(item, uniquePrefix) => (
<span>
<strong>{item.label}</strong>
<kbd>{uniquePrefix}</kbd>
</span>
)}
/>
{/* List of current items */}
<QuickMenu.ItemList>
{(item, uniquePrefix, typed) => (
<div>
{/* Highlight the typed portion of the prefix */}
<span style={{ fontWeight: 'bold' }}>{uniquePrefix.slice(0, typed.length)}</span>
<span>{uniquePrefix.slice(typed.length)}</span>
<span>{item.label}</span>
{item.children ? <span>▶</span> : null}
</div>
)}
</QuickMenu.ItemList>
</QuickMenu.Content>
</QuickMenu.Root>
);
}API
QuickMenuItem
interface QuickMenuItem {
id: string;
label: React.ReactNode;
keySequence: string; // alphanumeric string used for keyboard navigation
children?: QuickMenuItem[]; // nested items
/**
* Called when this item is selected. Optional for all items.
* For leaf items, fires before the menu closes and before `onSelect`.
* For parent items (with children), fires before navigating into the children.
*/
onClick?: () => void;
}<Root>
| Prop | Type | Default | Description |
|-----------------|------------------------------|-----------|-------------|
| items | QuickMenuItem[] | — | Top-level menu items |
| onSelect | (ids: string[]) => void | — | Called with the full id path when a leaf is selected |
| stackPosition | 'above' \| 'below' | 'above' | Where to render the breadcrumb relative to the item list |
| hotkey | string | — | Global hotkey to open the menu (e.g. "/" or "Mod+K") |
| closeOnRepeatHotkey | boolean | false | When true, pressing hotkey while the menu is open closes it |
| open | boolean | — | Controlled open state |
| onOpenChange | (open: boolean) => void | — | Called when open state changes |
| defaultOpen | boolean | false | Initial open state (uncontrolled) |
<Trigger>
Renders a <button> by default. Supports asChild to merge props onto a child element.
| Prop | Type | Default | Description |
|-----------|-----------|---------|-------------|
| asChild | boolean | false | Merge props onto child element |
| hotkey | string | — | Per-trigger hotkey (overrides Root's hotkey) |
<Content>
Renders the modal panel via @radix-ui/react-dialog. Keyboard events are captured here.
| Prop | Type | Default | Description |
|----------------|-------------|---------------|-------------|
| title | string | 'Quick Menu'| Accessible title (visually hidden) |
| description | string | — | Accessible description (visually hidden) |
| contentProps | object | — | Props forwarded to Dialog.Content |
| overlayProps | object | — | Props forwarded to Dialog.Overlay |
<ItemList>
Renders the filtered list of items at the current navigation depth.
<QuickMenu.ItemList>
{(item: QuickMenuItem, uniquePrefix: string, typed: string) => (
<YourItemComponent item={item} prefix={uniquePrefix} typed={typed} />
)}
</QuickMenu.ItemList><Breadcrumb>
Renders an <ol> of ancestor items in the current navigation stack.
| Prop | Type | Description |
|----------|------|-------------|
| render | (item: QuickMenuItem, uniquePrefix: string) => React.ReactNode | Custom item renderer |
Returns null when at the root level (no ancestors).
useQuickMenuContext()
Access the full menu context from any component inside <QuickMenu.Root>.
const {
filteredItems, // items matching the current typed sequence
currentItems, // all items at the current depth (unfiltered)
uniquePrefixes, // Map<id, minimumUniquePrefix>
typed, // characters typed so far
stack, // ancestor items navigated into
open, // current open state
stackPosition, // 'above' | 'below'
onClose, // close the menu
handleKeyDown, // bind to onKeyDown for custom content elements
} = useQuickMenuContext();Keyboard Shortcuts
| Key | Action |
|-------------|--------|
| Alphanumeric | Append to filter / navigate |
| Backspace | Remove last typed character; if nothing is typed, pop back to the parent level |
| Escape | Close menu |
Accessibility
- Built on
@radix-ui/react-dialog— modal dialog with focus trap,role="dialog",aria-modal="true" - Items rendered in a
role="list"/role="listitem"structure - Breadcrumb rendered as an
<ol>witharia-label="Navigation path" - Accessible title required via
<Content title="...">(visually hidden by default) - Invalid keystrokes produce an audible beep via the Web Audio API
License
MIT
