Popover
A Popover is a non-modal overlay that anchors itself to a trigger element. Unlike a standard tooltip, it is designed to hold interactive rich content such as forms, lists, or navigation, without breaking the user's current context.
Popover is used as an underlying component for other components like Dropdown, Select and Tooltip.
Key Characteristics
- Non-Modal: The user can still interact with the rest of the page while the popover is open.
- Floating Context: It uses position: fixed in combination with react-portals to ensure it remains visible and correctly placed relative to its disclosure, even within scrolling containers, or in any other context. It just works.
- Rich Interaction: Specifically built to handle interactive elements (buttons, inputs) that require focus management and keyboard navigation.
Import
There are 3 popover related components:
Popover: The main popover component.PopoverTrigger: The component that triggers the popover to open or close.PopoverContent: The content that is displayed when the popover is open.
import { Popover, PopoverTrigger, PopoverContent } from '@andrejground/lab';
Compound pattern is also supported:
Popover.TriggerPopover.Content
Customization
Add your styles and your component is ready to be used.
- MyPopover.tsx
- MyPopover.module.scss
import { Popover, PopoverProps } from '@andrejground/lab';
import styles from './MyPopover.module.scss';
type Props = PopoverProps;
function MyPopover(props: Props) {
return (
<Popover
{...props}
classNames={{
content: styles.popoverContent,
}}
/>
);
}
MyPopover.Content = Popover.Content;
MyPopover.Trigger = Popover.Trigger;
export default MyPopover;
.popoverContent {
background-color: var(--background-color);
color: var(--text-color);
border: 1px solid var(--border-color);
}
Usage
- Preview
- Code
import { Popover } from '@andrejground/lab';
export default function App() {
return (
<Popover>
<Popover.Trigger>
<button>Open Popover</button>
</Popover.Trigger>
<Popover.Content>
<div>Popover Content</div>
<div>This is the popover content</div>
</Popover.Content>
</Popover>
);
}
Placement
- Preview
- Code
import { Popover, type PopoverPlacement } from '@andrejground/lab';
const PLACEMENTS: PopoverPlacement[] = [
'top-start',
'top-center',
'top-end',
'bottom-start',
'bottom-center',
'bottom-end',
'right-start',
'right-center',
'right-end',
'left-start',
'left-center',
'left-end',
];
export default function App() {
return (
<>
{PLACEMENTS.map((placement) => (
<Popover key={placement} placement={placement} fullWidth>
<Popover.Trigger>
<button>{placement}</button>
</Popover.Trigger>
<Popover.Content>
<div>
<code>{placement}</code>
</div>
<small>This is the popover content</small>
</Popover.Content>
</Popover>
))}
</>
);
}
Controlled
- Preview
- Code
Open:
falseimport React from 'react';
import { Popover } from '@andrejground/lab';
export default function App() {
const [isOpen, setIsOpen] = React.useState(false);
return (
<>
<Popover isOpen={isOpen} onOpenChange={setIsOpen} placement="top-center">
<Popover.Trigger>
<button>Open Popover</button>
</Popover.Trigger>
<Popover.Content>
<div>Popover Content</div>
<small>This is the popover content</small>
</Popover.Content>
</Popover>
<br />
<div>
Open: <code>{`${isOpen}`}</code>
</div>
</>
);
}
Offset
- Preview
- Code
import { Popover } from '@andrejground/lab';
const OFFSETS = [0, 8, 16, 24];
export default function App() {
return (
<>
{OFFSETS.map((offset) => (
<Popover key={offset} offset={offset} placement="bottom-center">
<Popover.Trigger>
<button>offset: {offset}</button>
</Popover.Trigger>
<Popover.Content>
<div>
<code>offset: {offset}</code>
</div>
<small>This is the popover content</small>
</Popover.Content>
</Popover>
))}
</>
);
}
With Form
- Preview
- Code
import React from 'react';
import { Popover } from '@andrejground/lab';
const DEFAULT_VALUES = {
firstName: '',
lastName: '',
email: '',
};
export default function App() {
const [isOpen, setIsOpen] = React.useState(false);
const [values, setValues] = React.useState(DEFAULT_VALUES);
const handleCancel = () => {
setValues(DEFAULT_VALUES);
setIsOpen(false);
};
const handleSave = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsOpen(false);
alert(JSON.stringify(values, null, 2));
};
const handleChange =
(field: keyof typeof values) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
setValues((prev) => ({ ...prev, [field]: e.target.value }));
};
return (
<Popover
isOpen={isOpen}
onOpenChange={setIsOpen}
onClose={handleCancel}
onOpen={() => setValues(DEFAULT_VALUES)}
>
<Popover.Trigger>
<button>Open Form</button>
</Popover.Trigger>
<Popover.Content>
<form
style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}
onSubmit={handleSave}
>
<input
type="text"
className="form-control"
placeholder="First Name"
value={values.firstName}
onChange={handleChange('firstName')}
/>
<input
type="text"
className="form-control"
placeholder="Last Name"
value={values.lastName}
onChange={handleChange('lastName')}
/>
<input
type="text"
className="form-control"
placeholder="Email"
value={values.email}
onChange={handleChange('email')}
/>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
}}
>
<button
onClick={handleCancel}
type="button"
>
Cancel
</button>
<button
onClick={handleSave}
type="submit"
>
Save
</button>
</div>
</form>
</Popover.Content>
</Popover>
);
}
Backdrop
- Preview
- Code
import { Popover, type Backdrop } from '@andrejground/lab';
const BACKDROPS: Backdrop[] = ['none', 'transparent', 'opaque', 'blur'];
export default function App() {
return (
<>
{BACKDROPS.map((backdrop) => (
<Popover key={backdrop} backdrop={backdrop} placement="bottom-center">
<Popover.Trigger>
<button>{backdrop}</button>
</Popover.Trigger>
<Popover.Content>
<div>
<code>backdrop: {backdrop}</code>
</div>
<small>This is the popover content</small>
</Popover.Content>
</Popover>
))}
</>
);
}
API
Popover Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Popover content (expects Popover.Trigger and Popover.Content) |
trigger | ReactNode | - | Element that triggers the popover |
content | ReactNode | - | Content displayed inside the popover |
size | PopoverSize | - | Controls the width of the popover content |
isOpen | boolean | - | Controls the popover open state (controlled mode) |
isDisabled | boolean | - | Disables the popover trigger |
isNested | boolean | false | Indicates the popover is nested inside another popover |
placement | PopoverPlacement | "bottom-center" | Preferred placement of the popover relative to the trigger |
offset | number | 8 | Distance in pixels between the popover and the trigger |
backdrop | Backdrop | "none" | The backdrop style displayed behind the popover |
shouldFlip | boolean | true | Flips placement when there is not enough space |
shouldBlockScroll | boolean | true | Blocks page scroll when the popover is open |
shouldCloseOnScroll | boolean | !shouldBlockScroll | Closes the popover when the page is scrolled |
shouldCloseOnClickOutside | boolean | true | Closes the popover when clicking outside of it |
shouldCloseOnEsc | boolean | true | Closes the popover when the Escape key is pressed |
shouldCloseOnTriggerBlur | boolean | false | Closes the popover when the trigger loses focus |
showArrow | boolean | false | Shows an arrow pointing toward the trigger |
openOnHover | boolean | - | Opens the popover on hover instead of click |
openOnFocus | boolean | false | Opens the popover when the trigger receives focus |
focusTriggerOnClose | boolean | true | Returns focus to the trigger when the popover closes |
delayShow | number | 0 | Delay in milliseconds before showing the popover |
delayHide | number | 0 | Delay in milliseconds before hiding the popover |
hoverableContent | boolean | true | Keeps the popover open while hovering over its content |
growContent | boolean | false | Allows the popover content to grow beyond the trigger width |
onOpen | () => void | - | Callback fired when the popover opens |
onClose | () => void | - | Callback fired when the popover closes |
onClickOutside | () => void | - | Callback fired when clicking outside the popover |
onOpenChange | (isOpen: boolean) => void | - | Callback fired when the open state changes |
onTriggerFocus | () => void | - | Callback fired when the trigger receives focus |
onTriggerBlur | () => void | - | Callback fired when the trigger loses focus |
triggerWrapper | boolean | false | Wraps the trigger in a span instead of using Slot |
fullWidthTriggerWrapper | boolean | false | Makes the trigger wrapper take full width |
focusTrapProps | { trapFocus?: boolean; autoFocus?: boolean } | { trapFocus: true, autoFocus: true } | Configuration for focus trap behavior |
classNames | PopoverClassNames | - | Custom class names for the popover slots |
Popover.Trigger Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | A single element that acts as the popover trigger |
Popover.Content Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Content displayed inside the popover |