Popover
A popover is an overlay element positioned relative to a trigger.
install | yarn add react-aria-components |
---|---|
version | 1.0.0-beta.2 |
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} 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#
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>
Examples#
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#
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. |
isKeyboardDismissDisabled | boolean | false | 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. |
arrowSize | number | 0 | Cross size of the overlay arrow in pixels. |
boundaryElement | Element | document.body | Element that that serves as the positioning boundary. |
scrollRef | RefObject<Element> | overlayRef | A ref for the scrollable region within the overlay. |
shouldUpdatePosition | boolean | true | Whether the overlay should update its position automatically. |
arrowBoundaryOffset | number | 0 | The minimum distance the arrow's edge should be from the edge of the overlay element. |
isOpen | boolean | — | Whether the overlay is open by default (controlled). |
defaultOpen | boolean | — | Whether the overlay is open by default (uncontrolled). |
children | ReactNode | (
(values: PopoverRenderProps
)) => ReactNode | — | The children of the component. A function may be provided to alter the children based on component state. |
className | string | (
(values: PopoverRenderProps
)) => string | — | The CSS className for the element. A function may be provided to compute the class based on component state. |
style | CSSProperties | (
(values: PopoverRenderProps
)) => CSSProperties | — | The inline style for the element. A function may be provided to compute the style based on component state. |
Events
Name | Type | Description |
onOpenChange | (
(isOpen: boolean
)) => void | Handler that is called when the overlay's open state changes. |
Layout
Name | Type | Description |
slot | string | null | A slot name for the component. Slots allow the component to receive props from a parent component.
An explicit |
Sizing
Name | Type | Description |
maxHeight | number | 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:
Name | CSS Selector | Description |
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:
Name | CSS Selector | Description |
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).
Component | Context | Props | Ref |
Popover | PopoverContext | PopoverProps | HTMLElement |
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.