Popover

Popovers are perfect for floating panels with arbitrary content like navigation menus, mobile menus and flyout menus.

To get started, install Headless UI via npm:

npm install @headlessui/react

Popovers are built using the Popover, PopoverButton, and PopoverPanel components.

Clicking the PopoverButton will automatically open/close the PopoverPanel. When the panel is open, clicking anywhere outside of its contents, pressing the Escape key, or tabbing away from it will close the popover.

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
    <Popover className="relative">
      <PopoverButton>Solutions</PopoverButton>
      <PopoverPanel anchor="bottom" className="flex flex-col">
        <a href="/analytics">Analytics</a>
        <a href="/engagement">Engagement</a>
        <a href="/security">Security</a>
        <a href="/integrations">Integrations</a>
      </PopoverPanel>
    </Popover>
  )
}

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

<!-- Rendered `Popover` -->
<div data-open>
  <button data-open>Solutions</button>
  <div data-open>
    <a href="/insights">Insights</a>
    <a href="/automations">Automations</a>
    <a href="/reports">Reports</a>
  </div>
</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 { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'

function Example() {
  return (
<Popover className="group">
<PopoverButton className="flex items-center gap-2"> Solutions
<ChevronDownIcon className="size-5 group-data-[open]:rotate-180" />
</PopoverButton> <PopoverPanel anchor="bottom" className="flex flex-col"> <a href="/insights">Insights</a> <a href="/automations">Automations</a> <a href="/reports">Reports</a> </PopoverPanel> </Popover> ) }

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

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import clsx from 'clsx'

function Example() {
  return (
    <Popover>
{({ open }) => (
<> <PopoverButton className="flex items-center gap-2"> Solutions
<ChevronDownIcon className={clsx('size-5', open && 'rotate-180')} />
</PopoverButton> <PopoverPanel anchor="bottom" className="flex flex-col"> <a href="/insights">Insights</a> <a href="/automations">Automations</a> <a href="/reports">Reports</a> </PopoverPanel> </>
)}
</Popover> ) }

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

When rendering several related popovers, for example in a site's header navigation, use the PopoverGroup component. This ensures panels stay open while users are tabbing between popovers within a group, but closes any open panel once the user tabs outside of the group:

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
<PopoverGroup>
<Popover> <PopoverButton>Product</PopoverButton> <PopoverPanel>{/* ... */}</PopoverPanel> </Popover> <Popover> <PopoverButton>Solutions</PopoverButton> <PopoverPanel>{/* ... */}</PopoverPanel> </Popover> <Popover> <PopoverButton>Pricing</PopoverButton> <PopoverPanel>{/* ... */}</PopoverPanel> </Popover>
</PopoverGroup>
) }

The PopoverPanel has no width set by default, but you can add one using CSS:

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
    <Popover className="relative">
      <PopoverButton>Solutions</PopoverButton>
<PopoverPanel anchor="bottom" className="w-52">
<a href="/analytics">Analytics</a> <a href="/engagement">Engagement</a> <a href="/security">Security</a> <a href="/integrations">Integrations</a> </PopoverPanel> </Popover> ) }

If you'd like the panel width to match the PopoverButton width, use the --button-width CSS variable that's exposed on the PopoverPanel element:

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
    <Popover className="relative">
      <PopoverButton>Solutions</PopoverButton>
<PopoverPanel anchor="bottom" className="flex w-[var(--button-width)] flex-col">
<a href="/analytics">Analytics</a> <a href="/engagement">Engagement</a> <a href="/security">Security</a> <a href="/integrations">Integrations</a> </PopoverPanel> </Popover> ) }

Add the anchor prop to the PopoverPanel to automatically position the panel relative to the PopoverButton:

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
    <Popover className="relative">
      <PopoverButton>Solutions</PopoverButton>
<PopoverPanel anchor="bottom start" className="flex flex-col">
<a href="/analytics">Analytics</a> <a href="/engagement">Engagement</a> <a href="/security">Security</a> <a href="/integrations">Integrations</a> </PopoverPanel> </Popover> ) }

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

To control the gap between the button and the panel, use the --anchor-gap CSS variable:

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
    <Popover className="relative">
      <PopoverButton>Solutions</PopoverButton>
<PopoverPanel anchor="bottom start" className="flex flex-col [--anchor-gap:4px] sm:[--anchor-gap:8px]">
<a href="/analytics">Analytics</a> <a href="/engagement">Engagement</a> <a href="/security">Security</a> <a href="/integrations">Integrations</a> </PopoverPanel> </Popover> ) }

Additionally, you can use --anchor-offset to control the distance that the panel should be nudged from its original position, and --anchor-padding to control the minimum space that should exist between the panel 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 { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
    <Popover className="relative">
      <PopoverButton>Solutions</PopoverButton>
<PopoverPanel anchor={{ to: 'bottom start', gap: '4px' }} className="flex flex-col">
<a href="/analytics">Analytics</a> <a href="/engagement">Engagement</a> <a href="/security">Security</a> <a href="/integrations">Integrations</a> </PopoverPanel> </Popover> ) }

See the PopoverPanel API for more information about these options.

If you'd like to style a backdrop over your application UI whenever you open a popover, use the PopoverOverlay component:

import { Popover, PopoverButton, PopoverOverlay, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
    <Popover className="relative">
      <PopoverButton>Solutions</PopoverButton>
<PopoverOverlay className="fixed inset-0 bg-black/15" />
<PopoverPanel anchor="bottom" className="flex flex-col bg-white"> <a href="/analytics">Analytics</a> <a href="/engagement">Engagement</a> <a href="/security">Security</a> <a href="/integrations">Integrations</a> </PopoverPanel> </Popover> ) }

In this example, we put the PopoverOverlay before the Panel in the DOM so that it doesn't cover up the panel's contents.

But like all the other components, PopoverOverlay is completely headless, so how you style it is up to you.

To animate the opening and closing of the popover panel, wrap the PopoverPanel in a Transition and the transition will be applied automatically.

import { Popover, PopoverButton, PopoverPanel, Transition } from '@headlessui/react'

function Example() {
  return (
    <Popover>
      <PopoverButton>Solutions</PopoverButton>
<Transition
enter="duration-200 ease-out"
enterFrom="scale-95 opacity-0"
enterTo="scale-100 opacity-100"
leave="duration-200 ease-out"
leaveFrom="scale-100 opacity-100"
leaveTo="scale-95 opacity-0"
>
<PopoverPanel anchor="bottom" className="flex origin-top flex-col transition">
<a href="/analytics">Analytics</a> <a href="/engagement">Engagement</a> <a href="/security">Security</a> <a href="/integrations">Integrations</a> </PopoverPanel>
</Transition>
</Popover> ) }

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

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import { AnimatePresence, motion } from 'framer-motion'

function Example() {
  return (
    <Popover>
{({ open }) => (
<> <PopoverButton>Solutions</PopoverButton> <AnimatePresence>
{open && (
<PopoverPanel
static
as={motion.div} initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} anchor="bottom" className="flex origin-top flex-col" >
<a href="/analytics">Analytics</a> <a href="/engagement">Engagement</a> <a href="/security">Security</a> <a href="/integrations">Integrations</a> </PopoverPanel>
)}
</AnimatePresence> </>
)}
</Popover> ) }

Since popovers can contain interactive content like form controls, we can't automatically close them when you click something inside of them like we can with Menu components.

To close a popover 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, Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import MyLink from './MyLink'

function Example() {
  return (
    <Popover>
      <PopoverButton>Solutions</PopoverButton>
      <PopoverPanel anchor="bottom">
<CloseButton as={MyLink} href="/insights">
Insights
</CloseButton>
{/* ... */} </PopoverPanel> </Popover> ) }

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

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

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

By default the PopoverButton 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 popover ancestor when you don't have easy access to the close render prop, such as in a nested component:

import { Popover, PopoverButton, PopoverPanel, 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 ( <Popover> <PopoverButton>Filters</PopoverButton> <PopoverPanel> <MySearchForm /> {/* ... */} </PopoverPanel> </Popover> ) }

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

Popover and its subcomponents each render a default element that is sensible for that component: the Popover, Overlay, Panel, and Group components all render a <div>, and the Button component renders a <button>.

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 { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import { forwardRef } from 'react'

let MyCustomButton = forwardRef(function (props, ref) {
return <button className="..." ref={ref} {...props} />
})
function Example() { return (
<Popover as="nav">
<PopoverButton as={MyCustomButton}>Solutions</PopoverButton>
<PopoverPanel as="form">{/* ... */}</PopoverPanel>
</Popover> ) }

CommandDescription

Enter or Spacewhen a PopoverButton is focused.

Toggle panel

Esc

Closes any open popovers

Tab

Cycle through an open panel's contents

Tabbing out of an open panel will close that panel, and tabbing from one open panel to a sibling popover's button (within a PopoverGroup) closes the first panel

Shift + Tab

Cycle backwards through the focus order

The main popover component.

PropDefaultDescription
asdiv
String | Component

The element or component the popover should render as.

Data AttributeRender PropDescription
data-openopen

Boolean

Whether or not the popover is open.

close

(ref) => void

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

This can be used to create an overlay for your popover component. Clicking on the overlay will close the popover.

PropDefaultDescription
asdiv
String | Component

The element or component the popover overlay should render as.

Data AttributeRender PropDescription
data-openopen

Boolean

Whether or not the popover is open.

This is the trigger component to toggle a popover.

PropDefaultDescription
asbutton
String | Component

The element or component the popover button should render as.

disabledfalse
Boolean

Whether or not the popover button is disabled.

autoFocusfalse
Boolean

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

Data AttributeRender PropDescription
data-openopen

Boolean

Whether or not the popover is open.

data-focusfocus

Boolean

Whether or not the popover button is focused.

data-hoverhover

Boolean

Whether or not the popover button is hovered.

data-activeactive

Boolean

Whether or not the popover 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 popover.

PropDefaultDescription
asdiv
String | Component

The element or component the popover overlay should render as.

anchor
Object

Configures the way the panel is anchored to the button.

anchor.tobottom
String

Where to position the popover overlay relative to the trigger.

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

anchor.gap0
Number | String

The space between the popover button and the popover overlay.

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

anchor.offset0
Number | String

The distance the popover overlay 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 popover overlay 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 and focus trapping.

focusfalse
Boolean

This will force focus inside the PopoverPanel when the popover is open. It will also close the popover if focus left this component.

Data AttributeRender PropDescription
data-openopen

Boolean

Whether or not the popover is open.

close

(ref) => void

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

Link related sibling popovers by wrapping them in a PopoverGroup. Tabbing out of one PopoverPanel will focus the next popover's PopoverButton, and tabbing outside of the PopoverGroup completely will close all popovers inside the group.

PropDefaultDescription
asdiv
String | Component

The element or component the popover group should render as.

This button will close the nearest PopoverPanel ancestor when clicked. Alternatively, use the useClose hook to imperatively close the popover 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.