alpha

Popover

A popover is an overlay element positioned relative to a trigger.

installyarn add react-aria-components
version1.0.0-alpha.0
usageimport {Popover} from 'react-aria-components'

Example#


import {Button, Dialog, DialogTrigger, OverlayArrow, Popover} from 'react-aria-components';

<DialogTrigger>
  <Button>Open popover</Button>
  <Popover>
    <OverlayArrow>
      <svg width={12} height={12}>
        <path d="M0 0,L6 6,L12 0" />
      </svg>
    </OverlayArrow>
    <Dialog>
      This is a popover.
    </Dialog>
  </Popover>
</DialogTrigger>
import {
  Button,
  Dialog,
  DialogTrigger,
  OverlayArrow,
  Popover
} from 'react-aria-components';

<DialogTrigger>
  <Button>Open popover</Button>
  <Popover>
    <OverlayArrow>
      <svg width={12} height={12}>
        <path d="M0 0,L6 6,L12 0" />
      </svg>
    </OverlayArrow>
    <Dialog>
      This is a popover.
    </Dialog>
  </Popover>
</DialogTrigger>
import {
  Button,
  Dialog,
  DialogTrigger,
  OverlayArrow,
  Popover
} from 'react-aria-components';

<DialogTrigger>
  <Button>
    Open popover
  </Button>
  <Popover>
    <OverlayArrow>
      <svg
        width={12}
        height={12}
      >
        <path d="M0 0,L6 6,L12 0" />
      </svg>
    </OverlayArrow>
    <Dialog>
      This is a
      popover.
    </Dialog>
  </Popover>
</DialogTrigger>
Show CSS
.react-aria-Popover {
  --background-color: var(--page-background);
  --border-color: var(--spectrum-global-color-gray-400);

  border: 1px solid var(--border-color);
  box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
  border-radius: 6px;
  background: var(--background-color);
  outline: none;
  padding: 30px;
  max-width: 250px;

  .react-aria-OverlayArrow svg {
    display: block;
    fill: var(--background-color);
    stroke: var(--border-color);
    stroke-width: 1px;
  }

  &[data-placement=top] {
    margin-bottom: 6px;
    --origin: translateY(8px);
  }

  &[data-placement=bottom] {
    margin-top: 6px;
    --origin: translateY(-8px);
    & .react-aria-OverlayArrow svg {
      transform: rotate(180deg);
    }
  }

  &[data-placement=right] {
    margin-left: 6px;
    --origin: translateX(-8px);
    & .react-aria-OverlayArrow svg {
      transform: rotate(90deg);
    }
  }

  &[data-placement=left] {
    margin-right: 6px;
    --origin: translateX(8px);
    & .react-aria-OverlayArrow svg {
      transform: rotate(-90deg);
    }
  }

  &[data-entering] {
    animation: slide 200ms;
  }

  &[data-exiting] {
    animation: slide 200ms reverse ease-in;
  }
}

@keyframes slide {
  from {
    transform: var(--origin);
    opacity: 0;
  }

  to {
    transform: translateY(0);
    opacity: 1;
  }
}

.react-aria-Dialog {
  outline: none;
}

.react-aria-Button {
  background: var(--spectrum-global-color-gray-50);
  border: 1px solid var(--spectrum-global-color-gray-400);
  border-radius: 4px;
  color: var(--spectrum-alias-text-color);
  appearance: none;
  vertical-align: middle;
  font-size: 1.2rem;
  text-align: center;
  margin: 0;
  outline: none;
  padding: 6px;
  transition: border-color 200ms;

  &[data-hovered] {
    border-color: var(--spectrum-global-color-gray-500);
  }

  &[data-pressed] {
    box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
    background: var(--spectrum-global-color-gray-100);
    border-color: var(--spectrum-global-color-gray-600);
  }

  &[data-focus-visible] {
    border-color: slateblue;
    box-shadow: 0 0 0 1px slateblue;
  }
}

@media (forced-colors: active) {
  .react-aria-Popover {
    --background-color: Canvas;
    --border-color: ButtonBorder;
  }
}
.react-aria-Popover {
  --background-color: var(--page-background);
  --border-color: var(--spectrum-global-color-gray-400);

  border: 1px solid var(--border-color);
  box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
  border-radius: 6px;
  background: var(--background-color);
  outline: none;
  padding: 30px;
  max-width: 250px;

  .react-aria-OverlayArrow svg {
    display: block;
    fill: var(--background-color);
    stroke: var(--border-color);
    stroke-width: 1px;
  }

  &[data-placement=top] {
    margin-bottom: 6px;
    --origin: translateY(8px);
  }

  &[data-placement=bottom] {
    margin-top: 6px;
    --origin: translateY(-8px);
    & .react-aria-OverlayArrow svg {
      transform: rotate(180deg);
    }
  }

  &[data-placement=right] {
    margin-left: 6px;
    --origin: translateX(-8px);
    & .react-aria-OverlayArrow svg {
      transform: rotate(90deg);
    }
  }

  &[data-placement=left] {
    margin-right: 6px;
    --origin: translateX(8px);
    & .react-aria-OverlayArrow svg {
      transform: rotate(-90deg);
    }
  }

  &[data-entering] {
    animation: slide 200ms;
  }

  &[data-exiting] {
    animation: slide 200ms reverse ease-in;
  }
}

@keyframes slide {
  from {
    transform: var(--origin);
    opacity: 0;
  }

  to {
    transform: translateY(0);
    opacity: 1;
  }
}

.react-aria-Dialog {
  outline: none;
}

.react-aria-Button {
  background: var(--spectrum-global-color-gray-50);
  border: 1px solid var(--spectrum-global-color-gray-400);
  border-radius: 4px;
  color: var(--spectrum-alias-text-color);
  appearance: none;
  vertical-align: middle;
  font-size: 1.2rem;
  text-align: center;
  margin: 0;
  outline: none;
  padding: 6px;
  transition: border-color 200ms;

  &[data-hovered] {
    border-color: var(--spectrum-global-color-gray-500);
  }

  &[data-pressed] {
    box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
    background: var(--spectrum-global-color-gray-100);
    border-color: var(--spectrum-global-color-gray-600);
  }

  &[data-focus-visible] {
    border-color: slateblue;
    box-shadow: 0 0 0 1px slateblue;
  }
}

@media (forced-colors: active) {
  .react-aria-Popover {
    --background-color: Canvas;
    --border-color: ButtonBorder;
  }
}
.react-aria-Popover {
  --background-color: var(--page-background);
  --border-color: var(--spectrum-global-color-gray-400);

  border: 1px solid var(--border-color);
  box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
  border-radius: 6px;
  background: var(--background-color);
  outline: none;
  padding: 30px;
  max-width: 250px;

  .react-aria-OverlayArrow svg {
    display: block;
    fill: var(--background-color);
    stroke: var(--border-color);
    stroke-width: 1px;
  }

  &[data-placement=top] {
    margin-bottom: 6px;
    --origin: translateY(8px);
  }

  &[data-placement=bottom] {
    margin-top: 6px;
    --origin: translateY(-8px);
    & .react-aria-OverlayArrow svg {
      transform: rotate(180deg);
    }
  }

  &[data-placement=right] {
    margin-left: 6px;
    --origin: translateX(-8px);
    & .react-aria-OverlayArrow svg {
      transform: rotate(90deg);
    }
  }

  &[data-placement=left] {
    margin-right: 6px;
    --origin: translateX(8px);
    & .react-aria-OverlayArrow svg {
      transform: rotate(-90deg);
    }
  }

  &[data-entering] {
    animation: slide 200ms;
  }

  &[data-exiting] {
    animation: slide 200ms reverse ease-in;
  }
}

@keyframes slide {
  from {
    transform: var(--origin);
    opacity: 0;
  }

  to {
    transform: translateY(0);
    opacity: 1;
  }
}

.react-aria-Dialog {
  outline: none;
}

.react-aria-Button {
  background: var(--spectrum-global-color-gray-50);
  border: 1px solid var(--spectrum-global-color-gray-400);
  border-radius: 4px;
  color: var(--spectrum-alias-text-color);
  appearance: none;
  vertical-align: middle;
  font-size: 1.2rem;
  text-align: center;
  margin: 0;
  outline: none;
  padding: 6px;
  transition: border-color 200ms;

  &[data-hovered] {
    border-color: var(--spectrum-global-color-gray-500);
  }

  &[data-pressed] {
    box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1);
    background: var(--spectrum-global-color-gray-100);
    border-color: var(--spectrum-global-color-gray-600);
  }

  &[data-focus-visible] {
    border-color: slateblue;
    box-shadow: 0 0 0 1px slateblue;
  }
}

@media (forced-colors: active) {
  .react-aria-Popover {
    --background-color: Canvas;
    --border-color: ButtonBorder;
  }
}

Features#


There is no built in way to create popovers in HTML. Popover helps achieve accessible popovers that can be styled as needed.

  • Styleable – States for entry and exit animations are included for easy styling, and an optional arrow element can be rendered.
  • Accessible – The trigger and popover are automatically associated semantically via ARIA. Content outside the popover is hidden from assistive technologies while it is open. The popover closes when interacting outside, or pressing the Escape key.
  • Focus management – Focus is moved into the popover on mount, and restored to the trigger element on unmount.
  • Positioning – The popover is positioned relative to the trigger element, and automatically flips and adjusts to avoid overlapping with the edge of the browser window.

Note: Popover only provides the overlay itself. It should be combined with Dialog to create fully accessible popovers. Other overlays such as menus may also be placed in a popover.

Anatomy#


Arrow (optional)Popover

A popover consists of a trigger element (e.g. button) and an overlay, which is positioned relative to the trigger. The overlay may contain a Dialog, or another element such as a Menu or ListBox when used within a component such as a Select or ComboBox.

Props#


Popover#

NameTypeDefaultDescription
triggerRefRefObject<Element>

The ref for the element which the popover positions itself with respect to.

When used within a trigger component such as DialogTrigger, MenuTrigger, Select, etc., this is set automatically. It is only required when used standalone.

placementPlacement'bottom'The placement of the element with respect to its anchor element.
containerPaddingnumber12

The placement padding that should be applied between the element and its surrounding container.

offsetnumber0

The additional offset applied along the main axis between the element and its anchor element.

crossOffsetnumber0

The additional offset applied along the cross axis between the element and its anchor element.

shouldFlipbooleantrue

Whether the element should flip its orientation (e.g. top to bottom or left to right) when there is insufficient room for it to render completely.

isNonModalboolean

Whether the popover is non-modal, i.e. elements outside the popover may be interacted with by assistive technologies.

Most popovers should not use this option as it may negatively impact the screen reader experience. Only use with components such as combobox, which are designed to handle this situation carefully.

isKeyboardDismissDisabledbooleanfalse

Whether pressing the escape key to close the popover should be disabled.

Most popovers should not use this option. When set to true, an alternative way to close the popover with a keyboard must be provided.

arrowSizenumber0Cross size of the overlay arrow in pixels.
boundaryElementElementdocument.bodyElement that that serves as the positioning boundary.
scrollRefRefObject<Element>overlayRefA ref for the scrollable region within the overlay.
shouldUpdatePositionbooleantrueWhether the overlay should update its position automatically.
arrowBoundaryOffsetnumber0The minimum distance the arrow's edge should be from the edge of the overlay element.
childrenReactNode( (values: PopoverRenderProps )) => ReactNodeThe children of the component. A function may be provided to alter the children based on component state.
classNamestring( (values: PopoverRenderProps )) => stringThe CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: PopoverRenderProps )) => CSSPropertiesThe inline style for the element. A function may be provided to compute the style based on component state.
Layout
NameTypeDefaultDescription
slotstringA slot name for the component. Slots allow the component to receive props from a parent component.
Sizing
NameTypeDefaultDescription
maxHeightnumber

The maxHeight specified for the overlay element. By default, it will take all space up to the current viewport height.

OverlayArrow#

OverlayArrow accepts all HTML attributes.

Styling#


React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin className attribute which can be targeted using CSS selectors. These follow the react-aria-ComponentName naming convention.

.react-aria-Popover {
  /* ... */
}
.react-aria-Popover {
  /* ... */
}
.react-aria-Popover {
  /* ... */
}

A custom className can also be specified on any component. This overrides the default className provided by React Aria with your own.

<Popover className="my-popover">
  {/* ... */}
</Popover>
<Popover className="my-popover">
  {/* ... */}
</Popover>
<Popover className="my-popover">
  {/* ... */}
</Popover>

In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using DOM attributes, which you can target in CSS selectors. These are ARIA attributes wherever possible, or data attributes when a relevant ARIA attribute does not exist. For example:

.react-aria-Popover[data-placement=left] {
  /* ... */
}
.react-aria-Popover[data-placement=left] {
  /* ... */
}
.react-aria-Popover[data-placement=left] {
  /* ... */
}

The className and style props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind.

<OverlayArrow
  className={({ placement }) =>
    placement === 'left' || placement === 'right' ? 'rotate-90' : 'rotate-0'}
>
  {/* ... */}
</OverlayArrow>
<OverlayArrow
  className={({ placement }) =>
    placement === 'left' || placement === 'right'
      ? 'rotate-90'
      : 'rotate-0'}
>
  {/* ... */}
</OverlayArrow>
<OverlayArrow
  className={(
    { placement }
  ) =>
    placement ===
        'left' ||
      placement ===
        'right'
      ? 'rotate-90'
      : 'rotate-0'}
>
  {/* ... */}
</OverlayArrow>

Popovers also support entry and exit animations via states exposed as data attributes and render props. Popover will automatically wait for any exit animations to complete before it is removed from the DOM.

.react-aria-Popover[data-entering] {
  animation: slide 300ms;
}

.react-aria-Popover[data-exiting] {
  animation: slide 300ms reverse;
}

@keyframes slide {
  /* ... */
}
.react-aria-Popover[data-entering] {
  animation: slide 300ms;
}

.react-aria-Popover[data-exiting] {
  animation: slide 300ms reverse;
}

@keyframes slide {
  /* ... */
}
.react-aria-Popover[data-entering] {
  animation: slide 300ms;
}

.react-aria-Popover[data-exiting] {
  animation: slide 300ms reverse;
}

@keyframes slide {
  /* ... */
}

The states, selectors, and render props for each component used in a Popover are documented below.

Popover#

A Popover can be targeted with the .react-aria-Popover CSS selector, or by overriding with a custom className. It supports the following states and render props:

NameCSS SelectorDescription
placement[data-placement="left | right | top | bottom"]The placement of the tooltip relative to the trigger.
isEntering[data-entering]Whether the popover is currently entering. Use this to apply animations.
isExiting[data-exiting]Whether the popover is currently exiting. Use this to apply animations.

OverlayArrow#

A OverlayArrow can be targeted with the .react-aria-OverlayArrow CSS selector, or by overriding with a custom className. It supports the following states and render props:

NameCSS SelectorDescription
placement[data-placement="left | right | top | bottom"]The placement of the overlay relative to the trigger.

Reusable wrappers#


If you will use a Popover in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency.

This example wraps Popover and all of its children together into a single component. Since the Dialog is built in, this means it can't be used for components like Select, Menu, and ComboBox. Exclude the dialog if your popover will be reused in these components.

import type {PopoverProps} from 'react-aria-components';

function MyPopover({children, ...props}: PopoverProps) {
  return (
    <Popover {...props}>
      <OverlayArrow>
        <svg width={12} height={12}><path d="M0 0,L6 6,L12 0" /></svg>
      </OverlayArrow>
      <Dialog>
        {children}
      </Dialog>
    </Popover>
  );
}

<DialogTrigger>
  <Button>Open popover</Button>
  <MyPopover>This is an example popover.</MyPopover>
</DialogTrigger>
import type {PopoverProps} from 'react-aria-components';

function MyPopover({ children, ...props }: PopoverProps) {
  return (
    <Popover {...props}>
      <OverlayArrow>
        <svg width={12} height={12}>
          <path d="M0 0,L6 6,L12 0" />
        </svg>
      </OverlayArrow>
      <Dialog>
        {children}
      </Dialog>
    </Popover>
  );
}

<DialogTrigger>
  <Button>Open popover</Button>
  <MyPopover>This is an example popover.</MyPopover>
</DialogTrigger>
import type {PopoverProps} from 'react-aria-components';

function MyPopover(
  { children, ...props }:
    PopoverProps
) {
  return (
    <Popover {...props}>
      <OverlayArrow>
        <svg
          width={12}
          height={12}
        >
          <path d="M0 0,L6 6,L12 0" />
        </svg>
      </OverlayArrow>
      <Dialog>
        {children}
      </Dialog>
    </Popover>
  );
}

<DialogTrigger>
  <Button>
    Open popover
  </Button>
  <MyPopover>
    This is an example
    popover.
  </MyPopover>
</DialogTrigger>

Usage#


The following examples show how to use the MyPopover component created in the above example.

Placement#

The popover's placement with respect to its anchor element can be adjusted using the placement prop. See Placement for a full list of available placement combinations. Popovers will also automatically flip to the opposite direction if there isn't enough space.

<div style={{ display: 'flex', gap: 8 }}>
  <DialogTrigger>
    <Button>⬅️</Button>
    <MyPopover placement="start">
      In left-to-right, this is on the left. In right-to-left, this is on the
      right.
    </MyPopover>
  </DialogTrigger>
  <DialogTrigger>
    <Button>⬆️</Button>
    <MyPopover placement="top">This popover is above the button.</MyPopover>
  </DialogTrigger>
  <DialogTrigger>
    <Button>⬇️</Button>
    <MyPopover placement="bottom">
      This popover is below the button.
    </MyPopover>
  </DialogTrigger>
  <DialogTrigger>
    <Button>➡️</Button>
    <MyPopover placement="end">
      In left-to-right, this is on the right. In right-to-left, this is on the
      left.
    </MyPopover>
  </DialogTrigger>
</div>
<div style={{ display: 'flex', gap: 8 }}>
  <DialogTrigger>
    <Button>⬅️</Button>
    <MyPopover placement="start">
      In left-to-right, this is on the left. In
      right-to-left, this is on the right.
    </MyPopover>
  </DialogTrigger>
  <DialogTrigger>
    <Button>⬆️</Button>
    <MyPopover placement="top">
      This popover is above the button.
    </MyPopover>
  </DialogTrigger>
  <DialogTrigger>
    <Button>⬇️</Button>
    <MyPopover placement="bottom">
      This popover is below the button.
    </MyPopover>
  </DialogTrigger>
  <DialogTrigger>
    <Button>➡️</Button>
    <MyPopover placement="end">
      In left-to-right, this is on the right. In
      right-to-left, this is on the left.
    </MyPopover>
  </DialogTrigger>
</div>
<div
  style={{
    display: 'flex',
    gap: 8
  }}
>
  <DialogTrigger>
    <Button>⬅️</Button>
    <MyPopover placement="start">
      In left-to-right,
      this is on the
      left. In
      right-to-left,
      this is on the
      right.
    </MyPopover>
  </DialogTrigger>
  <DialogTrigger>
    <Button>⬆️</Button>
    <MyPopover placement="top">
      This popover is
      above the button.
    </MyPopover>
  </DialogTrigger>
  <DialogTrigger>
    <Button>⬇️</Button>
    <MyPopover placement="bottom">
      This popover is
      below the button.
    </MyPopover>
  </DialogTrigger>
  <DialogTrigger>
    <Button>➡️</Button>
    <MyPopover placement="end">
      In left-to-right,
      this is on the
      right. In
      right-to-left,
      this is on the
      left.
    </MyPopover>
  </DialogTrigger>
</div>

Offset and cross offset#

The popover's offset with respect to its anchor element can be adjusted using the offset and crossOffset props. The offset prop controls the spacing applied along the main axis between the element and its anchor element whereas the crossOffset prop handles the spacing applied along the cross axis.

Below is a popover offset by an additional 50px above the trigger.

<DialogTrigger>
  <Button>Open popover</Button>
  <MyPopover placement="top" offset={50}>
    Offset by an additional 50px.
  </MyPopover>
</DialogTrigger>
<DialogTrigger>
  <Button>Open popover</Button>
  <MyPopover placement="top" offset={50}>
    Offset by an additional 50px.
  </MyPopover>
</DialogTrigger>
<DialogTrigger>
  <Button>
    Open popover
  </Button>
  <MyPopover
    placement="top"
    offset={50}
  >
    Offset by an
    additional 50px.
  </MyPopover>
</DialogTrigger>

Below is a popover cross offset by an additional 100px to the right of the trigger.

<DialogTrigger>
  <Button>Open popover</Button>
  <MyPopover placement="top" crossOffset={100}>
    Offset by an additional 100px.
  </MyPopover>
</DialogTrigger>
<DialogTrigger>
  <Button>Open popover</Button>
  <MyPopover placement="top" crossOffset={100}>
    Offset by an additional 100px.
  </MyPopover>
</DialogTrigger>
<DialogTrigger>
  <Button>
    Open popover
  </Button>
  <MyPopover
    placement="top"
    crossOffset={100}
  >
    Offset by an
    additional 100px.
  </MyPopover>
</DialogTrigger>

Flipping#

By default, usePopover attempts to flip popovers on the main axis in situations where the original placement would cause it to render out of view. This can be overridden by setting shouldFlip={false}. To see the difference between the two options, scroll this page so that the example below is near the bottom of the window.

<DialogTrigger>
  <Button>Default</Button>
  <MyPopover placement="bottom">
    This is a popover that will flip if it can't fully render below the button.
  </MyPopover>
</DialogTrigger>

<DialogTrigger>
  <Button>shouldFlip=false</Button>
  <MyPopover placement="bottom" shouldFlip={false}>
    This is a popover that won't flip if it can't fully render below the button.
  </MyPopover>
</DialogTrigger>
<DialogTrigger>
  <Button>Default</Button>
  <MyPopover placement="bottom">
    This is a popover that will flip if it can't fully
    render below the button.
  </MyPopover>
</DialogTrigger>
 <DialogTrigger>
  <Button>shouldFlip=false</Button>
  <MyPopover placement="bottom" shouldFlip={false}>
    This is a popover that won't flip if it can't fully
    render below the button.
  </MyPopover>
</DialogTrigger>
<DialogTrigger>
  <Button>
    Default
  </Button>
  <MyPopover placement="bottom">
    This is a popover
    that will flip if
    it can't fully
    render below the
    button.
  </MyPopover>
</DialogTrigger>
 <DialogTrigger>
  <Button>
    shouldFlip=false
  </Button>
  <MyPopover
    placement="bottom"
    shouldFlip={false}
  >
    This is a popover
    that won't flip if
    it can't fully
    render below the
    button.
  </MyPopover>
</DialogTrigger>

Container padding#

You can control the minimum padding required between the popover and the surrounding container via the containerPadding prop. This affects the positioning breakpoints that determine when it will attempt to flip.

The example below will maintain at least 50px between the popover and the edge of the browser window.

<DialogTrigger>
  <Button>Trigger</Button>
  <MyPopover placement="top" containerPadding={50}>
    This is a popover.
  </MyPopover>
</DialogTrigger>
<DialogTrigger>
  <Button>Trigger</Button>
  <MyPopover placement="top" containerPadding={50}>
    This is a popover.
  </MyPopover>
</DialogTrigger>
<DialogTrigger>
  <Button>
    Trigger
  </Button>
  <MyPopover
    placement="top"
    containerPadding={50}
  >
    This is a popover.
  </MyPopover>
</DialogTrigger>

Advanced customization#


Hooks#

If you need to customize things further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See usePopover for more details.