react-menu

An accessible, keyboard-friendly React menu library for dropdowns, context menus, and nested submenus.

Library
npm
v4.5.2
1,217stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
71/100Good
Development Activity68
Maintenance68
Community48
Maturity60
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
76/100Good
Architecture85
Code Quality82
Innovation62
Learning Curve75

@szhsin/react-menu is a React component library for building menus: dropdown menus, context menus, nested submenus, and menu-driven UI patterns like checkbox and radio menu items. It implements the WAI-ARIA menu authoring practices pattern directly, so keyboard navigation (arrow keys, Home/End, type-ahead), focus management, and screen-reader semantics come built in rather than being left to the consuming application to reimplement.

The library ships unstyled, exposing composable components (Menu, MenuItem, SubMenu, MenuButton, ControlledMenu, MenuRadioGroup, and more) plus hooks (useMenuState, useClick, useHover) so teams can either use the batteries-included Menu wrapper or drop down to ControlledMenu for fully custom state control, portaling, and positioning. It supports concurrent-rendering-safe React 18+ usage and server-side rendering via an initialMounted escape hatch, addressing a common pain point for menu libraries that assume client-only mounting.

Built for teams that need menu/dropdown interaction patterns without hand-rolling ARIA compliance and focus trapping, or without pulling in a full component-library dependency just for a menu.

What You Get

  • A Menu component paired with MenuButton for standard dropdown menus, plus ControlledMenu for fully custom-controlled menu state and positioning
  • SubMenu support with unlimited nesting depth for multi-level dropdown and context menus
  • MenuItem, FocusableItem, MenuRadioGroup, and checkbox/radio menu item types with WAI-ARIA-correct roles and keyboard behavior
  • Flexible positioning options (direction, align, position, boundingBoxRef) that keep menus within the viewport, plus optional portal rendering to document.body
  • Built-in transition/animation support via the react-transition-state dependency, with state values (opening, open, closing, closed) exposed for custom styling
  • Server-side rendering support through an initialMounted prop that renders menu content before first open

Common Use Cases

  • Adding a dropdown navigation or action menu to a toolbar or button without hand-building keyboard and focus handling
  • Building a right-click context menu for a canvas, table row, or file-manager-style UI
  • Implementing settings or filter menus that use checkbox and radio menu items instead of a separate dropdown/select component
  • Constructing deeply nested menu systems, such as an application menu bar or a multi-level command palette
  • Adding accessible menu interaction to a design system or component library that otherwise ships unstyled primitives

Under The Hood

Architecture The library is organized as a layered component tree: Menu (src/components/Menu.js) is a thin convenience wrapper that owns button-cloning and keyboard-open behavior, delegating actual rendering to ControlledMenu (src/components/ControlledMenu.js), which sets up React context providers (SettingsContext, EventHandlersContext) consumed by MenuList and, transitively, every MenuItem/SubMenu descendant. State transitions (opening/open/closing/closed) are delegated to the useMenuState/useMenuStateAndFocus hooks built on top of the external react-transition-state package rather than reimplemented locally. Positioning is isolated into its own positionUtils/ module (positionMenu.js dispatching to placeLeftorRight.js/placeToporBottom.js), a clean separation of layout math from component logic. Portal rendering (createPortal) is handled conditionally in ControlledMenu, and MenuButton/instanceRef plumbing via useImperativeHandle lets parent code imperatively open/close a menu. The design cleanly separates concerns (state, positioning, event handling, rendering) and would tolerate a positioning-algorithm swap without touching component logic.

Tech Stack A React 16.14+ / React 18+ peer-dependency library written in plain JavaScript (not TypeScript), with hand-authored .d.ts type definitions maintained separately under types/. The only runtime dependency is react-transition-state (the author’s own companion package) for transition state management. The build pipeline uses Rollup (rollup.config.mjs) to produce dual CJS/ESM output (dist/cjs, dist/esm), Babel for JSX/preset-env transpilation, and Sass for compiling the optional CSS theme (src/styles) shipped separately from the JS bundle so consumers can opt in to default styling. TypeScript is used only to type-check the hand-written declaration files (types/tsconfig.json), not to compile the source.

Code Quality The project has a substantial Jest test suite (src/__tests__/) covering Menu, ControlledMenu, SubMenu, MenuItem, MenuGroup, useMenuState, styling, and a dedicated SSR test file, using @testing-library/react and jest-axe for accessibility assertions rather than shallow rendering. ESLint (flat config) is wired with eslint-plugin-react-hooks and a hooks-addons plugin plus Prettier for formatting, and lint/pret checks run as part of the build script. Error handling is minimal but intentional (e.g. Menu throws explicitly if menuButton is missing rather than failing silently). Naming is consistent and the codebase avoids any-typed escape hatches in its declaration files.

What Makes It Unique Most dropdown/menu libraries in the React ecosystem either come bundled inside a full component-library (pulling in unrelated primitives) or leave WAI-ARIA menu semantics and keyboard navigation to the consumer. This library’s narrow focus — just menus, unstyled, with the ARIA authoring-practices pattern implemented directly and a genuine SSR story via initialMounted — is the differentiator, not a novel technical mechanism. The react-transition-state-driven four-state lifecycle (rather than a simple open/closed boolean) is a deliberate, if not groundbreaking, choice that gives consumers hooks for enter/exit animations without extra state tracking of their own.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search