Knit UI
GitHub

Usage

Menu.Target wraps the trigger, Menu.Dropdown holds the items. The target keeps its own props — the menu only adds the press handling.

import { Button, Menu } from "@knitui/components";

<Menu>
  <Menu.Target>
    <Button>Actions</Button>
  </Menu.Target>

  <Menu.Dropdown>
    <Menu.Label>Document</Menu.Label>
    <Menu.Item onPress={rename}>Rename</Menu.Item>
    <Menu.Item onPress={duplicate}>Duplicate</Menu.Item>
    <Menu.Divider />
    <Menu.Item theme="red" onPress={remove}>
      Delete
    </Menu.Item>
  </Menu.Dropdown>
</Menu>;

Uncontrolled by default. Pass opened + onChange to control it, or use defaultOpened for the initial state only.

Items take leftSection / rightSection for icons and shortcut hints. A menu is for actions; if the dropdown is for choosing a value, you want a select.

Positioning goes through the kit's own floating engine: position, offset and the arrow* props behave the same as on Popover.

When to use something else

  • Choosing a value from a list → Select or Combobox
  • Rich anchored content rather than a list of commands → Popover
  • A label that appears on hover → Tooltip

Examples

Controlled

Fully controlled open state driven by React.useState — demonstrates the Trigger pattern.

Loading example…

ControlledSourceStorybook

With Labels And Divider

Labels and a divider group related items into named sections.

Loading example…

With Labels And DividerSourceStorybook

With Sections

Left and right sections hold icons or badges on either side of the label.

Loading example…

With SectionsSourceStorybook

Disabled Items

Disabled items are visually muted and do not respond to press events.

Loading example…

Disabled ItemsSourceStorybook

Disabled Menu

The whole menu can be disabled — the dropdown never renders.

Loading example…

Disabled MenuSourceStorybook

With Arrow

The arrow variant draws a pointer from the dropdown toward the target.

Loading example…

With ArrowSourceStorybook

Styles

Per-slot styles targets individual parts — here the label, item, and itemLabel.

Loading example…

StylesSourceStorybook

Shadows

All five shadow levels side by side to compare dropdown elevation. Each menu is always open and rendered in its own contained relative wrapper (withinPortal={false}) so the dropdowns sit inline and don't overlap.

Loading example…

ShadowsSourceStorybook

Hover Trigger

Hover trigger opens the menu on pointer enter and closes on pointer leave.

Loading example…

Hover TriggerSourceStorybook

Props

PropTypeDefaultDescription
arrowOffsetnumber | undefined5Arrow distance from the start/end edge when `arrowPosition="side"`.
arrowPositionArrowPosition | undefined'side'Arrow alignment: centered, or pinned toward an aligned placement.
arrowRadiusnumber | undefined0Corner radius of the arrow's outward corner in px.
arrowSizenumber | undefined7Arrow square edge length in px.
childrenReactNode—`Menu.Target` + `Menu.Dropdown`.
closeDelaynumber | undefined100Close delay in ms for hover triggers.
closeOnClickOutsideboolean | undefinedtrueClose on outside press (web).
closeOnEscapeboolean | undefinedtrueClose on `Escape` (web).
closeOnItemClickboolean | undefinedtrueClose the menu when an item is pressed.
defaultOpenedboolean | undefinedfalseUncontrolled initial opened state.
disabled systemboolean | undefinedfalseSkip rendering the dropdown entirely.
keepMountedboolean | undefinedfalseKeep the dropdown mounted while closed.
loopboolean | undefinedtrueWrap Arrow-key focus at the ends of the item list (web).
menuItemTabIndex0 | -1 | undefined-1`tabIndex` set on every item. Leave at `-1`: items are reached with the Arrow keys (roving focus), which is the ARIA menu pattern, and `-1` keeps them programmatically focusable without putting every item in the page's tab order. `0` additionally makes each item a Tab stop — occasionally wanted for a menu used as a plain link list.
offsetnumber | undefined8Gap between target and dropdown in px.
onChange((opened: boolean) => void) | undefined—Called when the opened state changes.
onClose(() => void) | undefined—Called when the menu closes.
onOpen(() => void) | undefined—Called when the menu opens.
openDelaynumber | undefined0Open delay in ms for hover triggers.
openedboolean | undefined—Controlled opened state.
positionPlacement | undefined
top · top-start · top-end · bottom · bottom-start · bottom-end · left · left-start · left-end · right · right-start · right-end
'bottom-start'Dropdown placement relative to the target.
radius systemRadiusTokens | undefined—Dropdown border radius.
returnFocusboolean | undefined—Return focus to the trigger when the menu closes (web). Defaults to `true` for click triggers and `false` for hover triggers: a menu opened by pointing at something never took focus in the first place, so "returning" it would move focus somewhere the user never put it.
shadow system"xs" | "sm" | "md" | "lg" | "xl" | "xxs" | "xxl" | undefined
xs · sm · md · lg · xl
'md'Dropdown shadow scale.
styles systemSlotStyles<MenuStyles> | undefined—Per-slot style sugar — props spread onto the matching styled part.
theme systemThemeName | null | undefined—Accent theme applied to the menu subtree (trigger + dropdown).
trapFocusboolean | undefined—Trap Tab within the open dropdown (web). Defaults to `true` for click triggers, `false` for hover triggers — see `returnFocus`.
triggerMenuTrigger | undefined
click · hover · click-hover
'click'Event that opens the menu.
widthPopoverWidth | undefined'max-content'Dropdown width; `'target'` matches the target.
withArrowboolean | undefinedfalseRender an arrow pointing at the target.
withinPortalboolean | undefinedtrueRender the dropdown in a portal.
zIndexnumber | undefined300Dropdown `z-index`.

Style slots

Every part of Menu can be styled through the styles prop. Explicit props on the component always win over slot styles.

SlotTargets
dividerProps spread onto every divider (Menu.Divider).
dropdownProps spread onto the dropdown frame (Menu.Dropdown).
itemProps spread onto every item frame (Menu.Item).
itemLabelProps spread onto every item's label text node.
itemSectionProps spread onto every item's left/right section wrappers.
labelProps spread onto every label (Menu.Label).

Accessibility

Menu implements the ARIA menu-button pattern. The dropdown is a role="menu", each item a role="menuitem", and focus is managed: opening moves focus to the first item, Tab is trapped inside the dropdown, and closing returns focus to the trigger.

Items are reached with the arrow keys — roving focus — so they stay out of the page's tab order rather than adding one tab stop per menu entry.

KeyAction
↓ / ↑ on the triggerOpen the menu, focusing the first / last item
Enter / Space on the triggerOpen the menu
↓ / ↑ in the menuNext / previous item, wrapping (loop)
Home / EndFirst / last item
Enter / SpaceActivate the focused item
EscClose and return focus to the trigger

Disabled items are skipped by arrow navigation and are never focused.

Hover menus deliberately do not manage focus. With trigger="hover" or "click-hover", opening the menu does not take focus and closing it does not move focus to the trigger — a menu you opened by pointing at something never had focus, so "returning" it would move the caret somewhere you never put it. Keyboard users reach a hover menu the same way as any other: by pressing or arrowing on the trigger. Override with trapFocus / returnFocus if your case differs.

  • loop={false} stops arrow focus wrapping at the ends.
  • menuItemTabIndex={0} additionally makes every item a Tab stop. Rarely wanted — the arrow keys are the menu pattern — but useful if you are using Menu as a plain list of links.
  • Give an icon-only trigger an accessible name (aria-label on the target, or use ActionIcon, which is built for it).

Edit this page on GitHub