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/closing of the listbox dropdown, use the provided Transition component. All you need to do is wrap the ListboxOptions in a Transition, and the transition will be applied automatically.

import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } 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>
<Transition
enter="duration-200 ease-out"
enterFrom="scale-95 opacity-0"
enterTo="scale-100 opacity-100"
leave="duration-300 ease-out"
leaveFrom="scale-100 opacity-100"
leaveTo="scale-95 opacity-0"
>
<ListboxOptions anchor="bottom" className="origin-top transition">
{people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions>
</Transition>
</Listbox> ) }

By default our built-in Transition component automatically communicates with the Listbox components to handle the open/closed states. However, 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 as="ul" value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton as={MyCustomButton}>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom" as="li"> {people.map((person) => (
<ListboxOption as="span" 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&hellip;">
      {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.

CommandDescription

Enter, Space, ArrowDown, or ArrowUpwhen ListboxButton is focused

Opens listbox and focuses the selected item

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 horizontal is set

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

The main listbox component.

PropDefaultDescription
asFragment
String | Component

The element or component the listbox should render as.

invalidfalse
Boolean

Whether or not the listbox is invalid.

disabledfalse
Boolean

Use this to disable the entire Listbox component & related children.

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 value prop, by will default toid when present.

onChange
(value: T) => void

The function to call when a new option is selected.

horizontalfalse
Boolean

When true, the orientation of the ListboxOptions will be horizontal, otherwise it will be vertical.

multiplefalse
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 name is provided but form is not, the listbox will add its state to the nearest ancestor form element.

Data AttributeRender PropDescription
value

T

The selected value.

data-openopen

Boolean

Whether or not the listbox is open.

data-invalidinvalid

Boolean

Whether or not the listbox is invalid.

data-disableddisabled

Boolean

Whether or not the listbox is disabled.

The Listbox's button.

PropDefaultDescription
asbutton
String | Component

The element or component the listbox button should render as.

Data AttributeRender PropDescription
value

T

The selected value.

data-openopen

Boolean

Whether or not the listbox is open.

data-invalidinvalid

Boolean

Whether or not the listbox is invalid.

data-disableddisabled

Boolean

Whether or not the listbox button is disabled.

data-focusfocus

Boolean

Whether or not the listbox button is focused.

data-hoverhover

Boolean

Whether or not the listbox button is hovered.

data-activeactive

Boolean

Whether or not the listbox button is in an active or pressed state.

data-autofocusautofocus

Boolean

Whether or not the autoFocus prop was set to true.

Renders the currently selected option, or a placeholder if no option is selected. Designed to be a child of ListboxButton.

PropDefaultDescription
asFragment
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 ListboxOption React elements. ListboxSelectedOption will filter this list to find and render the currently selected option.

The component that directly wraps the list of options in your custom Listbox.

PropDefaultDescription
asdiv
String | Component

The element or component the listbox options should render as.

anchor
Object

Configures the way the dropdown is anchored to the button.

anchor.tobottom
String

Where to position the listbox options relative to the trigger.

Use the values top, right, bottom, left to center the listbox options along the appropriate edge, or combine it with start or end to align the listbox options to a specific corner, such as top start or bottom end.

Alternatively, use the selection option to position the currently selected option directly above the listbox button.

anchor.gap0
Number | String

The space between the listbox button and the listbox options.

Can also be controlled using the --anchor-gap CSS variable.

anchor.offset0
Number | String

The distance the listbox options should be nudged from its original position.

Can also be controlled using the --anchor-offset CSS variable.

anchor.padding0
Number | String

The minimum space between the listbox options and the viewport.

Can also be controlled using the --anchor-padding CSS variable.

staticfalse
Boolean

Whether the element should ignore the internally managed open/closed state.

unmounttrue
Boolean

Whether the element should be unmounted or hidden based on the open/closed state.

portalfalse
Boolean

Whether the element should be rendered in a portal.

Automatically set to true when anchor prop is set.

modaltrue
Boolean

Whether to enable accessibility features like scroll locking, focus trapping, and making other elements inert.

Data AttributeRender PropDescription
data-openopen

Boolean

Whether or not the listbox is open.

Used to wrap each item within your Listbox.

PropDefaultDescription
asdiv
String | Component

The element or component the listbox option should render as.

value
T

The option value.

disabledfalse
Boolean

Whether or not the listbox option is disabled for keyboard navigation and ARIA purposes.

Data AttributeRender PropDescription
data-selectedselected

Boolean

Whether or not the listbox option is selected.

data-disableddisabled

Boolean

Whether or not the listbox option is disabled.

data-focusfocus

Boolean

Whether or not the listbox option is focused.

data-selectedOptionselectedOption

Boolean

Whether or not the listbox option is a child of ListboxSelectedOption.

If you're interested in predesigned component examples using Headless UI and Tailwind CSS, 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.