beta

Popover

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

installyarn add react-aria-components
version1.0.0-beta.1
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} viewBox="0 0 12 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} viewBox="0 0 12 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}
        viewBox="0 0 12 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: 1rem;
  text-align: center;
  margin: 0;
  outline: none;
  padding: 6px 10px;
  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: 1rem;
  text-align: center;
  margin: 0;
  outline: none;
  padding: 6px 10px;
  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: 1rem;
  text-align: center;
  margin: 0;
  outline: none;
  padding: 6px 10px;
  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.

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

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

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

<DialogTrigger>
  <Button />
  <Popover>
    <OverlayArrow />
    <Dialog />
  </Popover>
</DialogTrigger>

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';

interface MyPopoverProps extends Omit<PopoverProps, 'children'> {
  children: React.ReactNode;
}

function MyPopover({ children, ...props }: MyPopoverProps) {
  return (
    <Popover {...props}>
      <OverlayArrow>
        <svg width={12} height={12} viewBox="0 0 12 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';

interface MyPopoverProps
  extends Omit<PopoverProps, 'children'> {
  children: React.ReactNode;
}

function MyPopover({ children, ...props }: MyPopoverProps) {
  return (
    <Popover {...props}>
      <OverlayArrow>
        <svg width={12} height={12} viewBox="0 0 12 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';

interface MyPopoverProps
  extends
    Omit<
      PopoverProps,
      'children'
    > {
  children:
    React.ReactNode;
}

function MyPopover(
  { children, ...props }:
    MyPopoverProps
) {
  return (
    <Popover {...props}>
      <OverlayArrow>
        <svg
          width={12}
          height={12}
          viewBox="0 0 12 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>

Positioning#


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>

Controlled open state#


The above examples have shown Popover used within a <DialogTrigger>, which handles opening the popover when a button is clicked, and positioning relative to the trigger. This is convenient, but there are cases where you want to show a popover programmatically rather than as a result of a user action, or render the <Popover> in a different part of the JSX tree.

To do this, you can manage the popover's isOpen state yourself and provide it as a prop to the <Popover> element. The onOpenChange prop will be called when the user closes the popover, and should be used to update your state. In addition, the triggerRef prop must be set to the element that the popover should be positioned relative to.

function Example() {
  let [isOpen, setOpen] = React.useState(false);
  let triggerRef = React.useRef(null);

  return (
    <>
      <Button onPress={() => setOpen(true)}>Trigger</Button>
      <span ref={triggerRef} style={{ paddingLeft: 12 }}>
        Popover will be positioned relative to me
      </span>
      <MyPopover triggerRef={triggerRef} isOpen={isOpen} onOpenChange={setOpen}>
        This is a popover.
      </MyPopover>
    </>
  );
}
function Example() {
  let [isOpen, setOpen] = React.useState(false);
  let triggerRef = React.useRef(null);

  return (
    <>
      <Button onPress={() => setOpen(true)}>Trigger</Button>
      <span ref={triggerRef} style={{ paddingLeft: 12 }}>
        Popover will be positioned relative to me
      </span>
      <MyPopover
        triggerRef={triggerRef}
        isOpen={isOpen}
        onOpenChange={setOpen}
      >
        This is a popover.
      </MyPopover>
    </>
  );
}
function Example() {
  let [isOpen, setOpen] =
    React.useState(
      false
    );
  let triggerRef = React
    .useRef(null);

  return (
    <>
      <Button
        onPress={() =>
          setOpen(true)}
      >
        Trigger
      </Button>
      <span
        ref={triggerRef}
        style={{
          paddingLeft: 12
        }}
      >
        Popover will be
        positioned
        relative to me
      </span>
      <MyPopover
        triggerRef={triggerRef}
        isOpen={isOpen}
        onOpenChange={setOpen}
      >
        This is a
        popover.
      </MyPopover>
    </>
  );
}

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.
isOpenbooleanWhether the overlay is open by default (controlled).
defaultOpenbooleanWhether the overlay is open by default (uncontrolled).
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.
Events
NameTypeDescription
onOpenChange( (isOpen: boolean )) => voidHandler that is called when the overlay's open state changes.
Layout
NameTypeDescription
slotstringnull

A slot name for the component. Slots allow the component to receive props from a parent component. An explicit null value indicates that the local props completely override all props received from a parent.

Sizing
NameTypeDescription
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 data attributes, which you can target in CSS selectors. 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 popover 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.

Advanced customization#


Contexts#

All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps).

ComponentContextPropsRef
PopoverPopoverContextPopoverPropsHTMLElement

This example shows a HelpTrigger component that wraps a button and a popover and shows the popover when the user presses the ? key. It uses ButtonContext to provide a keyboard listener and ref to a nested button, and PopoverContext to provide the trigger ref and open state to the nested popover.

import {ButtonContext, PopoverContext} from 'react-aria-components';

interface HelpTriggerProps {
  children?: React.ReactNode;
}

function HelpTrigger({ children }: HelpTriggerProps) {
  let triggerRef = React.useRef(null);
  let [isOpen, setOpen] = React.useState(false);
  let onKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === '?') {
      setOpen(true);
    }
  };

  return (
    <ButtonContext.Provider value={{ onKeyDown, ref: triggerRef }}>
      <PopoverContext.Provider
        value={{ triggerRef, isOpen, onOpenChange: setOpen }}
      >        {children}
      </PopoverContext.Provider>
    </ButtonContext.Provider>
  );
}

<HelpTrigger>
  <Button>Press ? for help</Button>
  <MyPopover>
    Do you need help?
  </MyPopover>
</HelpTrigger>
import {
  ButtonContext,
  PopoverContext
} from 'react-aria-components';

interface HelpTriggerProps {
  children?: React.ReactNode;
}

function HelpTrigger({ children }: HelpTriggerProps) {
  let triggerRef = React.useRef(null);
  let [isOpen, setOpen] = React.useState(false);
  let onKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === '?') {
      setOpen(true);
    }
  };

  return (
    <ButtonContext.Provider
      value={{ onKeyDown, ref: triggerRef }}
    >
      <PopoverContext.Provider
        value={{
          triggerRef,
          isOpen,
          onOpenChange: setOpen
        }}
      >        {children}
      </PopoverContext.Provider>
    </ButtonContext.Provider>
  );
}

<HelpTrigger>
  <Button>Press ? for help</Button>
  <MyPopover>
    Do you need help?
  </MyPopover>
</HelpTrigger>
import {
  ButtonContext,
  PopoverContext
} from 'react-aria-components';

interface HelpTriggerProps {
  children?:
    React.ReactNode;
}

function HelpTrigger(
  { children }:
    HelpTriggerProps
) {
  let triggerRef = React
    .useRef(null);
  let [isOpen, setOpen] =
    React.useState(
      false
    );
  let onKeyDown = (
    e: React.KeyboardEvent
  ) => {
    if (e.key === '?') {
      setOpen(true);
    }
  };

  return (
    <ButtonContext.Provider
      value={{
        onKeyDown,
        ref: triggerRef
      }}
    >
      <PopoverContext.Provider
        value={{
          triggerRef,
          isOpen,
          onOpenChange:
            setOpen
        }}
      >        {children}
      </PopoverContext.Provider>
    </ButtonContext.Provider>
  );
}

<HelpTrigger>
  <Button>
    Press ? for help
  </Button>
  <MyPopover>
    Do you need help?
  </MyPopover>
</HelpTrigger>

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.