Listbox
Listboxes are a great foundation for building custom, accessible select menus for your app, complete with robust support for keyboard navigation.
To get started, install Headless UI via npm:
npm install @headlessui/react
Listboxes are built using the Listbox
, ListboxButton
, ListboxSelectedOption
, ListboxOptions
, and ListboxOption
components.
The ListboxButton
will automatically open/close the ListboxOptions
when clicked, and when the listbox is open, the
list of options receives focus and is automatically navigable via the keyboard.
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom">
{people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
Headless UI keeps track of a lot of state about each component, like which listbox option is currently selected, whether a popover is open or closed, or which item in a menu is currently focused via the keyboard.
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 ListboxOption
component exposes a data-focus
attribute, which tells you if the option is currently
focused via the mouse or keyboard, and a data-selected
attribute, which tells you if that option matches the current
value
of the Listbox
.
<!-- Rendered `ListboxOption` -->
<div data-focus data-selected>Arlene Mccoy</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 { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { CheckIcon } from '@heroicons/react/20/solid'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom">
{people.map((person) => (
<ListboxOption key={person.id} value={person} className="group flex gap-2 bg-white data-[focus]:bg-blue-100"> <CheckIcon className="invisible size-5 group-data-[selected]:visible" /> {person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
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 ListboxOption
component exposes a focus
state, which tells you if the option is currently focused
via the mouse or keyboard, and a selected
state, which tells you if that option matches the current value
of the
Listbox
.
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { CheckIcon } from '@heroicons/react/20/solid'
import clsx from 'clsx'
import { Fragment, useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom">
{people.map((person) => (
<ListboxOption key={person.id} value={person} as={Fragment}> {({ focus, selected }) => ( <div className={clsx('flex gap-2', focus && 'bg-blue-100')}> <CheckIcon className={clsx('size-5', !selected && 'invisible')} /> {person.name} </div> )} </ListboxOption> ))}
</ListboxOptions>
</Listbox>
)
}
See the component API for a list of all the available render props.
Wrap a Label
and Listbox
with the Field
component to automatically associate them using a generated ID:
import { Field, Label, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Field> <Label>Assignee:</Label> <Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom">
{people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
</Field> )
}
Use the Description
component within a Field
to automatically associate it with a Listbox
using the
aria-describedby
attribute:
import { Description, Field, Label, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Field> <Label>Assignee:</Label>
<Description>This person will have full access to this project.</Description> <Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom">
{people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
</Field> )
}
Add the disabled
prop to the Field
component to disable a Listbox
and its associated Label
and Description
:
import { Description, Field, Label, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Field disabled> <Label>Assignee:</Label>
<Description>This person will have full access to this project.</Description>
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom">
{people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
</Field>
)
}
You can also disable a listbox outside of a Field
by adding the disabled prop directly to the Listbox
itself.
Use the disabled
prop to disable a ListboxOption
and prevent it from being selected:
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds', available: true },
{ id: 2, name: 'Kenton Towne', available: true },
{ id: 3, name: 'Therese Wunsch', available: true },
{ id: 4, name: 'Benedict Kessler', available: false }, { id: 5, name: 'Katelyn Rohan', available: true },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom">
{people.map((person) => (
<ListboxOption
key={person.id}
value={person}
disabled={!person.available} className="data-[focus]:bg-blue-100 data-[disabled]:opacity-50" >
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
If you add the name
prop to your Listbox
, a hidden input
element will be rendered and kept in sync with the
listbox state.
import { Field, Label, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<form action="/projects/1" method="post"> <Field>
<Label>Assignee:</Label>
<Listbox name="assignee" value={selectedPerson} onChange={setSelectedPerson}> <ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom">
{people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
</Field>
<button>Submit</button>
</form> )
}
This lets you use a listbox inside a native HTML <form>
and make traditional form submissions as if your listbox was a
native HTML form control.
Basic values like strings will be rendered as a single hidden input containing that value, but complex values like objects will be encoded into multiple inputs using a square bracket notation for the names:
<!-- Rendered hidden inputs -->
<input type="hidden" name="assignee[id]" value="1" />
<input type="hidden" name="assignee[name]" value="Durward Reynolds" />
If you omit the value
prop, Headless UI will track its state internally for you, allowing you to use it as an
uncontrolled component.
When uncontrolled, use the defaultValue
prop to provide an initial value to the Listbox
.
import { Field, Label, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
return (
<form action="/projects/1" method="post">
<Field>
<Label>Assignee:</Label>
<Listbox name="assignee" defaultValue={people[0]}> <ListboxButton>{({ value }) => value.name}</ListboxButton>
<ListboxOptions anchor="bottom">
{people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
</Field>
<button>Submit</button>
</form>
)
}
This can simplify your code when using the listbox with HTML forms or with form APIs that collect their state using FormData instead of tracking it using React state.
Any onChange
prop you provide will still be called when the component's value changes in case you need to run any side
effects, but you won't need to use it to track the component's state yourself.
The ListboxOptions
dropdown has no width set by default, but you can add one using CSS:
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom" className="w-52"> {people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
If you'd like the dropdown width to match the ListboxButton
width, use the --button-width
CSS variable that's
exposed on the ListboxOptions
element:
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom" className="w-[var(--button-width)]"> {people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
Add the anchor
prop to the ListboxOptions
to automatically position the dropdown relative to the ListboxButton
:
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom start"> {people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
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 { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom start" className="[--anchor-gap:4px] sm:[--anchor-gap:8px]"> {people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
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 { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor={{ to: 'bottom start', gap: '4px' }}> {people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
See the ListboxOptions API for more information about these options.
If you've styled your ListboxOptions
to appear horizontally, use the horizontal
prop on the Listbox
component to
enable navigating the options with the left and right arrow keys instead of up and down, and to update the
aria-orientation
attribute for assistive technologies.
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox horizontal value={selectedPerson} onChange={setSelectedPerson}> <ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom" className="flex flex-row"> {people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
To animate the opening and closing of the listbox dropdown, add the transition
prop to the ListboxOptions
component
and then use CSS to style the different stages of the transition:
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions
anchor="bottom"
transition className="origin-top transition duration-200 ease-out data-[closed]:scale-95 data-[closed]:opacity-0" >
{people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
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 listbox with Framer Motion, add the static
prop to the ListboxOptions
component and then
conditionally render it based on the open
render prop:
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { AnimatePresence, motion } from 'framer-motion'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
{({ open }) => ( <>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<AnimatePresence>
{open && ( <ListboxOptions
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"
>
{people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
)} </AnimatePresence>
</>
)} </Listbox>
)
}
Unlike native HTML form controls, which only allow you to provide strings as values, Headless UI supports binding complex objects as well.
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [ { id: 1, name: 'Durward Reynolds' }, { id: 2, name: 'Kenton Towne' }, { id: 3, name: 'Therese Wunsch' }, { id: 4, name: 'Benedict Kessler' }, { id: 5, name: 'Katelyn Rohan' },]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}> <ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom">
{people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name} </ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
When binding objects as values, it's important to make sure that you use the same instance of the object as both the
value
of the Listbox
as well as the corresponding ListboxOption
, otherwise they will fail to be equal and cause
the listbox to behave incorrectly.
To make it easier to work with different instances of the same object, you can use the by
prop to compare the objects
by a particular field instead of comparing object identity.
When you pass an object to the value
prop, by
will default to id
when present, but you can set it to any field you
like:
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
const departments = [
{ name: 'Marketing', contact: 'Durward Reynolds' },
{ name: 'HR', contact: 'Kenton Towne' },
{ name: 'Sales', contact: 'Therese Wunsch' },
{ name: 'Finance', contact: 'Benedict Kessler' },
{ name: 'Customer service', contact: 'Katelyn Rohan' },
]
function Example({ selectedDepartment, onChange }) {
return (
<Listbox value={selectedDepartment} by="name" onChange={onChange}> <ListboxButton>{selectedDepartment.name}</ListboxButton>
<ListboxOptions anchor="bottom">
{departments.map((department) => (
<ListboxOption key={department.name} value={department} className="data-[focus]:bg-blue-100">
{department.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
You can also pass your own comparison function to the by
prop if you'd like complete control over how objects are
compared:
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
const departments = [
{ id: 1, name: 'Marketing', contact: 'Durward Reynolds' },
{ id: 2, name: 'HR', contact: 'Kenton Towne' },
{ id: 3, name: 'Sales', contact: 'Therese Wunsch' },
{ id: 4, name: 'Finance', contact: 'Benedict Kessler' },
{ id: 5, name: 'Customer service', contact: 'Katelyn Rohan' },
]
function compareDepartments(a, b) { return a.name.toLowerCase() === b.name.toLowerCase()}
function Example({ selectedDepartment, onChange }) {
return (
<Listbox value={selectedDepartment} by={compareDepartments} onChange={onChange}> <ListboxButton>{selectedDepartment.name}</ListboxButton>
<ListboxOptions anchor="bottom">
{departments.map((department) => (
<ListboxOption key={department.id} value={department} className="data-[focus]:bg-blue-100">
{department.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
To allow selecting multiple values in your listbox, use the multiple
prop and pass an array to value
instead of a
single option.
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPeople, setSelectedPeople] = useState([people[0], people[1]])
return (
<Listbox value={selectedPeople} onChange={setSelectedPeople} multiple> <ListboxButton>{selectedPeople.map((person) => person.name).join(', ')}</ListboxButton>
<ListboxOptions anchor="bottom">
{people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
This will keep the listbox open when you are selecting options, and choosing an option will toggle it in place.
Your onChange
handler will be called with an array containing all selected options any time an option is added or
removed.
By default, the Listbox
and its subcomponents each render a default element that is sensible for that component.
For example, ListboxButton
renders a button
, ListboxOptions
renders a div
, and ListboxOption
renders a div
.
By contrast, Listbox
does not render an element, and instead renders its children directly.
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 { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { forwardRef, useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
let MyCustomButton = forwardRef(function (props, ref) { return <button className="..." ref={ref} {...props} />})
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}> <ListboxButton as={MyCustomButton}>{selectedPerson.name}</ListboxButton> <ListboxOptions anchor="bottom" as="ul">
{people.map((person) => (
<ListboxOption as="li" key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
To tell an element to render its children directly with no wrapper element, use a Fragment
.
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { Fragment, useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton as={Fragment}> <button>{selectedPerson.name}</button> </ListboxButton> <ListboxOptions anchor="bottom">
{people.map((person) => (
<ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
)
}
While the ListboxButton
component is required when building custom listboxes, it's possible to build them in such a
way that the button is included by default and therefore not required each time you use your listbox. For example, an
API like this:
<MyListbox name="status">
<MyListboxOption value="active">Active</MyListboxOption>
<MyListboxOption value="paused">Paused</MyListboxOption>
<MyListboxOption value="delayed">Delayed</MyListboxOption>
<MyListboxOption value="canceled">Canceled</MyListboxOption>
</MyListbox>
To achieve this use the ListboxSelectedOption
component within your ListboxButton
to render the currently selected
listbox option.
For this to work you must pass the children
of your custom listbox (all the ListboxOption
instances) to both the
ListboxOptions
as it's children as well as to the ListboxSelectedOption
via the options
prop.
Then, to style the ListboxOption
based on whether it's being rendered in the ListboxButton
or in the
ListboxOptions
, use the selectedOption
render prop to conditionally apply different styles or render different
content.
import { Listbox, ListboxButton, ListboxOption, ListboxOptions, ListboxSelectedOption } from '@headlessui/react'
import { Fragment, useState } from 'react'
const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() {
const [selectedPerson, setSelectedPerson] = useState(people[0])
return (
<MyListbox value={selectedPerson} onChange={setSelectedPerson} placeholder="Select a person…">
{people.map((person) => (
<MyListboxOption key={person.id} value={person}>
{person.name}
</MyListboxOption>
))}
</MyListbox>
)
}
function MyListbox({ placeholder, children, ...props }) {
return (
<Listbox {...props}>
<ListboxButton>
<ListboxSelectedOption options={children} placeholder={<span className="opacity-50">{placeholder}</span>} /> </ListboxButton>
<ListboxOptions anchor="bottom">{children}</ListboxOptions> </Listbox>
)
}
function MyListboxOption({ children, ...props }) {
return (
<ListboxOption as={Fragment} {...props}>
{({ selectedOption }) => { return selectedOption ? children : <div className="data-[focus]:bg-blue-100">{children}</div> }} </ListboxOption>
)
}
The ListboxSelectedOption
component also has a placeholder
prop that you can use to render a placeholder when no
option is selected.
Command | Description |
Space, ArrowDown, or ArrowUpwhen | Opens listbox and focuses the selected item |
Enterwhen | Submit the parent form if it exists |
Escwhen listbox is open | Closes listbox |
ArrowDown or ArrowUpwhen listbox is open | Focuses previous/next non-disabled item |
ArrowLeft or ArrowRightwhen listbox is open and | Focuses previous/next non-disabled item |
Home or PageUpwhen listbox is open | Focuses first non-disabled item |
End or PageDownwhen listbox is open | Focuses last non-disabled item |
Enter or Spacewhen listbox is open | Selects the current item |
A–Z or a–zwhen listbox is open | Focuses first item that matches keyboard input |
Prop | Default | Description |
as | Fragment | String | Component The element or component the listbox should render as. |
invalid | false | Boolean Whether or not the listbox is invalid. |
disabled | false | Boolean Use this to disable the entire |
value | — | T The selected value. |
defaultValue | — | T The default value when using as an uncontrolled component. |
by | — | keyof T | ((a: T, z: T) => boolean) Use this to compare objects by a particular field, or pass your own comparison function for complete control over how objects are compared. When you pass an object to the |
onChange | — | (value: T) => void The function to call when a new option is selected. |
horizontal | false | Boolean When true, the orientation of the |
multiple | false | Boolean Whether multiple options can be selected or not. |
name | — | String The name used when using the listbox inside a form. |
form | — | String The id of the form that the listbox belongs to. If |
Data Attribute | Render Prop | Description |
— | value |
The selected value. |
data-open | open |
Whether or not the listbox is open. |
data-invalid | invalid |
Whether or not the listbox is invalid. |
data-disabled | disabled |
Whether or not the listbox is disabled. |
Prop | Default | Description |
as | button | String | Component The element or component the listbox button should render as. |
Data Attribute | Render Prop | Description |
— | value |
The selected value. |
data-open | open |
Whether or not the listbox is open. |
data-invalid | invalid |
Whether or not the listbox is invalid. |
data-disabled | disabled |
Whether or not the listbox button is disabled. |
data-focus | focus |
Whether or not the listbox button is focused. |
data-hover | hover |
Whether or not the listbox button is hovered. |
data-active | active |
Whether or not the listbox button is in an active or pressed state. |
data-autofocus | autofocus |
Whether or not the |
Renders the currently selected option, or a placeholder if no option is selected. Designed to be a child of
ListboxButton
.
Prop | Default | Description |
as | Fragment | String | Component The element or component the listbox selected option should render as. |
placeholder | — | ReactNode The React element to render when no option is selected. |
options | — | ReactNode[] Your full array of |
Prop | Default | Description |
as | div | String | Component The element or component the listbox options should render as. |
transition | false | Boolean Whether the element should render transition attributes like |
anchor | — | Object Configures the way the dropdown is anchored to the button. |
anchor.to | bottom | String Where to position the listbox options relative to the trigger. Use the values Alternatively, use the |
anchor.gap | 0 | Number | String The space between the listbox button and the listbox options. Can also be controlled using the |
anchor.offset | 0 | Number | String The distance the listbox options should be nudged from its original position. Can also be controlled using the |
anchor.padding | 0 | Number | String The minimum space between the listbox options and the viewport. Can also be controlled using the |
static | false | Boolean Whether the element should ignore the internally managed open/closed state. |
unmount | true | Boolean Whether the element should be unmounted or hidden based on the open/closed state. |
portal | false | Boolean Whether the element should be rendered in a portal. Automatically set to |
modal | true | Boolean Whether 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 listbox is open. |
Prop | Default | Description |
as | div | String | Component The element or component the listbox option should render as. |
value | — | T The option value. |
disabled | false | Boolean Whether or not the listbox option is disabled for keyboard navigation and ARIA purposes. |
Data Attribute | Render Prop | Description |
data-selected | selected |
Whether or not the listbox option is selected. |
data-disabled | disabled |
Whether or not the listbox option is disabled. |
data-focus | focus |
Whether or not the listbox option is focused. |
data-selectedOption | selectedOption |
Whether or not the listbox option is a child of |
If you're interested in predesigned Tailwind CSS select menu and listbox examples using Headless UI, check out Tailwind UI — 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.