Popover
Popover displays additional information without interrupting user flow.
Basic Usage
To implement the Popover component, you need to import it first:
import { Popover, PopoverWrapper } from '@react-ui-org/react-ui';
And use it:
React.createElement(() => {
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
// All inline styles in this example are for demonstration purposes only.
return (
<div
style={{
display: 'grid',
placeContent: 'center',
minWidth: '20rem',
minHeight: '10rem',
}}
>
<PopoverWrapper>
<Button
aria-describedby={isPopoverOpen ? 'my-popover' : undefined}
label="Want to see a popover? Click me!"
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
/>
{isPopoverOpen && (
<Popover id="my-popover">
Hello there!
</Popover>
)}
</PopoverWrapper>
</div>
);
});
See API for all available options.
Placement
Available placements are: top, right, bottom, and left. Additionally, all basic
placements can be aligned to the center (default, no suffix), start (e.g.
top-start), or end (e.g. bottom-end). Check Popover API for the
complete list of accepted values.
React.createElement(() => {
const [align, setAlign] = React.useState('');
// All inline styles in this example are for demonstration purposes only.
return (
<>
<Toolbar align="baseline">
<ToolbarItem>
<span id="alignment-options-label">Alignment:</span>
</ToolbarItem>
<ToolbarItem>
<ButtonGroup aria-labelledby="alignment-options-label">
<Button
aria-pressed={align === '-start'}
color={align === '-start' ? 'selected' : 'secondary'}
label="start"
onClick={() => setAlign('-start')}
/>
<Button
aria-pressed={align === ''}
color={align === '' ? 'selected' : 'secondary'}
label="center"
onClick={() => setAlign('')}
/>
<Button
aria-pressed={align === '-end'}
color={align === '-end' ? 'selected' : 'secondary'}
label="end"
onClick={() => setAlign('-end')}
/>
</ButtonGroup>
</ToolbarItem>
</Toolbar>
<div
style={{
display: 'grid',
placeContent: 'center',
minWidth: '20rem',
minHeight: '15rem',
}}
>
<PopoverWrapper>
<docoff-placeholder bordered aria-describedby="my-popover-top">
Popovers
<br />
all day longβ¦
</docoff-placeholder>
<Popover id="my-popover-top" placement={`top${align}`}>
Top side
</Popover>
<Popover id="my-popover-right" placement={`right${align}`}>
Right side
</Popover>
<Popover id="my-popover-bottom" placement={`bottom${align}`}>
Bottom side
</Popover>
<Popover id="my-popover-left" placement={`left${align}`}>
Left side
</Popover>
</PopoverWrapper>
</div>
</>
);
});
Smart Positioning
Popover needs a positioning anchor β a parent element with position: relative
or position: absolute. PopoverWrapper provides one, and it does something
more: when your trigger and Popover are wrapped in PopoverWrapper, Floating UI
runs automatically with flip() and shift() middleware and autoUpdate. No
extra imports or setup are required.
<PopoverWrapper>
<Button
aria-describedby={isPopoverOpen ? 'my-popover' : undefined}
label="Want to see a popover? Click me!"
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
/>
{isPopoverOpen && <Popover id="my-popover">Hello there!</Popover>}
</PopoverWrapper>
If your CSS already has a positioned ancestor wrapping your trigger, PopoverWrapper is not strictly required β but you lose automatic flip and shift.
π± Try scrolling the example to see how Popover flips to stay in view. The suggested placement is what you set; Floating UI may use a different final placement after applying flip and shift.
React.createElement(() => {
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const [placement, setPlacement] = React.useState('top');
const placementOptions = [
'top',
'top-start',
'top-end',
'right',
'right-start',
'right-end',
'bottom',
'bottom-start',
'bottom-end',
'left',
'left-start',
'left-end',
];
// All inline styles in this example are for demonstration purposes only.
return (
<>
<Toolbar>
<ToolbarItem>
<SelectField
label="Suggested placement:"
onChange={e => setPlacement(e.target.value)}
options={placementOptions.map((el) => ({
label: el,
value: el,
}))}
value={placement}
/>
</ToolbarItem>
</Toolbar>
<div
style={{
width: '40rem',
maxWidth: '100%',
height: '12rem',
overflow: 'auto',
}}
>
<div
style={{
alignItems: 'center',
display: 'flex',
height: '20rem',
justifyContent: 'center',
width: '60rem',
}}
>
<PopoverWrapper>
<Button
aria-describedby={isPopoverOpen ? 'my-smart-popover' : undefined}
label="Trigger Popover"
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
/>
{isPopoverOpen && (
<Popover
id="my-smart-popover"
placement={placement}
>
Auto-repositioning Popover
</Popover>
)}
</PopoverWrapper>
</div>
</div>
</>
);
});
When Popover is rendered outside a PopoverWrapper β for example inside a portal
or a custom layout β pass the anchor DOM element directly via the
referenceElement prop.
Head to PopoverWrapper API for all available options.
Rendering in a React Portal
When a Popover is placed inside a CSS stacking context β for example, inside
a layout element that uses transform, opacity, or an explicit z-index
β it may be covered by other page elements regardless of its own z-index.
This is a fundamental CSS limitation: z-index only works within the same
stacking context.
The solution is to render the Popover in a React portal attached to a container
at the root of the document, where it is free of any stacking context
constraints. Use the portalId prop to specify the ID of the portal container.
π PopoverWrapper is still needed when using a portal β it provides the reference element for Floating UI. The portal only determines where the Popover is rendered in the DOM, not how it is positioned.
In the example below, the trigger is inside a container with transform
(a stacking context). The sibling element below it has z-index: 2 and covers
the Popover without a portal.
π± Enable Use portal to see the Popover break free of the stacking context. Then try scrolling the whole page to see how Popover flips to stay in view.
React.createElement(() => {
const PORTAL_ID = 'my-popover-portal';
const [isOpen, setIsOpen] = React.useState(false);
const [usePortal, setUsePortal] = React.useState(false);
// All inline styles in this example are for demonstration purposes only.
return (
<>
<Toolbar>
<ToolbarItem>
<Toggle
checked={usePortal}
label="Use portal"
onChange={() => { setIsOpen(false); setUsePortal(v => !v); }}
/>
</ToolbarItem>
</Toolbar>
<div
style={{
display: 'flex',
justifyContent: 'center',
paddingTop: '1.5rem',
transform: 'translateZ(0)',
}}
>
<PopoverWrapper>
<Button
aria-describedby={isOpen ? 'my-portal-popover' : undefined}
label="Open Popover"
onClick={() => setIsOpen(v => !v)}
/>
{isOpen && (
<Popover
id="my-portal-popover"
placement="bottom"
portalId={usePortal ? PORTAL_ID : undefined}
>
Hello there!
</Popover>
)}
</PopoverWrapper>
</div>
<div
style={{
position: 'relative',
display: 'grid',
placeContent: 'center',
minHeight: '5rem',
marginTop: '2.25rem',
borderRadius: 'var(--rui-dimension-radius-2, 0.2rem)',
background: 'var(--rui-color-background-danger, #ffe3e3)',
}}
>
Sibling element (z-index: 2)
</div>
<div id={PORTAL_ID} />
</>
);
});
For apps that use many Popovers, configure this globally via GlobalPropsProvider so every Popover uses the portal automatically:
{/* Once in the app root, outside any stacking contexts: */}
<div id="popover-portal" />
<GlobalPropsProvider globalProps={{ Popover: { portalId: 'popover-portal' } }}>
{/* All Popovers in the subtree use the portal */}
</GlobalPropsProvider>
Forwarding HTML Attributes
In addition to the options below in the component's API section, you
can specify React synthetic events or any HTML attribute you like. All
attributes that don't interfere with the API are forwarded to the root <div>
HTML element. This enables making the component interactive and helps to improve
its accessibility.
π Refer to the MDN reference for the full list of supported attributes of the div element.
Forwarding ref
If you provide ref, it is forwarded to the root native HTML <div> element.
API
PopoverWrapper API
Theming
| Custom Property | Description |
|---|---|
--rui-Popover__width |
Popover width |
--rui-Popover__padding |
Popover padding |
--rui-Popover__border-width |
Border width |
--rui-Popover__border-color |
Border color |
--rui-Popover__border-radius |
Corner radius |
--rui-Popover__color |
Text color |
--rui-Popover__background-color |
Background color |
--rui-Popover__box-shadow |
Popover box shadow |
--rui-Popover__z-index |
Popover z-index (RADES default: 150) |