Features
#- React menu components for easy and fast web development.
- Unlimited levels of submenu.
- Supports dropdown or context menu.
- Supports radio and checkbox menu items.
- Flexible menu positioning.
- Comprehensive keyboard interactions.
- Unstyled components and easy customisation.
- Works in major browsers without polyfills.
- Adheres to WAI-ARIA Practices.
Install
## with npm
npm install @szhsin/react-menu
# with Yarn
yarn add @szhsin/react-menu
Usage
#Each of the following sections includes a live example. They are grouped into related categories. You could toggle between the brief and full versions of source code.
Menu
#The group includes 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 React from 'react';
import {
Menu,
MenuItem,
MenuButton
} from '@szhsin/react-menu';
import '@szhsin/react-menu/dist/index.css';
import '@szhsin/react-menu/dist/transitions/slide.css';
export default function Example() {
return (
<Menu menuButton={<MenuButton>Open menu</MenuButton>} transition>
<MenuItem>New File</MenuItem>
<MenuItem>Save</MenuItem>
<MenuItem>Close Window</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>Open menu</MenuButton>}>
<MenuItem>New File</MenuItem>
<SubMenu label="Open">
<MenuItem>index.html</MenuItem>
<MenuItem>example.js</MenuItem>
<SubMenu label="Styles">
<MenuItem>about.css</MenuItem>
<MenuItem>home.css</MenuItem>
<MenuItem>index.css</MenuItem>
</SubMenu>
</SubMenu>
<MenuItem>Save</MenuItem>
</Menu>
Event handling
#When a menu item is activated, the onClick
event fires on menu item. Unless you set stopPropagation
of event object to true, the onItemClick
of root menu component will then fires.
For details of the event object, please refer to MenuItem.
const [text, setText] = useState('');
const handleMenuClick = e => {
setText(t => t + `[Menu] ${e.value} clicked\n\n`);
};
const handleFileClick = e => {
setText(t => t + `[MenuItem] ${e.value} clicked\n`);
};
const handleSaveClick = e => {
setText(t => t + `[MenuItem] ${e.value} clicked\n\n`);
e.stopPropagation = true;
};
<div>
<Menu menuButton={<MenuButton>Open menu</MenuButton>}
onItemClick={handleMenuClick}>
<MenuItem value="File" onClick={handleFileClick}>
File
</MenuItem>
<MenuItem value="Save" onClick={handleSaveClick}>
Save
</MenuItem>
<MenuItem value="Close">Close</MenuItem>
</Menu>
<button onClick={() => setText('')}>
Clear
</button>
</div>
<textarea readOnly ref={ref} value={text} />
Radio group
#You could make menu items behave like radio buttons by 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 value="red">Red</MenuItem>
<MenuItem value="green">Green</MenuItem>
<MenuItem 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>Open 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>
Combined example
#An example combines the usage of several components.
Menu item
#Advanced usage 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.
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 exmaple.
<Menu menuButton={<MenuButton>Open menu</MenuButton>}>
<MenuItem href="https://www.google.com/">Google</MenuItem>
<MenuItem href="https://github.com/szhsin/react-menu/"
target="_blank" rel="noopener noreferrer">
GitHub (new window)
</MenuItem>
<MenuItem>Regular item</MenuItem>
<MenuItem disabled>Disabled item</MenuItem>
</Menu>
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>Open menu</MenuButton>}>
<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>
<MenuDivider />
<MenuItem href="https://github.com/szhsin/react-menu/">
<img src="octocat.png" alt="" role="presentation" />GitHub
</MenuItem>
</Menu>
Hover and active
#MenuItem
manages a number of internal states such as 'hover' and 'active'. If you need to display different contents under different states, you are able to use children
as a render prop and pass it a callback function.
For more menu item states, please refer to MenuItem.
<Menu menuButton={<MenuButton>Open menu</MenuButton>}>
<MenuItem>
{({ hover, active }) =>
active ? 'Pressed' : hover ? 'Press me' : 'Hover me'}
</MenuItem>
<MenuDivider />
<MenuItem styles={{ justifyContent: 'center' }}>
{({ hover, active }) =>
<i className="material-icons md-48">
{active ? 'sentiment_very_satisfied'
: hover ? 'sentiment_satisfied_alt'
: 'sentiment_very_dissatisfied'}
</i>
}
</MenuItem>
</Menu>
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.
const [filter, setFilter] = useState('');
<Menu menuButton={<MenuButton>Open 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.
Optionally, menu can be set to display an arrow pointing to its anchor element or add an offset using the arrow
, offsetX
, and offsetY
props.
info Try to select different option combinations and scroll page up and down to see the behaviour.
const [display, setDisplay] = useState('arrow');
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={display === 'arrow'}
offsetX={display === 'offset' &&
(direction === 'left' || direction === 'right')
? 12 : 0}
offsetY={display === 'offset' &&
(direction === 'top' || direction === 'bottom')
? 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.
If you want to fix some items at the top or bottom, use a MenuGroup
and add takeOverflow
prop to make the group scrollable.
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(40).fill(0).map(
(_, i) => <MenuItem key={i}>Item {i + 1}</MenuItem>)}
</Menu>
<Menu menuButton={<MenuButton>Grouping</MenuButton>}
overflow={overflow} 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(40).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>
Bounding box
#Normally menu positions itself within its nearest ancestor which has CSS overflow
set to a value other than 'visible', or the browser viewport. 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 ref = useRef(null);
const leftAnchor = useRef(null);
const rightAnchor = useRef(null);
const { state, toggleMenu } = useMenuState();
useEffect(() => {
toggleMenu(true);
}, [toggleMenu]);
const tooltipProps = {
state,
captureFocus: false,
arrow: true,
role: 'tooltip',
align: 'center',
viewScroll: 'auto',
position: 'anchor',
boundingBoxPadding: '1 8 1 1'
};
<div ref={ref}>
<div ref={leftAnchor} />
<ControlledMenu {...tooltipProps}
anchorRef={leftAnchor} direction="top">
I can flip if you scroll this block
</ControlledMenu>
<div ref={rightAnchor} />
{/* explicitly set bounding box with the boundingBoxRef prop */}
<ControlledMenu {...tooltipProps} boundingBoxRef={ref}
anchorRef={rightAnchor} direction="right">
I'm a tooltip built with React-Menu
</ControlledMenu>
</div>
Menu button
#Change the look and contents of your menu button.
Menu open state
#If you need to change the contents of a menu button when the menu opens, you could use the menuButton
as a render prop and pass it a callback function.
<Menu menuButton={
({ open }) =>
<MenuButton>{open ? 'Close' : 'Open'}</MenuButton>}>
<MenuItem>New File</MenuItem>
<MenuItem>Save</MenuItem>
<MenuItem>Close Window</MenuItem>
</Menu>
Customised button
#You are free to 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 an example on CodeSandbox.
The benefit of using MenuButton is it has additional aria
attributes.
<Menu menuButton={
<button className="btn-primary">Open menu</button>}>
<MenuItem>New File</MenuItem>
<MenuItem>Save</MenuItem>
<MenuItem>Close Window</MenuItem>
</Menu>
Controlled menu
#Get more control of the states with ControlledMenu
.
Managing state
#In some use cases you may need to control how and when a menu is open or closed, e.g. when something is hovered. 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.
const ref = useRef(null);
const [state, setState] = useState();
<div ref={ref} onMouseEnter={() => setState('open')}>
Hover to Open
</div>
<ControlledMenu state={state} anchorRef={ref}
onMouseLeave={() => setState('closed')}
onClose={() => setState('closed')}>
<MenuItem>New File</MenuItem>
<MenuItem>Save</MenuItem>
<MenuItem>Close Window</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.
const ref = useRef(null);
const { toggleMenu, ...menuProps } = useMenuState({ transition: true });
<div ref={ref} onMouseEnter={() => toggleMenu(true)}>
Hover to Open
</div>
<ControlledMenu {...menuProps} anchorRef={ref}
onMouseLeave={() => toggleMenu(false)}
onClose={() => toggleMenu(false)}>
<MenuItem>New File</MenuItem>
<MenuItem>Save</MenuItem>
<MenuItem>Close Window</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.
const { toggleMenu, ...menuProps } = useMenuState();
const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 });
<div onContextMenu={e => {
e.preventDefault();
setAnchorPoint({ x: e.clientX, y: e.clientY });
toggleMenu(true);
}}>
Right click to open context menu
<ControlledMenu {...menuProps} anchorPoint={anchorPoint}
onClose={() => toggleMenu(false)}>
<MenuItem>Cut</MenuItem>
<MenuItem>Copy</MenuItem>
<MenuItem>Paste</MenuItem>
</ControlledMenu>
</div >
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 will usually import the @szhsin/react-menu/dist/core.css
and target different CSS selectors. There is even a style-utils
which helps you easily write the selectors. You can find a complete list of CSS selectors in the styling guide.
In addition, you can use className
or styles
props.
className prop
#You can give components 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.
// If you use the functional form of className prop,
// it's advisable to put it outside React component scope whenever possible.
const menuItemClassName = ({ hover, active }) =>
active ? 'my-menuitem-active' : hover ? 'my-menuitem-hover' : 'my-menuitem';
<Menu menuButton={<MenuButton>Open menu</MenuButton>}
menuClassName="my-menu">
<MenuItem>New File</MenuItem>
<MenuItem>Save</MenuItem>
<MenuItem className={menuItemClassName}>
I'm special
</MenuItem>
</Menu>
// CSS classes
.my-menu {
border: 2px solid green;
}
.my-menuitem {
color: blue;
background-color: #ee1;
}
.my-menuitem-hover {
color: #ee1;
background-color: #bf4080;
}
.my-menuitem-active {
background-color: #333;
}
styles prop
#You can apply your style by giving an object to the various *styles
props. Regular styles are put in the object directly just like React's style
prop, and styles which are only applied to specific component states are written in nested objects under corresponding keys. The styles
object will be flattened by applying the properties from top to bottom, with later properties overriding earlier ones of the same name.
Optionally, you may pass a function to the prop and receive states about the component.
For more details about the state keys, please refer to the *styles
props under each component.
// It's advisable to put styles object outside React component scope whenever possible.
const menuStyles = {
border: '2px dashed green'
};
const menuItemStyles = {
color: 'blue',
backgroundColor: '#ee1',
hover: {
color: '#ee1',
backgroundColor: '#bf4080'
},
active: {
backgroundColor: '#333'
}
};
<Menu menuButton={<MenuButton>Open menu</MenuButton>} menuStyles={menuStyles}>
<MenuItem>New File</MenuItem>
<MenuItem>Save</MenuItem>
<MenuItem styles={menuItemStyles}>I'm special</MenuItem>
</Menu>