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.

#

The group includes common usage examples of Menu, SubMenu, and MenuItem.

Basic menu

#

The most basic menu consists of several MenuItems 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 can be placed in a Menu and has its own MenuItems 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.

    Sample text
    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".

    Sample text
    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.

    Sample text
    #

    Advanced usage examples with menu items.

    #

    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>
    #

    Control the display and position of menu related to menu button.

    #

    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.

    Align with anchor
    Menu to anchor
    When window scrolls
    Menu position

    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>
    ));
    #

    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.

    Overflow
    Menu position
    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>
    #

    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.

    Hover to Open
    Tip: try the example with a mouse
    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.

    Hover to Open
    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.

    Right click to open context menu
    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>