hono-preact
Overview
Quick Start
The Route Table
Layouts & Nesting
Adding Pages
Active Links
Server Loaders
Loading States
Reloading Data
Prefetching
Streaming
Live Loaders
Realtime Channels
Server Actions
Validation
Optimistic UI
View Transitions
Middleware
CSRF Protection
CLI
Vite Config
Project Structure
Composing Hono Middleware
WebSockets
Rooms & Presence
renderPage
Link Prefetch
Build & Deploy
Overview
Dialog
Popover
Tooltip
Menu
Context Menu
Select
Combobox
Toast
renderElement
useControllableState
mergeRefs
useListNavigation
useTypeahead
useListboxSelection
usePosition
usePositioner
useDismiss
useFocusReturn
useSafeArea
usePresence

Menu#

A button-triggered dropdown of commands: a list of actions, toggles, and single-select choices that opens from a trigger and closes when you pick something or dismiss it. Positioning runs on Floating UI; the popup renders in place and promotes to the browser top layer using the Popover API. It ships unstyled: style it through the data-state, data-highlighted, data-disabled, data-side, and data-align contract.

The popup uses the role="menu" pattern, which is for application and command menus (a list of actions to perform), not for site navigation. For a set of links between pages, use a plain list of anchors so each link is a real <a> in the tab order; reserve Menu for commands.

Demo#

Usage#

Menu.Item activates and closes the menu on click or Enter; call event.preventDefault() in its onSelect to keep the menu open after a choice. Menu.CheckboxItem and Menu.RadioItem carry their own checked state (each takes a controlled checked/value pair or an uncontrolled defaultChecked/ defaultValue). A Menu.SubmenuRoot nests another menu off a trigger row.

Styling#

Parts expose data-state (open/closed on the popup and submenu trigger). Checkbox and radio items expose data-checked (present when checked). Items expose data-highlighted while active and data-disabled when disabled; the Positioner and Arrow expose data-side and data-align. The Positioner is the fixed-positioned wrapper, so size, z-index, and the entry animation go on the Popup inside it. The demo above uses the styles below; copy a starting point in either flavor:

.docs-menu-positioner {
  z-index: 50;
}
.docs-menu {
  box-sizing: border-box;
  min-width: 12rem;
  padding: 0.25rem;
  border: 1px solid #e4e4e7;
  border-radius: 0.625rem;
  background: #fff;
  color: #18181b;
  box-shadow:
    0 10px 15px -3px rgb(0 0 0 / 0.25),
    0 4px 6px -4px rgb(0 0 0 / 0.25);
  outline: none;
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity 120ms ease,
    transform 120ms ease;
}
@starting-style {
  .docs-menu[data-state='open'] {
    opacity: 0;
    transform: translateY(-4px);
  }
}
.docs-menu[data-state='closed'] {
  opacity: 0;
  transform: translateY(-4px);
}
/* Items are roving-tabindex rows; the active row is highlighted via the
   data-attribute the component sets on hover and arrow navigation. */
.docs-menu__item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.875rem;
  padding: 0.4rem 0.6rem;
  border-radius: 0.375rem;
  cursor: pointer;
  outline: none;
}
.docs-menu__item[data-highlighted] {
  background: #18181b;
  color: #fafafa;
}
.docs-menu__item[data-disabled] {
  color: #a1a1aa;
  pointer-events: none;
}
.docs-menu__separator {
  height: 1px;
  margin: 0.25rem 0.3rem;
  background: #e4e4e7;
}
.docs-menu__label {
  padding: 0.25rem 0.6rem;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: #71717a;
}
@media (prefers-color-scheme: dark) {
  .docs-menu {
    border-color: #27272a;
    background: #18181b;
    color: #fafafa;
  }
  .docs-menu__item[data-highlighted] {
    background: #fafafa;
    color: #18181b;
  }
  .docs-menu__separator {
    background: #27272a;
  }
}
@media (prefers-reduced-motion: reduce) {
  .docs-menu {
    transition: none;
  }
}

Keyboard#

KeysWhenAction
Enter / Space / / Trigger, closedOpen the menu. focuses the first item, the last.
/ Menu openMove to the next / previous item, wrapping at the ends.
Home / EndMenu openMove to the first / last item.
printable charactersMenu openTypeahead: focus the next item whose label starts with the typed text.
Enter / SpaceOn an itemActivate the highlighted item.
EscapeMenu openClose the menu and return focus to the trigger.
TabMenu openClose the menu, then move focus to the next element.
On a submenu triggerOpen the submenu and focus its first item.
In a submenuClose the submenu and return focus to its trigger.
EscapeIn a submenuClose the innermost open menu first.

Set loop={false} on Menu.Root to stop arrow navigation from wrapping, and typeahead={false} to disable type-to-focus.

Data attributes#

AttributeOnValues
data-stateTrigger, Popup, SubmenuTriggeropen | closed
data-checkedCheckboxItem, RadioItempresent when checked
data-highlightedItem, CheckboxItem, RadioItem, SubmenuTriggerpresent while the item is the active row
data-disabledItem, CheckboxItem, RadioItempresent when the item is disabled
data-sidePositioner, Arrowtop | right | bottom | left (resolved)
data-alignPositionerstart | center | end (resolved)

API reference#

Every part accepts a render prop for composition (see renderElement) and forwards unknown props to the element it renders. Items pass their { disabled, highlighted } (plus checked on checkbox / radio items) state to a render function.

Provides open state, ids, refs, navigation config, and placement to the parts. Renders only its children.

PropTypeDefaultDescription
openboolean-Controlled open state. Pair with onOpenChange.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => void-Called when the menu requests an open or close.
side'top'|'right'|'bottom'|'left''bottom'Preferred side of the trigger to place the popup.
align'start'|'center'|'end''start'Alignment along that side.
offsetnumber8Gap in pixels between the trigger and the popup.
loopbooleantrueWrap arrow navigation at the first / last item.
typeaheadbooleantrueFocus items by typing their leading characters.
childrenComponentChildren-The trigger and positioner.

Toggles the menu on click and anchors it. Default element <button type="button">.

PropTypeDefaultDescription
renderRenderProp<{ open: boolean }>-Compose or replace the element.
childrenComponentChildren-Trigger label.
...propsJSX.HTMLAttributes<HTMLButtonElement>-Forwarded to the element; a passed onClick runs before the menu toggles.

Sets aria-haspopup="menu", aria-expanded, id, data-state, and aria-controls (only while open, since the popup is mounted on open).

The fixed-positioned wrapper that Floating UI drives. Renders nothing until the menu is open (mount on open). Default element <div>.

PropTypeDefaultDescription
renderRenderProp<{ side: Side; align: Align }>-Compose or replace the element.
childrenComponentChildren-The popup (and optional arrow).
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element. Style positioning via class.

Sets position: fixed and the resolved data-side / data-align. The element is promoted to the top layer via the Popover API, so it escapes ancestor clipping.

The menu surface and navigation root. Default element <div> with role="menu".

PropTypeDefaultDescription
renderRenderProp<{ open: boolean }>-Compose or replace the element.
aria-labelstring-Accessible name. Defaults to the trigger's label.
childrenComponentChildren-Items, separators, groups, and submenus.
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element.

Sets role="menu", id, tabindex="-1", aria-orientation="vertical", data-state, and aria-labelledby (the trigger) or aria-label. Owns the keyboard navigation, typeahead, Escape / outside-press dismissal, and focus move-in / return.

A command row. Default element <div> with role="menuitem".

PropTypeDefaultDescription
renderRenderProp<{ disabled: boolean; highlighted: boolean }>-Compose or replace the element.
disabledbooleanfalseSkip the item in navigation and ignore activation.
onSelect(event: Event) => void-Called on activation. Call event.preventDefault() to keep the menu open.
childrenComponentChildren-Item content.
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element.

A toggle row that holds its own checked state. Default element <div> with role="menuitemcheckbox".

PropTypeDefaultDescription
renderRenderProp<{ checked: boolean; disabled: boolean; highlighted: boolean }>-Compose or replace the element.
checkedboolean-Controlled checked state. Pair with onCheckedChange.
defaultCheckedbooleanfalseInitial checked state when uncontrolled.
onCheckedChange(checked: boolean) => void-Called when the checked state changes.
disabledbooleanfalseSkip the item and ignore activation.
onSelect(event: Event) => void-Called on activation; preventDefault() keeps the menu open.
childrenComponentChildren-Item content (render your own check indicator).
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element.

Sets aria-checked and data-checked (present when checked).

Single-select context for the radio items inside it. Default element <div> with role="group".

PropTypeDefaultDescription
renderRenderProp-Compose or replace the element.
valuestring-Controlled selected value. Pair with onValueChange.
defaultValuestring-Initial selected value when uncontrolled.
onValueChange(value: string) => void-Called when the selection changes.
childrenComponentChildren-The radio items.
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element.

A choice within a Menu.RadioGroup. Default element <div> with role="menuitemradio".

PropTypeDefaultDescription
valuestringrequiredIdentifies the item within its group.
renderRenderProp<{ checked: boolean; disabled: boolean; highlighted: boolean }>-Compose or replace the element.
disabledbooleanfalseSkip the item and ignore activation.
onSelect(event: Event) => void-Called on activation; preventDefault() keeps the menu open.
childrenComponentChildren-Item content (render your own selected indicator).
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element.

Sets aria-checked and data-checked (present when checked).

A horizontal divider between groups of items. Default element <div> with role="separator".

PropTypeDefaultDescription
renderRenderProp-Compose or replace it.
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element.

Wraps related items and labels them with a Menu.GroupLabel. Default element <div> with role="group".

PropTypeDefaultDescription
renderRenderProp-Compose or replace it.
childrenComponentChildren-A Menu.GroupLabel and the items.
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element.

Wires its label to aria-labelledby.

The accessible name for a Menu.Group. Presentational, not focusable. Default element <div>.

PropTypeDefaultDescription
renderRenderProp-Compose or replace it.
childrenComponentChildren-Label text.
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element.

Optional pointer positioned from the Floating UI arrow data. Default element <div>. Place it on the correct edge with CSS keyed on data-side.

PropTypeDefaultDescription
renderRenderProp<{ side: Side }>-Compose or replace the element.
childrenComponentChildren-Optional arrow content.
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element.

Sets data-side and position: absolute with the computed offset.

Provides a nested menu's open state and placement off a trigger row. Must be placed inside a Menu.Popup. Renders only its children.

PropTypeDefaultDescription
openboolean-Controlled open state. Pair with onOpenChange.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => void-Called when the submenu requests an open or close.
side'top'|'right'|'bottom'|'left''right'Preferred side of the trigger to place the submenu.
align'start'|'center'|'end''start'Alignment along that side.
offsetnumber0Gap in pixels between the trigger and the submenu.
openDelaynumber100Hover open delay in ms.
closeDelaynumber300Safe-corridor grace in ms before closing on pointer leave.
childrenComponentChildren-The submenu trigger and positioner.

The row that opens the submenu. It is itself a menuitem in the parent menu. Default element <div> with role="menuitem".

PropTypeDefaultDescription
renderRenderProp<{ open: boolean; highlighted: boolean }>-Compose or replace the element.
disabledbooleanfalseSkip the row and ignore activation.
childrenComponentChildren-Row content.
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element.

Sets aria-haspopup="menu", aria-expanded, and (while open) aria-controls. Opens on hover (after openDelay), , Enter, Space, or click; the safe corridor keeps it open while the pointer travels toward the submenu.

The submenu's fixed-positioned wrapper. Same surface as Menu.Positioner, bound to the submenu. Default element <div>.

PropTypeDefaultDescription
renderRenderProp<{ side: Side; align: Align }>-Compose or replace the element.
childrenComponentChildren-The submenu popup.
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element. Style positioning via class.

The submenu surface. Same surface as Menu.Popup, with wired to close the submenu and return focus to its trigger. Default element <div> with role="menu".

PropTypeDefaultDescription
renderRenderProp<{ open: boolean }>-Compose or replace the element.
aria-labelstring-Accessible name. Defaults to the submenu trigger.
childrenComponentChildren-The nested items.
...propsJSX.HTMLAttributes<HTMLDivElement>-Forwarded to the element.

Primitives#

hono-preact-ui exports the building blocks the parts use, for composing your own components. Several have their own page with examples: usePosition, useDismiss, useFocusReturn, useSafeArea, renderElement, useControllableState, and mergeRefs.

Accessibility#

The menu follows the ARIA menu-button pattern. The trigger carries aria-haspopup="menu", aria-expanded, and (while open) aria-controls; the popup is a role="menu" with aria-orientation="vertical", named by the trigger or an explicit aria-label. Items are menuitem, menuitemcheckbox, and menuitemradio.

  • Both Enter / Space and the arrow keys open the menu from the trigger, so it is reachable without a pointer.
  • Focus moves into the menu when it opens (the first or last item) and returns to the trigger when it closes. The items use a roving tabindex, so only the active item is in the tab order.
  • Escape closes the menu, an outside pointer press closes it, and nested submenus close innermost-first.
  • Use the role="menu" pattern only for commands. For navigation between pages, render a list of links instead, so each destination is a real <a>.