Popover
A click-triggered, non-modal floating panel for small chunks of contextual content and quick edits.
Uses role="dialog"; the trigger exposes aria-expanded; focus moves into the panel on open; Esc closes and restores focus to the trigger.
import { Button, Popover, PopoverContent, PopoverTrigger } from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<Popover>
<PopoverTrigger asChild>
<Button>Open popover</Button>
</PopoverTrigger>
<PopoverContent>
<div style={{ display: 'grid', gap: '0.25rem', maxWidth: '16rem' }}>
<strong>Quick note</strong>
<span>Popovers are great for small bits of contextual content close to the trigger.</span>
</div>
</PopoverContent>
</Popover>
);
}
Variants
Section titled “Variants”Sides and alignment
Section titled “Sides and alignment”PopoverContent accepts side="top" | "right" | "bottom" (default) | "left" and align="start" | "center" (default) | "end". The default sideOffset is 8 and collisionPadding is 8 — the panel flips automatically near viewport edges.
import { Button, Popover, PopoverContent, PopoverTrigger } from '@arshad-shah/cynosure-react';
const sides = ['top', 'right', 'bottom', 'left'] as const;
export default function Example() {
return (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, auto)',
gap: '0.75rem',
justifyContent: 'start',
}}
>
{sides.map((side) => (
<Popover key={side}>
<PopoverTrigger asChild>
<Button variant="outline">{side}</Button>
</PopoverTrigger>
<PopoverContent side={side}>
<div style={{ padding: '0.25rem 0.5rem', fontSize: '0.875rem' }}>side="{side}"</div>
</PopoverContent>
</Popover>
))}
</div>
);
}
With arrow
Section titled “With arrow”Add PopoverArrow inside PopoverContent to render a pointer toward the trigger.
import {
Button,
Popover,
PopoverArrow,
PopoverContent,
PopoverTrigger,
} from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<Popover>
<PopoverTrigger asChild>
<Button>Show details</Button>
</PopoverTrigger>
<PopoverContent>
<div style={{ display: 'grid', gap: '0.25rem', maxWidth: '16rem' }}>
<strong>Pointed at the trigger</strong>
<span style={{ fontSize: '0.875rem' }}>
PopoverArrow renders a directional pointer toward the trigger.
</span>
</div>
<PopoverArrow />
</PopoverContent>
</Popover>
);
}
Controlled
Section titled “Controlled”Drive the open state externally with open / onOpenChange. Useful when the popover hosts a form whose submit closes the panel.
import {
Button,
Input,
Label,
Popover,
PopoverClose,
PopoverContent,
PopoverTrigger,
} from '@arshad-shah/cynosure-react';
import { useState } from 'react';
export default function Example() {
const [open, setOpen] = useState(false);
const [name, setName] = useState('Untitled document');
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
setOpen(false);
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button>Rename</Button>
</PopoverTrigger>
<PopoverContent>
<form
onSubmit={handleSubmit}
style={{ display: 'grid', gap: '0.75rem', minWidth: '16rem' }}
>
<div style={{ display: 'grid', gap: '0.375rem' }}>
<Label htmlFor="popover-name">Document name</Label>
<Input id="popover-name" value={name} onChange={setName} autoFocus />
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.5rem' }}>
<PopoverClose asChild>
<Button type="button" variant="ghost" size="sm">
Cancel
</Button>
</PopoverClose>
<Button type="submit" size="sm">
Save
</Button>
</div>
</form>
</PopoverContent>
</Popover>
);
}
Anchored to a different element
Section titled “Anchored to a different element”Use PopoverAnchor to position the panel relative to an arbitrary element while keeping a separate PopoverTrigger button.
import {
Button,
Popover,
PopoverAnchor,
PopoverContent,
PopoverTrigger,
} from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<Popover>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '1.5rem',
flexWrap: 'wrap',
}}
>
<PopoverAnchor asChild>
<div
style={{
padding: '0.75rem 1rem',
border: '1px dashed var(--cynosure-color-border-default, #d4d4d4)',
borderRadius: '0.5rem',
fontSize: '0.875rem',
}}
>
Visual anchor (the panel points here)
</div>
</PopoverAnchor>
<PopoverTrigger asChild>
<Button variant="outline">Toggle from far away</Button>
</PopoverTrigger>
</div>
<PopoverContent side="top">
<div style={{ padding: '0.25rem 0.5rem', fontSize: '0.875rem', maxWidth: '16rem' }}>
The trigger and the anchor are different elements — the panel positions itself next to the
anchor.
</div>
</PopoverContent>
</Popover>
);
}
Accessibility
Section titled “Accessibility”- The trigger toggles
aria-expandedand references the panel viaaria-controls. PopoverContentcarriesrole="dialog"; focus moves into the panel on open and returns to the trigger on close.Esccloses the popover; clicking outside also closes it.- The popover is non-modal — the rest of the page remains interactive, but a focus scope keeps Tab cycling within the panel until it is dismissed.
- Use
PopoverClosefor an explicit close button when the popover hosts a long form.
Recipes
Section titled “Recipes”- Use for color pickers, quick filter clusters, inline edit forms, and “more info” panels.
- Prefer
Tooltipfor short text-only hover hints andDialogwhen the task warrants a full modal interrupt. - Position with
sideandalign; let the built-in collision detection auto-flip the surface around the viewport edge instead of hard-coding placement. - Pair with
PopoverAnchorwhen the trigger and the visual anchor must be different (e.g., a global ”?” button anchoring help next to whichever field has focus).