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.
  • Supports React 18 concurrent rendering.
  • 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 {
    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 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>Open 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.

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

    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>

    NOTE: you can render any valid JSX into menu children.

    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.

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

    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.

    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>

    Hovering items

    #

    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 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 }) => (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>

    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, set setDownOverflow prop on Menu and takeOverflow prop on a MenuGroup which makes 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(50).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(50).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

    #

    Normally menu positions itself within its nearest ancestor element which has CSS overflow set to a value other than 'visible', or the browser viewport when such an element is not present. 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 ref = 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={ref}>
        <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={ref}
            anchorRef={rightAnchor} direction="right">
            I'm a tooltip built with React-Menu
        </ControlledMenu>
    </div>

    You could render menu into a specified DOM node instead of document.body using the portal prop, please see a CodeSandbox example.

    #

    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 a CodeSandbox example.

    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 [isOpen, setOpen] = useState();
    
    <div ref={ref} className="btn" onMouseEnter={() => setOpen(true)}>
      Hover to Open
    </div>
    
    <ControlledMenu
      state={isOpen ? 'open' : 'closed'}
      anchorRef={ref}
      onMouseLeave={() => setOpen(false)}
      onClose={() => setOpen(false)}
    >
      <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 [menuProps, toggleMenu] = 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 [menuProps, toggleMenu] = 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, or you might define all the styles 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.

    // If you use the functional form of className prop, 
    // it's advisable to put it outside React component scope whenever possible.
    const menuItemClassName = ({ hover }) => (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: yellow;
    }
    
    .my-menuitem-hover {
        color: yellow;
        background-color: black;
    }