Popover
A popover is an overlay element positioned relative to a trigger.
install | yarn add react-aria-components |
---|---|
version | 3.17.0 |
usage | import {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 {
border: 1px solid var(--spectrum-global-color-gray-400);
box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
border-radius: 6px;
background: var(--page-background);
outline: none;
padding: 30px;
max-width: 250px;
& .react-aria-OverlayArrow svg {
display: block;
fill: var(--page-background);
stroke: var(--spectrum-global-color-gray-400);
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;
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;
}
}
.react-aria-Popover {
border: 1px solid var(--spectrum-global-color-gray-400);
box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
border-radius: 6px;
background: var(--page-background);
outline: none;
padding: 30px;
max-width: 250px;
& .react-aria-OverlayArrow svg {
display: block;
fill: var(--page-background);
stroke: var(--spectrum-global-color-gray-400);
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;
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;
}
}
.react-aria-Popover {
border: 1px solid var(--spectrum-global-color-gray-400);
box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
border-radius: 6px;
background: var(--page-background);
outline: none;
padding: 30px;
max-width: 250px;
& .react-aria-OverlayArrow svg {
display: block;
fill: var(--page-background);
stroke: var(--spectrum-global-color-gray-400);
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;
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;
}
}
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#
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#
Name | Type | Default | Description |
triggerRef | RefObject<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. |
placement | Placement | 'bottom' | The placement of the element with respect to its anchor element. |
containerPadding | number | 12 | The placement padding that should be applied between the element and its surrounding container. |
offset | number | 0 | The additional offset applied along the main axis between the element and its anchor element. |
crossOffset | number | 0 | The additional offset applied along the cross axis between the element and its anchor element. |
shouldFlip | boolean | true | 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. |
isNonModal | boolean | — | 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. |
children | ReactNode | (
(values: PopoverRenderProps
)) => ReactNode | — | |
className | string | (
(values: PopoverRenderProps
)) => string | — | |
style | CSSProperties | (
(values: PopoverRenderProps
)) => CSSProperties | — |
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:
Name | CSS Selector | Description |
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:
Name | CSS Selector | Description |
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.
function MyPopover({children, ...props}) {
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>
function MyPopover({ children, ...props }) {
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>
function MyPopover(
{ children, ...props }
) {
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.
<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.