Popover
A panel hung from what opened it, on a leader line. Escape or a click elsewhere puts it away.
Report an issue with PopoverExample
leader
brace
cut
leader, open
Anyone with the link can view
studio.com/halden
brace, open
Set in two faces
Archivo for what the page says, Bodoni Moda for what you say back.
cut, open
Last renamed 12 September
Spring notes, second draft
Install
Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base along.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/popover.jsonUsage
This example also uses Button.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/ button.json"use client"import * as React from "react"import { Button } from "@/components/ui/button"import { Popover, PopoverClose, PopoverContent, PopoverTrigger } from "@/components/ui/popover"function Share() {return (<><p className="ot-label">Anyone with the link can view</p><p className="ot-yours">studio.com/halden</p><PopoverClose asChild><Button variant="quiet" className="justify-self-start" onClick={() => navigator.clipboard?.writeText("studio.com/halden")}>Copy the link</Button></PopoverClose></>)}function Faces() {return (<><p className="ot-label">Set in two faces</p><p>Archivo for what the page says, <span className="ot-yours">Bodoni Moda</span> for what you say back.</p></>)}function Rename() {return (<><p className="ot-label">Last renamed 12 September</p><p className="ot-yours">Spring notes, second draft</p><PopoverClose asChild><Button variant="quiet" className="justify-self-start">Rename it</Button></PopoverClose></>)}const variants = [{ variant: "leader", open: "Share Halden", Body: Share },{ variant: "brace", open: "Two faces", Body: Faces },{ variant: "cut", open: "Spring notes", Body: Rename },] as constexport default function Example() {return (<div className="grid justify-items-start gap-x-10 gap-y-12 sm:grid-cols-3">{variants.map(({ variant, open, Body }) => (<div key={variant} className="grid justify-items-start gap-4"><span className="ot-label">{variant}</span><Popover><PopoverTrigger asChild><Button variant="bracket">{open}</Button></PopoverTrigger><PopoverContent variant={variant} aria-label={open} className="grid gap-3"><Body /></PopoverContent></Popover></div>))}</div>)}// Pinned open in place, without Radix placing them: each panel sits in the flow under a copy of its// trigger, with the attributes Radix would give it. The brace's point stands where Radix's arrow would.function Pinned({ variant, open, Body }: (typeof variants)[number]) {const brace = variant === "brace"return (<div className="grid justify-items-start" style={{ rowGap: variant === "cut" ? 0 : brace ? 26 : 27 }}><Button variant="bracket" tabIndex={-1} aria-hidden="true" className={brace ? "justify-self-center" : undefined}>{open}</Button><divclassName="ot-pop grid gap-3"data-state="open"data-side="bottom"data-align={brace ? "center" : "start"}data-variant={variant === "leader" ? undefined : variant}style={{ "--ot-pop-gap": "27px", "--radix-popover-trigger-height": "44px", width: "18rem", marginTop: variant === "cut" ? 0 : undefined } as React.CSSProperties}><Body />{brace && (<span className="ot-pop-brace" aria-hidden="true"><span className="ot-pop-brace-arms"><span style={{ position: "absolute", left: "calc(9rem - var(--ot-pop-brace-in) - var(--ot-pop-brace-r))", top: 0, transformOrigin: "center 0", transform: "rotate(180deg)" }}><span className="ot-pop-brace-point" style={{ display: "block" }} /></span></span></span>)}</div></div>)}Contract
- Underneath: Radix Popover.
- Anatomy:
<div class="ot-pop" popover>opened by a button withpopovertarget. Anchor positioning places it under the opener (position-area), using the implicit anchor. - Hung from what opened it on a leader line, after Weingart: the dot lands on the opener's edge, the line drops, and the panel settles below.
- Keyboard: Escape or a click elsewhere puts it away (native popover light dismiss).
- Variants go on
data-variantof the panel (PopoverContent variant): leader (the default), brace, cut. Both new ones hang above or below the trigger and drop the leader. - Brace, after the brace that joins a score's staves and the underbrace that sums a span: the panel loses its box and a hairline brace over its words gathers their whole width to one point at the trigger's middle. The ends curl at the words' edges; the arms and the point are Radix's arrow (
.ot-pop-brace-pointinside.ot-pop-brace-arms), so the point stays over the trigger when the panel shifts to stay on screen and the brace goes lopsided rather than point at nothing. Centred by default. Beside the trigger there's no width to gather, so the brace isn't drawn. - Cut, after SPECTRA's title cut by a line: the panel's top edge, in ink, falls through the trigger's words at half their height (
--radix-popover-trigger-height), and the panel rides over their lower half. There is no leader: the join is the cut. Flipped above, it cuts from the top and leaves the lower half. - Keyboard and screen readers: Radix's popover in all three; the brace is
aria-hidden. Reduced motion: the panel appears and the brace is whole. Right to left: the leader measures from the start edge, the brace is symmetric, the cut has no direction. Forced colours: the panel keeps a border in CanvasText (its hairline is a box-shadow, which forced colours drop; a border because Radix's menus set an inlineoutline: noneon the panel), the brace panel stays boxless, the leader is drawn in CanvasText, and the brace is a border, which follows the text colour.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
PopoverContent variant | "leader" | "brace" | "cut" | "leader" | leader: the panel hangs on a hairline from a dot on the trigger's edge. brace: the panel has no box; a brace over its words gathers their width to one point at the trigger's middle, and the point follows the trigger when the panel shifts to stay on screen. cut: the panel's edge cuts through the trigger's words at half their height and the panel rides over their lower half; the join is the cut. All three hang above or below the trigger. |
PopoverContent side / align / sideOffset | Radix | "bottom" / "start" (brace: "center") / 27 (brace: 6, cut: 0) | Where the panel hangs. The leader and the brace show when it sits above or below the trigger. |
PopoverAnchor | Radix | Hangs the panel from another element than the trigger. | |
PopoverClose | Radix | Puts the panel away. Use asChild to wear a button. | |
ot-pop | class | The panel's class, shared with the menus and pickers that open the same way. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Date picker, popover | Weingart callouts | Hung from the opener on a leader line |
| Brace popover | The score's brace; the underbrace | The panel's width gathered to one point at the opener |
| Cut popover | SPECTRA | The panel's edge cuts through the opener's words |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Popover | Arpeggio | Dot, leader, panel |
| Brace popover | Gather | The arms open out from the middle to the ends of the words, then the ends curl in |
| Cut popover | Cut | The panel's edge rises from the foot of the words to their middle |