Dropdown Menu
Menus offer an easy way to build custom, accessible dropdown components with robust support for keyboard navigation.
To get started, install Headless UI via npm:
npm install @headlessui/reactMenus are built using the Menu, MenuButton, MenuItems, and MenuItem components:
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom">
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}The MenuButton will automatically open and close the MenuItems when clicked, and when the menu is opened the list of
items receives focus and is navigable via the keyboard.
Headless UI keeps track of a lot of state about each component, like which menu item is currently focused via the keyboard, whether a popover is open or closed, or which listbox option is currently selected.
But because the components are headless and completely unstyled out of the box, you can't see this information in your UI until you provide the styles you want for each state yourself.
The easiest way to style the different states of a Headless UI component is using the data-* attributes that each
component exposes.
For example, the MenuButton component exposes a data-active attribute, which tells you if the menu is currently
open, and the MenuItem component exposes a data-focus attribute, which tells you if the menu item is currently
focused via the mouse or keyboard.
<!-- Rendered `MenuButton`, `MenuItems`, and `MenuItem` -->
<button data-active>Options</button>
<div data-open>
<a href="/settings">Settings</a>
<a href="/support" data-focus>Support</a>
<a href="/license">License</a>
</div>Use the CSS attribute selector to conditionally apply styles based on the presence of these data attributes. If you're using Tailwind CSS, the data attribute modifier makes this easy:
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
const links = [
{ href: '/settings', label: 'Settings' },
{ href: '/support', label: 'Support' },
{ href: '/license', label: 'License' },
]
function Example() {
return (
<Menu>
<MenuButton className="data-active:bg-blue-200">My account</MenuButton> <MenuItems anchor="bottom">
{links.map((link) => (
<MenuItem key={link.href} className="block data-focus:bg-blue-100"> <a href={link.href}>{link.label}</a>
</MenuItem>
))}
</MenuItems>
</Menu>
)
}
See the component API for a list of all the available data attributes.
Each component also exposes information about its current state via render props that you can use to conditionally apply different styles or render different content.
For example, the MenuButton component exposes an active state, which tells you if the menu is currently open, and
the MenuItem component exposes a focus state, which tells you if the menu item is currently focused via the mouse or
keyboard.
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
import clsx from 'clsx'
import { Fragment } from 'react'
const links = [
{ href: '/settings', label: 'Settings' },
{ href: '/support', label: 'Support' },
{ href: '/license', label: 'License' },
]
function Example() {
return (
<Menu>
<MenuButton as={Fragment}> {({ active }) => <button className={clsx(active && 'bg-blue-200')}>My account</button>} </MenuButton> <MenuItems anchor="bottom">
{links.map((link) => (
<MenuItem key={link.href} as={Fragment}> {({ focus }) => ( <a className={clsx('block', focus && 'bg-blue-100')} href={link.href}> {link.label} </a> )} </MenuItem> ))}
</MenuItems>
</Menu>
)
}
See the component API for a list of all the available render props.
In addition to links, you can also use buttons in a MenuItem. This is useful when you want to trigger an action like
opening a dialog or submitting a form.
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
function Example() {
function showSettingsDialog() {
alert('Open settings dialog!')
}
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom">
<MenuItem> <button onClick={showSettingsDialog} className="block w-full text-left data-focus:bg-blue-100"> Settings </button> </MenuItem> <MenuItem>
<a className="block data-focus:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/license">
License
</a>
</MenuItem>
<form action="/logout" method="post"> <MenuItem> <button type="submit" className="block w-full text-left data-focus:bg-blue-100"> Sign out </button> </MenuItem> </form> </MenuItems>
</Menu>
)
}
Use the disabled prop to disable a MenuItem and prevent it from being selected:
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom">
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/license">
License
</a>
</MenuItem>
<MenuItem disabled> <a className="block data-disabled:opacity-50" href="/invite-a-friend"> Invite a friend (coming soon!) </a> </MenuItem> </MenuItems>
</Menu>
)
}
Use the MenuSeparator component to add a visual separation between items in a menu.
import { Menu, MenuButton, MenuItem, MenuItems, MenuSeparator } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom">
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuSeparator className="my-1 h-px bg-black" /> <MenuItem>
<a className="block data-focus:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}
Use the MenuSection, MenuHeading, and MenuSeparator components to group items into sections with labels:
import { Menu, MenuButton, MenuHeading, MenuItem, MenuItems, MenuSection, MenuSeparator } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom">
<MenuSection> <MenuHeading className="text-sm opacity-50">Settings</MenuHeading> <MenuItem>
<a className="block data-focus:bg-blue-100" href="/profile">
My profile
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/notifications">
Notifications
</a>
</MenuItem>
</MenuSection> <MenuSeparator className="my-1 h-px bg-black" /> <MenuSection> <MenuHeading className="text-sm opacity-50">Support</MenuHeading> <MenuItem>
<a className="block data-focus:bg-blue-100" href="/support">
Documentation
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuSection> </MenuItems>
</Menu>
)
}
The MenuItems dropdown has no width set by default, but you can add one using CSS:
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom" className="w-52"> <MenuItem>
<a className="block data-focus:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}
If you'd like the dropdown width to match the MenuButton width, use the --button-width CSS variable that's exposed
on the MenuItems element:
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom" className="w-(--button-width)"> <MenuItem>
<a className="block data-focus:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}
Add the anchor prop to the MenuItems to automatically position the dropdown relative to the MenuButton:
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom start"> <MenuItem>
<a className="block data-focus:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}
Use the values top, right, bottom, or left to center the dropdown along the appropriate edge, or combine it with
start or end to align the dropdown to a specific corner, such as top start or bottom end.
To control the gap between the button and the dropdown, use the --anchor-gap CSS variable:
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom start" className="[--anchor-gap:4px] sm:[--anchor-gap:8px]"> <MenuItem>
<a className="block data-focus:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}
Additionally, you can use --anchor-offset to control the distance that the dropdown should be nudged from its original
position, and --anchor-padding to control the minimum space that should exist between the dropdown and the viewport.
The anchor prop also supports an object API that allows you to control the gap, offset, and padding values using
JavaScript:
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor={{ to: 'bottom start', gap: '4px' }}> <MenuItem>
<a className="block data-focus:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}
See the MenuItems API for more information about these options.
To animate the opening and closing of the dropdown, add the transition prop to the MenuItems component and then use
CSS to style the different stages of the transition:
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems
anchor="bottom"
transition className="origin-top transition duration-200 ease-out data-closed:scale-95 data-closed:opacity-0" >
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}
Internally, the transition prop is implemented in the exact same way as the Transition component. See the
Transition documentation to learn more.
Headless UI also composes well with other animation libraries in the React ecosystem like Framer Motion and React Spring. You just need to expose some state to those libraries.
For example, to animate the menu with Framer Motion, add the static prop to the MenuItems component and then
conditionally render it based on the open render prop:
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
import { AnimatePresence, motion } from 'framer-motion'
function Example() {
return (
<Menu>
{({ open }) => ( <>
<MenuButton>My account</MenuButton>
<AnimatePresence>
{open && ( <MenuItems
static as={motion.div}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
anchor="bottom"
className="origin-top"
>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
)} </AnimatePresence>
</>
)} </Menu>
)
}
By default, the Menu will close when clicking a MenuItem. However, some third-party Link components use
event.preventDefault() which prevents the menu from closing.
In these situations you can imperatively close the menu using the close render prop that's available on both the
Menu and MenuItem components:
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
import { MyCustomLink } from './MyCustomLink'
function Example() {
return (
<Menu>
<MenuButton>Terms</MenuButton>
<MenuItems anchor="bottom">
<MenuItem>
{({ close }) => ( <MyCustomLink href="/" onClick={close}> Read and accept
</MyCustomLink>
)} </MenuItem>
</MenuItems>
</Menu>
)
}
By default, the Menu and its subcomponents each render a default element that is sensible for that component.
For example, MenuButton renders a button by default, and MenuItems renders a div. By contrast, Menu and
MenuItem do not render an element, and instead render their children directly by default.
Use the as prop to render the component as a different element or as your own custom component, making sure your
custom components forward refs so that Headless UI can wire things up
correctly.
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
import { forwardRef } from 'react'
let MyCustomButton = forwardRef(function (props, ref) { return <button className="..." ref={ref} {...props} />})
function Example() {
return (
<Menu>
<MenuButton as={MyCustomButton}>My account</MenuButton> <MenuItems anchor="bottom" as="section"> <MenuItem as="a" className="block data-focus:bg-blue-100" href="/settings"> Settings
</MenuItem>
<MenuItem as="a" className="block data-focus:bg-blue-100" href="/support"> Support
</MenuItem>
<MenuItem as="a" className="block data-focus:bg-blue-100" href="/license"> License
</MenuItem>
</MenuItems>
</Menu>
)
}
To tell an element to render its children directly with no wrapper element, use as={Fragment}.
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
import { Fragment } from 'react'
function Example() {
return (
<Menu>
<MenuButton as={Fragment}> <button>My account</button> </MenuButton> <MenuItems anchor="bottom">
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-focus:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}
This is important if you are using an interactive element like an <a> tag inside the MenuItem. If the MenuItem had
an as="div", then the props provided by Headless UI would be forwarded to the div instead of the a, which means
that you can't go to the URL provided by the <a> tag anymore via your keyboard.
Prior to Next.js v13, the Link component did not forward unknown props to the underlying a element, preventing the
menu from closing on click when used inside a MenuItem.
If you're using Next.js v12 or older, you can work around this issue by creating your own component that wraps Link
and forwards unknown props to the child a element:
import { forwardRef } from 'react'
import Link from 'next/link'
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/react'
const MyLink = forwardRef((props, ref) => { let { href, children, ...rest } = props return ( <Link href={href}> <a ref={ref} {...rest}> {children} </a> </Link> )})
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom">
<MenuItem>
<MyLink href="/settings">Settings</MyLink> </MenuItem>
</MenuItems>
</Menu>
)
}
This will ensure that all of the event listeners Headless UI needs to add to the a element are properly applied.
This behavior was changed in Next.js v13 making this workaround no longer necessary.
| Command | Description |
Enter or Spacewhen | Opens menu and focuses first non-disabled item |
ArrowDown or ArrowUpwhen | Opens menu and focuses first/last non-disabled item |
Escwhen menu is open | Closes any open Menus |
ArrowDown or ArrowUpwhen menu is open | Focuses previous/next non-disabled item |
Home or PageUpwhen menu is open | Focuses first non-disabled item |
End or PageDownwhen menu is open | Focuses last non-disabled item |
Enter or Spacewhen menu is open | Activates/clicks the current menu item |
A–Z or a–zwhen menu is open | Focuses first item that matches keyboard input |
| Prop | Default | Description |
as | Fragment | String | ComponentThe element or component the menu should render as. |
| Data Attribute | Render Prop | Description |
data-open | open |
Whether or not the menu is open. |
| — | close |
Closes the menu and refocuses |
| Prop | Default | Description |
as | button | String | ComponentThe element or component the menu button should render as. |
disabled | false | BooleanWhether or not the menu button is disabled. |
| Data Attribute | Render Prop | Description |
data-open | open |
Whether or not the menu is open. |
data-focus | focus |
Whether or not the menu button is focused. |
data-hover | hover |
Whether or not the menu button is hovered. |
data-active | active |
Whether or not the menu button is in an active or pressed state. |
data-autofocus | autofocus |
Whether or not the |
| Prop | Default | Description |
as | div | String | ComponentThe element or component the menu items should render as. |
transition | false | BooleanWhether the element should render transition attributes like |
anchor | — | ObjectConfigures the way the dropdown is anchored to the button. |
anchor.to | bottom | StringWhere to position the menu items relative to the trigger. Use the values |
anchor.gap | 0 | Number | StringThe space between the menu button and the menu items. Can also be controlled using the |
anchor.offset | 0 | Number | StringThe distance the menu items should be nudged from its original position. Can also be controlled using the |
anchor.padding | 0 | Number | StringThe minimum space between the menu items and the viewport. Can also be controlled using the |
static | false | BooleanWhether the element should ignore the internally managed open/closed state. |
unmount | true | BooleanWhether the element should be unmounted or hidden based on the open/closed state. |
portal | false | BooleanWhether the element should be rendered in a portal. Automatically set to |
modal | true | BooleanWhether to enable accessibility features like scroll locking, focus trapping, and making other elements |
| Data Attribute | Render Prop | Description |
data-open | open |
Whether or not the menu is open. |
| Prop | Default | Description |
as | Fragment | String | ComponentThe element or component the menu item should render as. |
disabled | false | BooleanWhether or not the menu item is disabled for keyboard navigation and ARIA purposes. |
| Data Attribute | Render Prop | Description |
data-disabled | disabled |
Whether or not the menu item is disabled. |
data-focus | focus |
Whether or not the menu item is focused. |
| — | close |
Closes the menu and refocuses |
| Prop | Default | Description |
as | div | String | ComponentThe element or component the menu section should render as. |
| Prop | Default | Description |
as | header | String | ComponentThe element or component the menu heading should render as. |
| Prop | Default | Description |
as | div | String | ComponentThe element or component the menu separator should render as. |
If you're interested in predesigned Tailwind CSS dropdown component examples using Headless UI, check out Tailwind Plus — a collection of beautifully designed and expertly crafted components built by us.
It's a great way to support our work on open-source projects like this and makes it possible for us to improve them and keep them well-maintained.
