IX Surfaces

Star on GitHubFeedback
IX SurfacesRadixot-pop

Popover

A panel hung from what opened it, on a leader line. Escape or a click elsewhere puts it away.

Report an issue with Popover

Example

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.json

Usage

This example also uses Button.

npx shadcn@latest add https://thedirectors.agency/ui/r/button.json
popover-example.tsx
"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 with popovertarget. 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-variant of 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-point inside .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 inline outline: none on 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

Props of Popover
PropTypeDefaultWhat 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 / sideOffsetRadix"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.
PopoverAnchorRadixHangs the panel from another element than the trigger.
PopoverCloseRadixPuts the panel away. Use asChild to wear a button.
ot-popclassThe panel's class, shared with the menus and pickers that open the same way.

Craft

Where its move comes from

Where its move comes from
ComponentReferenceMove
Date picker, popoverWeingart calloutsHung from the opener on a leader line
Brace popoverThe score's brace; the underbraceThe panel's width gathered to one point at the opener
Cut popoverSPECTRAThe panel's edge cuts through the opener's words

How it moves

How it moves
ComponentArticulationWhat moves
PopoverArpeggioDot, leader, panel
Brace popoverGatherThe arms open out from the middle to the ends of the words, then the ends curl in
Cut popoverCutThe panel's edge rises from the foot of the words to their middle