Skip to content
Popover

A popover is an interactive mini-dialog floating element that displays information related to an anchor element when the element is clicked.

Essentials

An accessible popover component has the following:

  • Dynamic anchor positioning: The popover will remain attached to its reference element and remain in view for the user regardless of where it is positioned on the screen.
  • Events: When the reference element is clicked, it toggles the popover open or closed.
  • Dismissal: When the user presses the esc key or outside the popover while the it is open, it closes.
  • Role: The elements are given relevant role and ARIA attributes to be accessible to screen readers.
  • Focus management: Focus is managed for non-modal or modal behavior.

Examples

Both of these examples have sections explaining them in-depth below.

Basic popover

CodeSandbox demo

This example demonstrates how to create a popover for use in a single instance to familiarize yourself with the fundamentals.

Let’s walk through the example:

Open state

import {useState} from 'react';
 
function Popover() {
  const [isOpen, setIsOpen] = useState(false);
}
import {useState} from 'react';
 
function Popover() {
  const [isOpen, setIsOpen] = useState(false);
}

isOpenisOpen determines whether or not the popover is currently open on the screen. It is used for conditional rendering.

useFloating hook

The useFloating()useFloating() hook provides positioning and context for our popover. We need to pass it some information:

  • openopen: The open state from our useState()useState() hook above.
  • onOpenChangeonOpenChange: A callback function that will be called when the popover is opened or closed. We’ll use this to update our isOpenisOpen state.
  • middlewaremiddleware: Import and pass middleware to the array that ensure the popover remains on the screen, no matter where it ends up being positioned.
  • whileElementsMountedwhileElementsMounted: Ensure the popover remains anchored to the reference element by updating the position when necessary, only while both the reference and floating elements are mounted for performance.
import {
  useFloating,
  autoUpdate,
  offset,
  flip,
  shift,
} from '@floating-ui/react';
 
function Popover() {
  const [isOpen, setIsOpen] = useState(false);
 
  const {x, y, strategy, refs, context} = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen,
    middleware: [offset(10), flip(), shift()],
    whileElementsMounted: autoUpdate,
  });
}
import {
  useFloating,
  autoUpdate,
  offset,
  flip,
  shift,
} from '@floating-ui/react';
 
function Popover() {
  const [isOpen, setIsOpen] = useState(false);
 
  const {x, y, strategy, refs, context} = useFloating({
    open: isOpen,
    onOpenChange: setIsOpen,
    middleware: [offset(10), flip(), shift()],
    whileElementsMounted: autoUpdate,
  });
}

Interaction hooks

Interaction hooks return objects containing keys of props that enable the popover to be opened, closed, or accessible to screen readers.

Using the contextcontext that was returned from the hook, call the interaction hooks:

import {
  // ...
  useClick,
  useDismiss,
  useRole,
  useInteractions,
} from '@floating-ui/react';
 
function Popover() {
  const [isOpen, setIsOpen] = useState(false);
 
  const {x, y, reference, floating, strategy, context} =
    useFloating({
      open: isOpen,
      onOpenChange: setIsOpen,
      middleware: [offset(10), flip(), shift()],
      whileElementsMounted: autoUpdate,
    });
 
  const click = useClick(context);
  const dismiss = useDismiss(context);
  const role = useRole(context);
 
  // Merge all the interactions into prop getters
  const {getReferenceProps, getFloatingProps} = useInteractions([
    click,
    dismiss,
    role,
  ]);
}
import {
  // ...
  useClick,
  useDismiss,
  useRole,
  useInteractions,
} from '@floating-ui/react';
 
function Popover() {
  const [isOpen, setIsOpen] = useState(false);
 
  const {x, y, reference, floating, strategy, context} =
    useFloating({
      open: isOpen,
      onOpenChange: setIsOpen,
      middleware: [offset(10), flip(), shift()],
      whileElementsMounted: autoUpdate,
    });
 
  const click = useClick(context);
  const dismiss = useDismiss(context);
  const role = useRole(context);
 
  // Merge all the interactions into prop getters
  const {getReferenceProps, getFloatingProps} = useInteractions([
    click,
    dismiss,
    role,
  ]);
}
  • useClick()useClick() adds the ability to toggle the popover open or closed when the reference element is clicked.
  • useDismiss()useDismiss() adds the ability to dismiss the popover when the user presses the esc key or presses outside of the popover.
  • useRole()useRole() adds the correct ARIA attributes for a dialogdialog to the popover and reference elements.

Finally, useInteractions()useInteractions() merges all of their props into prop getters which can be used for rendering.

Rendering

Now we have all the variables and hooks set up, we can render out our elements.

function Popover() {
  // ...
  return (
    <>
      <button ref={refs.setReference} {...getReferenceProps()}>
        Reference element
      </button>
      {isOpen && (
        <FloatingFocusManager context={context} modal={false}>
          <div
            ref={refs.setFloating}
            style={{
              position: strategy,
              top: y ?? 0,
              left: x ?? 0,
              width: 'max-content',
            }}
            {...getFloatingProps()}
          >
            Popover element
          </div>
        </FloatingFocusManager>
      )}
    </>
  );
}
function Popover() {
  // ...
  return (
    <>
      <button ref={refs.setReference} {...getReferenceProps()}>
        Reference element
      </button>
      {isOpen && (
        <FloatingFocusManager context={context} modal={false}>
          <div
            ref={refs.setFloating}
            style={{
              position: strategy,
              top: y ?? 0,
              left: x ?? 0,
              width: 'max-content',
            }}
            {...getFloatingProps()}
          >
            Popover element
          </div>
        </FloatingFocusManager>
      )}
    </>
  );
}
  • {...getReferenceProps()}{...getReferenceProps()} / {...getFloatingProps()}{...getFloatingProps()} spreads the props from the interaction hooks onto the relevant elements. They contain props like onClickonClick, aria-expandedaria-expanded, etc.
  • <FloatingFocusManager /><FloatingFocusManager /> is a component that manages focus of the popover for non-modal or modal behavior. It should directly wrap the floating element and only be rendered when the popover is also rendered. FloatingFocusManager docs.

In the above example we used non-modal focus management, but the focus management behavior of a popover can be either modal or non-modal. They are differentiated as follows:

  • The popover and its contents are the only elements that can receive focus. When the popover is open, the user cannot interact with the rest of the page (nor can screen readers) until the popover is closed.
  • Needs an explicit close button (though, it can be visually hidden).

This behavior is the default:

<FloatingFocusManager context={context}>
  <div />
</FloatingFocusManager>
<FloatingFocusManager context={context}>
  <div />
</FloatingFocusManager>

Non-modal

  • The popover and its contents can receive focus, but the user can still interact with the rest of the page too.
  • When tabbing outside of it, the popover automatically closes when it loses focus and the next focusable element in the natural DOM order receives focus.
  • Does not need an explicit close button.

This behavior can be configured with the modalmodal prop like so:

<FloatingFocusManager context={context} modal={false}>
  <div />
</FloatingFocusManager>
<FloatingFocusManager context={context} modal={false}>
  <div />
</FloatingFocusManager>

Reusable popover component

CodeSandbox demo

It is better to create a reusable component API that can be used in a variety of different scenarios more easily. We can place all of our hooks into a single custom hook for better reusability, which is then used by a controller component which encapsulates the state.

The reusable component can:

  • Be uncontrolled or controlled
  • Accept any element as the <PopoverTrigger /><PopoverTrigger />
  • Read the open state to change styles
function App() {
  return (
    <Popover>
      <PopoverTrigger>My trigger</PopoverTrigger>
      <PopoverContent>
        <PopoverHeading>My popover heading</PopoverHeading>
        <PopoverDescription>
          My popover description
        </PopoverDescription>
        <PopoverClose>Close</PopoverClose>
      </PopoverContent>
    </Popover>
  );
}
function App() {
  return (
    <Popover>
      <PopoverTrigger>My trigger</PopoverTrigger>
      <PopoverContent>
        <PopoverHeading>My popover heading</PopoverHeading>
        <PopoverDescription>
          My popover description
        </PopoverDescription>
        <PopoverClose>Close</PopoverClose>
      </PopoverContent>
    </Popover>
  );
}

Controller component

  • <Popover /><Popover />

This is the controller component that manages the popover’s state and provides the API to the rest of the components.

Render components

These components read the context provided by the root Popover component and render the appropriate elements.

The components must be wrapped in forwardRef()forwardRef() to allow refs, and should merge the refs to ensure all refs are preserved and forwarded to the element. Props are also merged to prevent overwriting.

  • <PopoverTrigger /><PopoverTrigger /> is the trigger button the popover is attached to. This accepts an asChildasChild prop if you want to attach it to a custom element. It also has a data-statedata-state attached to style based on the open/closed state.
  • <PopoverContent /><PopoverContent /> is the popover element, which can contain any children (React nodes).
  • <PopoverHeading /><PopoverHeading /> (optional) is the heading element for the popover.
  • <PopoverDescription /><PopoverDescription /> (optional) is the description element for the popover.
  • <PopoverClose /><PopoverClose /> (optional) is the close button for the popover when using modal focus management.