Features
#- Unstyled and lightweight (8kB) React menu components.
- Unlimited levels of submenu
- Supports dropdown, hover, and context menu
- Supports radio and checkbox menu items
- Flexible menu positioning
- Comprehensive keyboard interactions
- Customisable styling
- Level 3 support of React 18 concurrent rendering
- Supports server-side rendering
- Implements WAI-ARIA menu pattern
Install
#Usage
#Each of the following sections includes a live example. They are grouped into related categories.
Menu
#Common usage examples of Menu
, SubMenu
, and MenuItem
.
Basic menu
#The most basic menu consists of several MenuItem
s wrapped in a Menu
, and is controlled by a MenuButton
.
import { Menu, MenuItem, MenuButton } from '@szhsin/react-menu';
import '@szhsin/react-menu/dist/index.css';
import '@szhsin/react-menu/dist/transitions/zoom.css';
export default function Example() {
return (
<Menu menuButton={<MenuButton>Menu</MenuButton>} transition>
<MenuItem>Cut</MenuItem>
<MenuItem>Copy</MenuItem>
<MenuItem>Paste</MenuItem>
</Menu>
);
}
Submenu
#SubMenu
can be placed in a Menu
and has its own MenuItem
s as children. You might also create nested submenus under a submenu.
<Menu menuButton={<MenuButton>Menu</MenuButton>}>
<MenuItem>New File</MenuItem>
<SubMenu label="Edit">
<MenuItem>Cut</MenuItem>
<MenuItem>Copy</MenuItem>
<MenuItem>Paste</MenuItem>
<SubMenu label="Find">
<MenuItem>Find...</MenuItem>
<MenuItem>Find Next</MenuItem>
<MenuItem>Find Previous</MenuItem>
</SubMenu>
</SubMenu>
<MenuItem>Print...</MenuItem>
</Menu>
The label
prop of submenu accepts not only string type but any valid JSX. Thus, you can render images or icons in the label.
Event handling
#When a menu item is activated, the onClick
event fires on menu item. Unless the stopPropagation
of event object is set true
, the onItemClick
of root menu component will fire afterwards. If the keepOpen
of event object is set true
, menu will be kept open after the menu item is clicked.
For details of the event object, please see MenuItem.
<Menu
menuButton={<MenuButton>Menu</MenuButton>}
onItemClick={(e) => console.log(`[Menu] ${e.value} clicked`)}
>
<MenuItem value="Cut" onClick={(e) => console.log(`[MenuItem] ${e.value} clicked`)}>
Cut
</MenuItem>
<MenuItem
value="Copy"
onClick={(e) => {
console.log(`[MenuItem] ${e.value} clicked`);
// Stop the `onItemClick` of root menu component from firing
e.stopPropagation = true;
// Keep the menu open after this menu item is clicked
e.keepOpen = true;
}}
>
Copy
</MenuItem>
<MenuItem value="Paste">Paste</MenuItem>
</Menu>
Radio group
#You could make menu items behave like radio buttons by setting type="radio"
and wrapping them in a MenuRadioGroup
. The child menu item which has the same value (strict equality ===) as the radio group is marked as checked.
const [textColor, setTextColor] = useState('red');
<Menu menuButton={<MenuButton>Text color</MenuButton>}>
<MenuRadioGroup
value={textColor}
onRadioChange={(e) => setTextColor(e.value)}
>
<MenuItem type="radio" value="red">
Red
</MenuItem>
<MenuItem type="radio" value="green">
Green
</MenuItem>
<MenuItem type="radio" value="blue">
Blue
</MenuItem>
</MenuRadioGroup>
</Menu>
Checkbox
#You could make menu items behave like checkboxes by setting type="checkbox"
.
const [isBold, setBold] = useState(true);
const [isItalic, setItalic] = useState(true);
const [isUnderline, setUnderline] = useState(false);
<Menu menuButton={<MenuButton>Text style</MenuButton>}>
<MenuItem
type="checkbox"
checked={isBold}
onClick={(e) => setBold(e.checked)}
>
Bold
</MenuItem>
<MenuItem
type="checkbox"
checked={isItalic}
onClick={(e) => setItalic(e.checked)}
>
Italic
</MenuItem>
<MenuItem
type="checkbox"
checked={isUnderline}
onClick={(e) => setUnderline(e.checked)}
>
Underline
</MenuItem>
</Menu>
Header and divider
#You could use MenuHeader
and MenuDivider
to group related menu items.
<Menu menuButton={<MenuButton>Menu</MenuButton>}>
<MenuItem>New File</MenuItem>
<MenuItem>Save</MenuItem>
<MenuItem>Close Window</MenuItem>
<MenuDivider />
<MenuHeader>Edit</MenuHeader>
<MenuItem>Cut</MenuItem>
<MenuItem>Copy</MenuItem>
<MenuItem>Paste</MenuItem>
<MenuDivider />
<MenuItem>Print</MenuItem>
</Menu>
NOTE: you can render any valid JSX into menu children.
Combined example
#An example combines the usage of several components.
Menu item
#More examples with menu items.
Link and disabled
#MenuItem
can be made a hyperlink by giving it a href
prop. Even if it's a link, the onClick
event still fires as normal. You could also disable a menu item using the disabled
prop.
<Menu menuButton={<MenuButton>Menu</MenuButton>}>
<MenuItem href="https://www.google.com/">Google</MenuItem>
<MenuItem
href="https://github.com/szhsin/react-menu/"
target="_blank"
rel="noopener noreferrer"
>
GitHub <ExternalLinkIcon />
</MenuItem>
<MenuItem>Regular item</MenuItem>
<MenuItem disabled>Disabled item</MenuItem>
</Menu>
NOTE: the href
prop is meant to be a redirect which causes browser to reload the document at the URL specified. If you want to prevent the reload or work with React Router, please see this example.
Icon and image
#React-Menu doesn't include any imagery. However, you are free to use your own or third-party icons and images, as you could wrap anything in a MenuItem
. This example uses Google's Material icons.
<Menu menuButton={<MenuButton>Menu</MenuButton>}>
<MenuItem href="https://github.com/szhsin/react-menu/">
<img src="octocat.png" alt="octocat" role="presentation" />
GitHub
</MenuItem>
<MenuDivider />
<SubMenu
label={
<>
<i className="material-icons">edit</i>Edit
</>
}
>
<MenuItem>
<i className="material-icons">content_cut</i>Cut
</MenuItem>
<MenuItem>
<i className="material-icons">content_copy</i>Copy
</MenuItem>
<MenuItem>
<i className="material-icons">content_paste</i>Paste
</MenuItem>
</SubMenu>
</Menu>
Render prop
#MenuItem
manages some internal states one of which indicates whether the item is hovered. If you need to render dynamic contents in response to state updates, you can use children
as a render prop and pass it a callback function.
For more menu item states, please refer to MenuItem.
<Menu menuButton={<MenuButton>Menu</MenuButton>}>
<MenuItem>{({ hover }) => (hover ? 'Hovered!' : 'Hover me')}</MenuItem>
<MenuDivider />
<MenuItem style={{ justifyContent: 'center' }}>
{({ hover }) => (
<i className="material-icons md-48">
{hover ? 'sentiment_very_satisfied' : 'sentiment_very_dissatisfied'}
</i>
)}
</MenuItem>
</Menu>
The children
of menu also supports render prop pattern. When a function is provided to a menu's children
, it receives the menu's state and computed direction resulted from bounding box check.
Focusable item
#FocusableItem
is a special menu item. It's used to wrap elements which are able to receive focus, such as input or button.
It receives a render prop as children
and passes down a ref
and several other states. This example demonstrates how to use an input element to filter menu items.
import { useState } from 'react';
import { Menu, MenuItem, FocusableItem, MenuButton } from '@szhsin/react-menu';
import '@szhsin/react-menu/dist/index.css';
export default function Example() {
const [filter, setFilter] = useState('');
return (
<Menu
menuButton={<MenuButton>Menu</MenuButton>}
onMenuChange={(e) => e.open && setFilter('')}
>
<FocusableItem>
{({ ref }) => (
<input
ref={ref}
type="text"
placeholder="Type to filter"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
)}
</FocusableItem>
{['Apple', 'Banana', 'Blueberry', 'Cherry', 'Strawberry']
.filter((fruit) =>
fruit.toUpperCase().includes(filter.trim().toUpperCase())
)
.map((fruit) => (
<MenuItem key={fruit}>{fruit}</MenuItem>
))}
</Menu>
);
}
Menu options
#Control the display and position of menu related to menu button.
Placement
#You can control the position of menu and how it behaves in response to window scroll event with the align
, direction
, position
, and viewScroll
props.
Menu can be set to display an arrow pointing to its anchor element with the arrow
prop. You can also adjust menu's position relating to its anchor using the gap
and shift
prop.
info Try to select different option combinations and scroll page up and down to see the behaviour.
const [align, setAlign] = useState('center');
const [position, setPosition] = useState('anchor');
const [viewScroll, setViewScroll] = useState('auto');
const menus = ['right', 'top', 'bottom', 'left'].map((direction) => (
<Menu
menuButton={<MenuButton>{direction}</MenuButton>}
key={direction}
direction={direction}
align={align}
position={position}
viewScroll={viewScroll}
arrow={hasArrow ? true : false}
gap={hasGap ? 12 : 0}
shift={hasShift ? 12 : 0}
>
{['Apple', 'Banana', 'Blueberry', 'Cherry', 'Strawberry'].map((fruit) => (
<MenuItem key={fruit}>{fruit}</MenuItem>
))}
</Menu>
))
Overflow
#When there isn't enough space for all menu items, you could use the overflow
prop to make the menu list scrollable. The value of this prop is similar to the CSS overflow property.
Setting the overflow
prop could make a menu touch screen edges. If this is visually unpleasant, you may use boundingBoxPadding
to add space around the menu.
If you want to fix some items at the top or bottom, set setDownOverflow
prop on Menu
and takeOverflow
prop on a MenuGroup
which makes the group scrollable.
import {
Menu,
MenuItem,
MenuButton,
FocusableItem,
MenuGroup
} from '@szhsin/react-menu';
import '@szhsin/react-menu/dist/index.css';
const [overflow, setOverflow] = useState('auto');
const [position, setPosition] = useState('auto');
const [filter, setFilter] = useState('');
<Menu
menuButton={<MenuButton>Overflow</MenuButton>}
overflow={overflow}
position={position}
>
{new Array(100).fill(0).map((_, i) => (
<MenuItem key={i}>Item {i + 1}</MenuItem>
))}
</Menu>
<Menu
menuButton={<MenuButton>Grouping</MenuButton>}
overflow={overflow}
setDownOverflow
position={position}
boundingBoxPadding="10"
onMenuChange={(e) => e.open && setFilter('')}
>
<FocusableItem>
{({ ref }) => (
<input
ref={ref}
type="text"
placeholder="Type a number"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
)}
</FocusableItem>
<MenuGroup takeOverflow>
{new Array(100)
.fill(0)
.map((_, i) => `Item ${i + 1}`)
.filter((item) => item.includes(filter.trim()))
.map((item, i) => (
<MenuItem key={i}>{item}</MenuItem>
))}
</MenuGroup>
<MenuItem>Last (fixed)</MenuItem>
</Menu>
A menu with overflowing items prevents arrow from displaying properly. To get around it, you can use a MenuGroup
, please see a CodeSandbox example.
Bounding box
#By default, menu positions itself within its nearest ancestor element which a CSS overflow
value other than visible
, or the browser viewport when such an element is not present. Alternalively, you can use the portal
prop to make menu visually “break out” of its scrollable container. Also, you can specify a container in the page as the bounding box for a menu using the boundingBoxRef
prop. Menu will try to position itself within that container.
const boundingBoxRef = useRef(null);
const leftAnchor = useRef(null);
const rightAnchor = useRef(null);
const [{ state }, toggleMenu] = useMenuState();
const [portal, setPortal] = useState(false);
useEffect(() => {
toggleMenu(true);
}, [toggleMenu]);
const tooltipProps = {
state,
captureFocus: false,
arrow: true,
role: 'tooltip',
align: 'center',
viewScroll: 'auto',
position: 'anchor',
boundingBoxPadding: '1 8 1 1'
};
<label>
<input type="checkbox" checked={portal}
onChange={(e) => setPortal(e.target.checked)} />
Render via portal
</label>
<div ref={boundingBoxRef} style={{ overflow: 'auto', position: 'relative' }}>
<div ref={leftAnchor} />
<ControlledMenu {...tooltipProps} portal={portal}
anchorRef={leftAnchor} direction="top">
I can flip over if you scroll this block
</ControlledMenu>
<div ref={rightAnchor} />
{/* explicitly set bounding box with the boundingBoxRef prop */}
<ControlledMenu {...tooltipProps} boundingBoxRef={boundingBoxRef}
anchorRef={rightAnchor} direction="right">
I'm a tooltip built with React-Menu
</ControlledMenu>
</div>
NOTE: when there is an ancestor element which has a CSS overflow
value other than visible
above menu, please ensure at least one element between that overflow element and menu has a CSS position
value other than static
. For example, you could add position: relative
to the ancestor element which has overflow: auto
.
TIP: you could render menu into a specified DOM node instead of document.body
using the portal
prop, please see a CodeSandbox example.
Menu button
#Customising the menu button.
Render prop
#If you need to dynamically render menu button based on menu state, the menuButton
supports the render prop pattern.
<Menu
menuButton={({ open }) => <MenuButton>{open ? 'Close' : 'Open'}</MenuButton>}
>
<MenuItem>Cut</MenuItem>
<MenuItem>Copy</MenuItem>
<MenuItem>Paste</MenuItem>
</Menu>
Using any button
#You can use a native button element with Menu
, or use your own React button component which implements a forwarding ref and accepts onClick
and onKeyDown
event props.
Menu
also works well with popular React libraries, such as the Material-UI. See a CodeSandbox example.
The benefit of using MenuButton from this package is it has WAI-ARIA compliant attributes.
<Menu menuButton={<button type="button">Menu</button>}>
<MenuItem>Cut</MenuItem>
<MenuItem>Copy</MenuItem>
<MenuItem>Paste</MenuItem>
</Menu>
Controlled menu
#Get control of menu's open or close state with ControlledMenu
.
Controlling state
#In some use cases you may need to access a menu's state and control how the menu is open or closed. This can be implemented using a ControlledMenu
.
You need to provide at least a state
prop, and a ref
of an element to which menu will be positioned. You also need to update state
in response to the onClose
event.
You can optionally leverage a useClick
hook which helps create a similar toggle menu experience to the Menu
component.
import { useRef, useState } from 'react';
import { ControlledMenu, MenuItem, useClick } from '@szhsin/react-menu';
import '@szhsin/react-menu/dist/index.css';
export default function () {
const ref = useRef(null);
const [isOpen, setOpen] = useState(false);
const anchorProps = useClick(isOpen, setOpen);
return (
<>
<button type="button" ref={ref} {...anchorProps}>
Menu
</button>
<ControlledMenu
state={isOpen ? 'open' : 'closed'}
anchorRef={ref}
onClose={() => setOpen(false)}
>
<MenuItem>Cut</MenuItem>
<MenuItem>Copy</MenuItem>
<MenuItem>Paste</MenuItem>
</ControlledMenu>
</>
);
}
useMenuState
#useMenuState
Hook works with ControlledMenu
and help you manage the state transition/animation when menu opens and closes.
Please see useMenuState for more details.
import { useRef } from 'react';
import { ControlledMenu, MenuItem, useClick, useMenuState } from '@szhsin/react-menu';
import '@szhsin/react-menu/dist/index.css';
import '@szhsin/react-menu/dist/transitions/zoom.css';
export default function () {
const ref = useRef(null);
const [menuState, toggleMenu] = useMenuState({ transition: true });
const anchorProps = useClick(menuState.state, toggleMenu);
return (
<>
<button type="button" ref={ref} {...anchorProps}>
Menu
</button>
<ControlledMenu {...menuState} anchorRef={ref} onClose={() => toggleMenu(false)}>
<MenuItem>Cut</MenuItem>
<MenuItem>Copy</MenuItem>
<MenuItem>Paste</MenuItem>
</ControlledMenu>
</>
);
}
Hover menu
#You can create a hover menu with the useHover
hook and ControlledMenu
.
A hover menu created using the useHover
hook can work on both desktop and touch screens. Keyboard navigation is still supported.
Similar to the click menu, you can create menu state using useState
(w/o transition), or useMenuState
hook (with transition).
import { useRef, useState } from 'react';
import { ControlledMenu, MenuItem, useHover, useMenuState } from '@szhsin/react-menu';
import '@szhsin/react-menu/dist/index.css';
import '@szhsin/react-menu/dist/transitions/zoom.css';
const HoverMenu = () => {
const ref = useRef(null);
const [isOpen, setOpen] = useState(false);
const { anchorProps, hoverProps } = useHover(isOpen, setOpen);
return (
<>
<div ref={ref} {...anchorProps}>
Hover
</div>
<ControlledMenu
{...hoverProps}
state={isOpen ? 'open' : 'closed'}
anchorRef={ref}
onClose={() => setOpen(false)}
>
<MenuItem>Cut</MenuItem>
<MenuItem>Copy</MenuItem>
<MenuItem>Paste</MenuItem>
</ControlledMenu>
</>
);
};
const HoverMenuWithTransition = () => {
const ref = useRef(null);
const [menuState, toggle] = useMenuState({ transition: true });
const { anchorProps, hoverProps } = useHover(menuState.state, toggle);
return (
<>
<div ref={ref} {...anchorProps}>
Hover with transition
</div>
<ControlledMenu
{...hoverProps}
{...menuState}
anchorRef={ref}
onClose={() => toggle(false)}
>
<MenuItem>Cut</MenuItem>
<MenuItem>Copy</MenuItem>
<MenuItem>Paste</MenuItem>
</ControlledMenu>
</>
);
};
Context menu
#Context menu is implemented using a ControlledMenu
.
You need to provide an anchorPoint
of viewport coordinates to which menu will be positioned.
import { useState } from 'react';
import { ControlledMenu, MenuItem } from '@szhsin/react-menu';
import '@szhsin/react-menu/dist/index.css';
export default function () {
const [isOpen, setOpen] = useState(false);
const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 });
return (
<div
onContextMenu={(e) => {
if (typeof document.hasFocus === 'function' && !document.hasFocus()) return;
e.preventDefault();
setAnchorPoint({ x: e.clientX, y: e.clientY });
setOpen(true);
}}
>
Right click to open context menu
<ControlledMenu
anchorPoint={anchorPoint}
state={isOpen ? 'open' : 'closed'}
direction="right"
onClose={() => setOpen(false)}
>
<MenuItem>Cut</MenuItem>
<MenuItem>Copy</MenuItem>
<MenuItem>Paste</MenuItem>
</ControlledMenu>
</div>
);
}
TIP: sometimes you may want to reuse one menu for both dropdown and context menu. In this case, you can provide ControlledMenu
with both the anchorRef
and anchorPoint
props and dynamically switch between them, please see a CodeSandbox example.
Styling
#React-Menu is unopinionated when it comes to styling. It doesn't depend on any particular CSS-in-JS runtime and works with all flavours of front-end stack. Please checkout the respective CodeSandbox example below:
All styles are locally scoped to the components except in the CSS/SASS example.
You may import the @szhsin/react-menu/dist/core.css
which contains minimal style and some reset. However, this is optional as you can define everything from scratch without importing any css files. There is a style-utils
which helps write selectors for CSS-in-JS. You can find a complete list of CSS selectors in the styling guide.
In addition, you can use *className
props.
className prop
#You can provide components with CSS classes using the various *className
props. Optionally, you may pass a function to the props and return different CSS class names under different component states.
For more details about available states, please refer to the *className
props under each component.
// When using the functional form of className prop,
// it's advisable to put it outside React component scope.
const menuItemClassName = ({ hover }) =>
hover ? 'my-menuitem-hover' : 'my-menuitem';
<Menu menuButton={<MenuButton>Menu</MenuButton>} menuClassName="my-menu">
<MenuItem>New File</MenuItem>
<MenuItem>Save</MenuItem>
<MenuItem className={menuItemClassName}>I'm special</MenuItem>
</Menu>
/* CSS file */
.my-menu {
border: 2px solid green;
}
.my-menuitem {
color: blue;
background-color: yellow;
}
.my-menuitem-hover {
color: yellow;
background-color: black;
}