Disclosure

A simple, accessible foundation for building custom UIs that show and hide content, like togglable accordion panels.

To get started, install Headless UI via npm:

npm install @headlessui/react

Disclosures are built using the Disclosure, DisclosureButton, and DisclosurePanel components.

The button will automatically open/close the panel when clicked, and all components will receive the appropriate aria-* related attributes like aria-expanded and aria-controls.

import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'

function Example() {
  return (
    <Disclosure>
      <DisclosureButton className="py-2">Is team pricing available?</DisclosureButton>
      <DisclosurePanel className="text-gray-500">
        Yes! You can purchase a license that you can share with your entire team.
      </DisclosurePanel>
    </Disclosure>
  )
}

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 disclosure 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 DisclosureButton component exposes a data-open attribute, which tells you if the disclosure is currently open.

<!-- Rendered `Disclosure` -->
<button data-open>Do you offer technical support?</button>
<div data-open>No</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 { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'

function Example() {
  return (
    <Disclosure>
      <DisclosureButton className="group flex items-center gap-2">
        Do you offer technical support?
<ChevronDownIcon className="w-5 group-data-[open]:rotate-180" />
</DisclosureButton> <DisclosurePanel>No</DisclosurePanel> </Disclosure> ) }

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 DisclosureButton component exposes an open state, which tells you if the disclosure is currently open.

import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import clsx from 'clsx'

function Example() {
  return (
    <Disclosure>
{({ open }) => (
<> <DisclosureButton className="flex items-center gap-2"> Do you offer technical support?
<ChevronDownIcon className={clsx('w-5', open && 'rotate-180')} />
</DisclosureButton> <DisclosurePanel>No</DisclosurePanel> </>
)}
</Disclosure> ) }

See the component API for a list of all the available render props.

To animate the opening/closing of the disclosure panel, use the provided Transition component. All you need to do is wrap the DisclosurePanel in a Transition, and the transition will be applied automatically.

import { Disclosure, DisclosureButton, DisclosurePanel, Transition } from '@headlessui/react'

function Example() {
  return (
    <Disclosure as="div" className="w-full max-w-md">
      <DisclosureButton className="w-full border-b pb-2 text-left">Is team pricing available?</DisclosureButton>
      <div className="overflow-hidden py-2">
<Transition
enter="duration-200 ease-out"
enterFrom="opacity-0 -translate-y-6"
enterTo="opacity-100 translate-y-0"
leave="duration-300 ease-out"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 -translate-y-6"
>
<DisclosurePanel className="origin-top transition">
Yes! You can purchase a license that you can share with your entire team. </DisclosurePanel> </Transition> </div> </Disclosure> ) }

By default our built-in Transition component automatically communicates with the Disclosure 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 menu with Framer Motion, add the static prop to the DisclosurePanel component and then conditionally render it based on the open render prop:

import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'
import { AnimatePresence, easeOut, motion } from 'framer-motion'

function Example() {
  return (
    <Disclosure as="div" className="w-full max-w-md">
{({ open }) => (
<> <DisclosureButton className="w-full border-b pb-2 text-left">Is team pricing available?</DisclosureButton> <div className="overflow-hidden py-2"> <AnimatePresence> {open && ( <DisclosurePanel
static
as={motion.div} initial={{ opacity: 0, y: -24 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -24 }} transition={{ duration: 0.2, ease: easeOut }} className="origin-top" >
Yes! You can purchase a license that you can share with your entire team. </DisclosurePanel> )} </AnimatePresence> </div> </>
)}
</Disclosure> ) }

To close a disclosure manually when clicking a child of its panel, render that child as a CloseButton. You can use the as prop to customize which element is being rendered.

import { CloseButton, Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'
import MyLink from './MyLink'

function Example() {
  return (
    <Disclosure>
      <DisclosureButton>Open mobile menu</DisclosureButton>
      <DisclosurePanel>
<CloseButton as={MyLink} href="/home">
Home
</CloseButton>
</DisclosurePanel> </Disclosure> ) }

This is especially useful when using disclosures for things like mobile menus that contain links where you want the disclosure to close when navigating to the next page.

The Disclosure and DisclosurePanel also expose a close render prop which you can use to imperatively close the panel, say after running an async action:

import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'

function Example() {
  return (
    <Disclosure>
      <DisclosureButton>Terms</DisclosureButton>
      <DisclosurePanel>
{({ close }) => (
<button
onClick={async () => {
await fetch('/accept-terms', { method: 'POST' })
close()
}}
>
Read and accept
</button>
)}
</DisclosurePanel> </Disclosure> ) }

By default the DisclosureButton receives focus after calling close, but you can change this by passing a ref into close(ref).

Finally, Headless UI also provides a useClose hook that can be used to imperatively close the nearest disclosure ancestor when you don't have easy access to the close render prop, such as in a nested component:

import { Disclosure, DisclosureButton, DisclosurePanel, useClose } from '@headlessui/react'

function MySearchForm() {
let close = useClose()
return ( <form onSubmit={(event) => { event.preventDefault() /* Perform search... */
close()
}}
>
<input type="search" /> <button type="submit">Submit</button> </form> ) } function Example() { return ( <Disclosure> <DisclosureButton>Filters</DisclosureButton> <DisclosurePanel> <MySearchForm /> {/* ... */} </DisclosurePanel> </Disclosure> ) }

The useClose hook must be used in a component that's nested within the Disclosure, otherwise it will not work.

Disclosure and its subcomponents each render a default element that is sensible for that component: the Button renders a <button>, Panel renders a <div>. By contrast, the root Disclosure component does not render an element, and instead renders its 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 { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'
import { forwardRef } from 'react'

let MyCustomButton = forwardRef(function (props, ref) {
return <button className="..." ref={ref} {...props} />
})
function Example() { return (
<Disclosure as="div">
<DisclosureButton as={MyCustomButton}>What languages do you support?</DisclosureButton>
<DisclosurePanel as="ul">
<li>HTML</li> <li>CSS</li> <li>JavaScript</li> </DisclosurePanel> </Disclosure> ) }

CommandDescription

Enter or Spacewhen a DisclosureButton is focused.

Toggles panel

The main disclosure component.

PropDefaultDescription
asFragment
String | Component

The element or component the disclosure should render as.

defaultOpenfalse
Boolean

Whether or not the Disclosure component should be open by default.

Data AttributeRender PropDescription
data-openopen

Boolean

Whether or not the disclosure is open.

close

(ref) => void

Closes the disclosure and refocuses DisclosureButton. Optionally pass in a ref or HTMLElement to focus that element instead.

The trigger component that toggles a Disclosure.

PropDefaultDescription
asbutton
String | Component

The element or component the disclosure button should render as.

autoFocusfalse
Boolean

Whether or not the disclosure button should receive focus when first rendered.

Data AttributeRender PropDescription
data-openopen

Boolean

Whether or not the disclosure is open.

data-focusfocus

Boolean

Whether or not the disclosure button is focused.

data-hoverhover

Boolean

Whether or not the disclosure button is hovered.

data-activeactive

Boolean

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

data-autofocusautofocus

Boolean

Whether or not the autoFocus prop was set to true.

This component contains the contents of your disclosure.

PropDefaultDescription
asdiv
String | Component

The element or component the disclosure panel should render as.

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.

Data AttributeRender PropDescription
data-openopen

Boolean

Whether or not the disclosure is open.

close

(ref) => void

Closes the disclosure and refocuses DisclosureButton. Optionally pass in a ref or HTMLElement to focus that element instead.

This button will close the nearest DisclosurePanel ancestor when clicked. Alternatively, use the useClose hook to imperatively close the disclosure panel.

PropDefaultDescription
asbutton
String | Component

The element or component the close button should render as.

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.