usePopover
Provides the behavior and accessibility implementation for a popover component. A popover is an overlay element positioned relative to a trigger.
| install | yarn add react-aria |
|---|---|
| version | 3.18.0 |
| usage | import {usePopover} from 'react-aria' |
API#
usePopover(
(props: PopoverProps,
, state: OverlayTriggerState
)): PopoverAria
Features#
There is no built in way to create popovers in
HTML. usePopover,
helps achieve accessible popovers that can be styled as needed.
- Exposes overlay trigger and connects trigger to overlay with ARIA
- Positions the overlay relative to the trigger
- Hides content behind the overlay from assistive technology while open
- Handles closing the overlay when interacting outside and pressing the Escape key
- Restores focus to the triggering element on unmount
Note: usePopover only handles the overlay itself. It should be combined
with useDialog 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.
usePopover returns props that you should spread onto the trigger element and
the appropriate element:
| Name | Type | Description |
popoverProps | DOMAttributes | Props for the popover element. |
arrowProps | DOMAttributes | Props for the popover tip arrow if any. |
State is managed by the useOverlayTriggerState
hook in @react-stately/overlays. The state object should be passed as an argument to usePopover.
Example#
This example shows how to build a typical popover overlay that is positioned relative to
a trigger button. The content of the popover is a dialog, built
with useDialog. The Dialog component used in this example can also be reused within a modal or other types of overlays. The implementation is available below.
The Popover component uses an <Overlay> to render its contents in a React Portal at the end of the document body, which ensures it is not clipped by other elements. It also acts as a focus scope, containing focus within the popover and restoring it to the trigger when it unmounts. usePopover handles positioning the popover relative to the trigger element, and closing it when the user interacts outside or presses the Escape key.
usePopover also hides content outside the popover from screen readers, which is important since the surrounding content won't be in context of the original trigger due to the portal. To allow screen reader users to dismiss the popover without a keyboard (e.g. on mobile), visually hidden <DismissButton> elements are added at the start and end of the popover.
import {DismissButton, Overlay, usePopover} from 'react-aria';
function Popover({ children, state, ...props }) {
let ref = React.useRef();
let { popoverRef = ref } = props;
let { popoverProps } = usePopover({
...props,
popoverRef
}, state);
return (
<Overlay>
<div
{...popoverProps}
ref={popoverRef}
style={{
...popoverProps.style,
background: 'var(--page-background)',
border: '1px solid gray'
}}
>
<DismissButton onDismiss={state.close} />
{children}
<DismissButton onDismiss={state.close} />
</div>
</Overlay>
);
}
import {
DismissButton,
Overlay,
usePopover
} from 'react-aria';
function Popover({ children, state, ...props }) {
let ref = React.useRef();
let { popoverRef = ref } = props;
let { popoverProps } = usePopover({
...props,
popoverRef
}, state);
return (
<Overlay>
<div
{...popoverProps}
ref={popoverRef}
style={{
...popoverProps.style,
background: 'var(--page-background)',
border: '1px solid gray'
}}
>
<DismissButton onDismiss={state.close} />
{children}
<DismissButton onDismiss={state.close} />
</div>
</Overlay>
);
}
import {
DismissButton,
Overlay,
usePopover
} from 'react-aria';
function Popover(
{
children,
state,
...props
}
) {
let ref = React
.useRef();
let {
popoverRef = ref
} = props;
let { popoverProps } =
usePopover({
...props,
popoverRef
}, state);
return (
<Overlay>
<div
{...popoverProps}
ref={popoverRef}
style={{
...popoverProps
.style,
background:
'var(--page-background)',
border:
'1px solid gray'
}}
>
<DismissButton
onDismiss={state
.close}
/>
{children}
<DismissButton
onDismiss={state
.close}
/>
</div>
</Overlay>
);
}
The above Popover component can be used as part of many different patterns, such as ComboBox, Select, and DatePicker. To use it standalone, we need a trigger element. The below PopoverTrigger component uses the useOverlayTrigger hook to trigger the popover when a button is pressed. This hook also ensures that the button and popover are semantically connected via ARIA.
import {useOverlayTrigger} from 'react-aria';
import {useOverlayTriggerState} from 'react-stately';
// Reuse the Button from your component library. See below for details.
import {Button} from 'your-component-library';
function PopoverTrigger({ label, children, ...props }) {
let ref = React.useRef();
let state = useOverlayTriggerState(props);
let { triggerProps, overlayProps } = useOverlayTrigger(
{ type: 'dialog' },
state,
ref
);
return (
<>
<Button {...triggerProps} buttonRef={ref}>{label}</Button>
{state.isOpen &&
(
<Popover triggerRef={ref} state={state}>
{React.cloneElement(children, overlayProps)}
</Popover>
)}
</>
);
}
import {useOverlayTrigger} from 'react-aria';
import {useOverlayTriggerState} from 'react-stately';
// Reuse the Button from your component library. See below for details.
import {Button} from 'your-component-library';
function PopoverTrigger({ label, children, ...props }) {
let ref = React.useRef();
let state = useOverlayTriggerState(props);
let { triggerProps, overlayProps } = useOverlayTrigger(
{ type: 'dialog' },
state,
ref
);
return (
<>
<Button {...triggerProps} buttonRef={ref}>
{label}
</Button>
{state.isOpen &&
(
<Popover triggerRef={ref} state={state}>
{React.cloneElement(children, overlayProps)}
</Popover>
)}
</>
);
}
import {useOverlayTrigger} from 'react-aria';
import {useOverlayTriggerState} from 'react-stately';
// Reuse the Button from your component library. See below for details.
import {Button} from 'your-component-library';
function PopoverTrigger(
{
label,
children,
...props
}
) {
let ref = React
.useRef();
let state =
useOverlayTriggerState(
props
);
let {
triggerProps,
overlayProps
} = useOverlayTrigger(
{ type: 'dialog' },
state,
ref
);
return (
<>
<Button
{...triggerProps}
buttonRef={ref}
>
{label}
</Button>
{state.isOpen &&
(
<Popover
triggerRef={ref}
state={state}
>
{React
.cloneElement(
children,
overlayProps
)}
</Popover>
)}
</>
);
}
Now, we can render an example popover containing a dialog.
// Reuse the Dialog from your component library. See below for details.
import {Dialog} from 'your-component-library';
<PopoverTrigger label="Open Popover">
<Dialog title="Popover title">
This is the content of the popover.
</Dialog>
</PopoverTrigger>
// Reuse the Dialog from your component library. See below for details.
import {Dialog} from 'your-component-library';
<PopoverTrigger label="Open Popover">
<Dialog title="Popover title">
This is the content of the popover.
</Dialog>
</PopoverTrigger>
// Reuse the Dialog from your component library. See below for details.
import {Dialog} from 'your-component-library';
<PopoverTrigger label="Open Popover">
<Dialog title="Popover title">
This is the content
of the popover.
</Dialog>
</PopoverTrigger>
Dialog#
The Dialog component is rendered within the Popover component. It is built using the useDialog hook, and can also be used in other overlay containers such as modals.
Show code
import {useDialog} from 'react-aria';
function Dialog({ title, children, ...props }) {
let ref = React.useRef();
let { dialogProps, titleProps } = useDialog(props, ref);
return (
<div {...dialogProps} ref={ref} style={{ padding: 30 }}>
{title &&
(
<h3 {...titleProps} style={{ marginTop: 0 }}>
{title}
</h3>
)}
{children}
</div>
);
}
import {useDialog} from 'react-aria';
function Dialog({ title, children, ...props }) {
let ref = React.useRef();
let { dialogProps, titleProps } = useDialog(props, ref);
return (
<div {...dialogProps} ref={ref} style={{ padding: 30 }}>
{title &&
(
<h3 {...titleProps} style={{ marginTop: 0 }}>
{title}
</h3>
)}
{children}
</div>
);
}
import {useDialog} from 'react-aria';
function Dialog(
{
title,
children,
...props
}
) {
let ref = React
.useRef();
let {
dialogProps,
titleProps
} = useDialog(
props,
ref
);
return (
<div
{...dialogProps}
ref={ref}
style={{
padding: 30
}}
>
{title &&
(
<h3
{...titleProps}
style={{
marginTop:
0
}}
>
{title}
</h3>
)}
{children}
</div>
);
}
Button#
The Button component is used in the above example to toggle the popover. It is built using the useButton hook, and can be shared with many other components.
Show code
import {useButton} from 'react-aria';
function Button(props) {
let ref = props.buttonRef;
let { buttonProps } = useButton(props, ref);
return (
<button {...buttonProps} ref={ref} style={props.style}>
{props.children}
</button>
);
}
import {useButton} from 'react-aria';
function Button(props) {
let ref = props.buttonRef;
let { buttonProps } = useButton(props, ref);
return (
<button {...buttonProps} ref={ref} style={props.style}>
{props.children}
</button>
);
}
import {useButton} from 'react-aria';
function Button(props) {
let ref =
props.buttonRef;
let { buttonProps } =
useButton(
props,
ref
);
return (
<button
{...buttonProps}
ref={ref}
style={props.style}
>
{props.children}
</button>
);
}