Skip to content

MenuButton (Dropdown)

Menu - MenuButton - MenuList - MenuItem - MenuLink

An accessible dropdown menu for the common dropdown menu button design pattern.

Please note that the buttons on this page are styled by this website. They are just buttons, so they will appear the same as any other button in your app.

<Menu>
  <MenuButton>
    Actions <span aria-hidden></span>
  </MenuButton>
  <MenuList>
    <MenuItem onSelect={() => alert("Download")}>Download</MenuItem>
    <MenuItem onSelect={() => alert("Copy")}>Create a Copy</MenuItem>
    <MenuItem onSelect={() => alert("Mark as Draft")}>Mark as Draft</MenuItem>
    <MenuItem onSelect={() => alert("Delete")}>Delete</MenuItem>
    <MenuLink
      component="a"
      href="https://reach.tech/workshops"
    >Attend a Workshop</MenuLink>
  </MenuList>
</Menu>

Installation

npm install @reach/menu-button
# or
yarn add @reach/menu-button

And then import the components you need:

import {
  Menu,
  MenuList,
  MenuButton,
  MenuItem,
  MenuLink
} from "@reach/menu-button";

Menu

The wrapper component for the other components. No DOM element is rendered.

PropTypeRequired
childrennodefalse

Type: node

Requires two children: a <MenuButton> and a <MenuList>.

<Menu>
  <MenuButton>Actions</MenuButton>
  <MenuList>
    <MenuItem>Download</MenuItem>
    <MenuLink to="view">View</MenuLink>
  </MenuList>
</Menu>

MenuButton

Wraps a DOM button that toggles the opening and closing of the dropdown menu. Must be rendered inside of a <Menu>.

<Menu>
  <MenuButton>Profile</MenuButton>
  {/* ... */}
</Menu>

Please see the styling guide.

A <MenuButton> wraps a normal <button> and no styles are applied to it, so any global button styles you have will be applied.

button {
  /* your normal button styles will be applied */
}

You can use the [data-reach-menu-button] selector to style only the dropdown buttons:

[data-reach-menu-button] {
  color: blue;
}

If you'd like to target when the menu is open use aria-expanded:

[data-reach-menu-button][aria-expanded="true"] {
  background: #000;
  color: white;
}
PropTypeRequired
button propsspreadn/a
childrennodefalse
onClickpreventableEventFuncfalse
onKeyDownpreventableEventFuncfalse

Type: spread

Any props not listed above will be spread onto the underlying button element. You can treat it like any other button in your app for styling.

<Menu>
  <MenuButton
    className="button-primary"
    style={{ boxShadow: "2px 2px 2px hsla(0, 0%, 0%, 0.25)" }}
  >
    Actions <span aria-hidden></span>
  </MenuButton>
  <MenuList>
    <MenuItem onSelect={() => {}}>Do nothing</MenuItem>
  </MenuList>
</Menu>

Type: node

Accepts any renderable content.

<MenuButton>
  Actions <span aria-hidden><Gear /></span>
</MenuButton>

MenuList

Wraps a DOM element that renders the menu items. Must be rendered inside of a <Menu>.

<Menu>
  {/* ... */}
  <MenuList>
    <MenuItem onSelect={() => {}}>Download</MenuItem>
  </MenuList>
</Menu>
[data-reach-menu-list] {
  padding: 20px 10px;
}
PropTypeRequired
element propsspreadn/a
childrennodefalse

Type: spread

All props are spread to the underlying element. Here we apply a className the element.

<Menu>
  <MenuButton>
    Actions <span aria-hidden></span>
  </MenuButton>
  <MenuList className="slide-down">
    <MenuItem onSelect={() => {}}>Start Video</MenuItem>
    <MenuItem onSelect={() => {}}>Start Screenshare</MenuItem>
    <MenuItem onSelect={() => {}}>Send a Message</MenuItem>
  </MenuList>
</Menu>

The stylesheet contains these rules to create the animation.

@keyframes slide-down {
  0% {
    opacity: 0;
    transform: translateY(-10px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

.slide-down[data-reach-menu-list] {
  border-radius: 5px;
  animation: slide-down 0.2s ease;
}

Type: node

Can contain only MenuItem or a MenuLink

<MenuList>
  <MenuItem />
  <MenuLink />
</MenuList>

MenuItem

Handles menu selection. Must be a direct child of a <MenuList>.

<MenuList>
  <MenuItem onSelect={() => alert('download!')}>Download</MenuItem>
</MenuList>

Please see the styling guide.

[data-reach-menu-item] {
  padding: 20px 10px;
}

To change the styles of a highlighted menu item, use this pseudo-pseudo selector:

[data-reach-menu-item][data-selected] {
  background: red;
}

The following example has this css applied:

.red-highlight[data-reach-menu-item][data-selected] {
  background: red;
}
<Menu>
  <MenuButton>
    Actions <span aria-hidden></span>
  </MenuButton>
  <MenuList>
    <MenuItem className="red-highlight" onSelect={() => {}}>
      Start Video
    </MenuItem>
    <MenuItem className="red-highlight" onSelect={() => {}}>
      Start Screenshare
    </MenuItem>
  </MenuList>
</Menu>
PropTypeRequired
element propsspreadn/a
childrennodefalse
onClickpreventableEventFuncfalse
onKeyDownpreventableEventFuncfalse
onMouseMovepreventableEventFuncfalse
onSelectpreventableEventFunctrue

Type: spread

All props are spread to the underlying element.

In this example the onFocus prop is passed down to the element.

<Component initialState={{ focusCount: 0 }}>
  {({ setState, state: { focusCount } }) => (
    <Menu>
      <MenuButton>
        Actions
      </MenuButton>
      <MenuList>
        <MenuItem
          onFocus={() => {
            setState(state => ({
              focusCount: state.focusCount + 1
            }))
          }}
          onSelect={() => {}}
        >Focused {focusCount} Times</MenuItem>
        <MenuItem onSelect={() => {}}>Start Screenshare</MenuItem>
        <MenuItem onSelect={() => {}}>Send a Message</MenuItem>
      </MenuList>
    </Menu>
  )}
</Component>

Type: node

You can put any type of content inside of a <MenuItem>.

<Menu>
  <MenuButton>
    Your Cats <span aria-hidden></span>
  </MenuButton>
  <MenuList className="kittys">
    <MenuItem onSelect={() => {}}>
      <img
        src="https://placekitten.com/100/100"
        alt="Fluffybuns the destroyer"
      />
      <span>Fluffybuns the Destroyer</span>
    </MenuItem>
    <MenuItem onSelect={() => {}}>
      <img
        src="https://placekitten.com/120/120"
        alt="Simon the pensive"
      />
      <span>Simon the pensive</span>
    </MenuItem>
  </MenuList>
</Menu>

MenuLink

Handles linking to a different page in the menu. By default it works with Reach Router, but also accepts any other kind of Link as long as the Link uses the React.forwardRef API.

Must be a direct child of a <MenuList>.

<MenuList>
  <MenuLink to="somewhere/else">Somewhere w/ Reach Router</MenuLink>
  <MenuLink
    component="a"
    href="https://reactjs.org"
  >Official React Site</MenuLink>
  <MenuLink
    component={GatsbyLink}
    to="/somewhere/with/gatsby"
  >Official React Site</MenuLink>
</MenuList>

Please see the styling guide.

[data-reach-menu-item] {
  padding: 20px 10px;
}

To change the styles of a highlighted menu item, use this pseudo-pseudo selector:

[data-reach-menu-item][data-selected] {
  background: red;
}
PropTypeRequired
element propsspreadn/a
componentnodestring
childrennodefalse
onClickpreventableEventFuncfalse
tostringtrue

Type: spread

All props are spread to the underlying element

// the `to` prop is spread onto the Reach Router Link
<MenuLink to="somewhere/else">
  Somewhere
</MenuLink>

// the `href` prop is spread onto the underlying `a`
<MenuLink
  component="a"
  href="https://reactjs.org"
>Official React Site</MenuLink>

Type: oneOfType(string, component)

By default, MenuLink renders a Reach Router Link, but if you have external links you can use component="a".

<Menu>
  <MenuButton>
    Products
  </MenuButton>
  <MenuList>
    <MenuLink component="a" href="https://reach.tech/workshops">
      Workshops
    </MenuLink>
    <MenuLink component="a" href="https://reach.tech/courses">
      Online Courses
    </MenuLink>
    <MenuLink component="a" href="https://reach.tech/ui">
      Reach UI
    </MenuLink>
  </MenuList>
</Menu>

Additionally, if other routers' Link component uses the React.forwardRef API, you can pass them in as well. If they don’t it won't work because we will not be able to manage focus on the element the component renders.

import GatsbyLink from "gatsby/link"

<MenuLink component={GatsbyLink} to="/somewhere"/>

Type: node

You can render any kind of content inside of a MenuLink.

<MenuLink>
  <ProfileImage userId="4"/>
  <UserName>Ryan Florence</UserName>
</MenuLink>

Notes

Unmounting the Menu after an action

If one of your menu items causes the <Menu> itself to unmount, it is your job to move focus to the changed content. One exception to this is if you're using <MenuLink> and Reach Router. In this case, the router will handle focus for you.

Note the callbacks given to setState in the following demo app where focus is managed between screens. If you don't do this you'll drop keyboard and screenreader users off at the top of the document. It'll then be hard for them to know what changed and how to find it. Moving focus helps them stay where you want them the very same way visual design does.

(() => {
  class App extends React.Component {
    constructor() {
      super();
      this.focusRefs = {
        screen1: React.createRef(),
        screenTwoButton: React.createRef()
      };

      this.state = {
        screen: 1
      };
    }

    render() {
      return this.state.screen === 1 ? (
        <div ref={this.focusRefs.screen1} tabIndex="-1">
          <h4>Screen One</h4>
          <Menu>
            <MenuButton>Actions</MenuButton>
            <MenuList>
              <MenuItem
                onSelect={() => {
                  this.setState({ screen: 2 }, () => {
                    this.focusRefs.screenTwoButton.current.focus();
                  });
                }}
              >
                Go to screen 2
              </MenuItem>
              <MenuItem onSelect={() => {}}>
                Do nothing
              </MenuItem>
            </MenuList>
          </Menu>
          <Menu />
        </div>
      ) : this.state.screen === 2 ? (
        <div>
          <h4>Screen 2</h4>
          <button
            ref={this.focusRefs.screenTwoButton}
            onClick={() => {
              this.setState({ screen: 1 }, () =>
                this.focusRefs.screen1.current.focus()
              );
            }}
          >
            Back to screen 1
          </button>
        </div>
      ) : null;
    }
  }

  return <App />;
})()

Screen One

Icons

If you add an icon to indicate to users the button is a dropdown menu, use aria-hidden on the icon. Screenreaders will already announce to the user that the element is a dropdown menu; adding a label to your icon would be redundant.

<MenuButton>
  Actions <span aria-hidden>▾</span>
</MenuButton>

However, if you have no text and only an icon, please make sure your icon has a screenreader friendly label:

// we'd rather it said "Actions" than
// "downward pointing triangle"
<MenuButton>
  <span aria-label="Actions">▾</span>
</MenuButton>

// add screen reader only text for svgs
import AriaText from "@reach/aria-text"
<MenuButton>
  <AriaText>Actions</AriaText>
  <svg aria-hidden>
    <polygon points="0,0 20,0 10,10 " />
  </svg>
</MenuButton>

// and your images an alt attribute
<MenuButton>
  <img src="gear.png" alt="gear"/>
</MenuButton>

// Or just label the button and hide everything
<MenuButton aria-label="Actions">
  <span aria-hidden>
    <TripleDots/>
  </span>
</MenuButton>

Keyboard Accessibility

KeyAction
EnterOpen/close
ArrowUpHighlight previous item
ArrowDownHighlight next item
EnterSelect item
EscapeClose
TabNo effect
TODO: Type charactersHighlights matching item