opsinjs
ComponentsOverlay

Popover

A small panel anchored to the control that opened it, floated in a portal and dismissed by leaving it, built as a thin themed wrapper over Base UI Popover.

Preview

Theme
Density
Text
Status
popover · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/popover

The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.

Usage

import { Popover } from "@/components/ui/popover"
<Popover title="How readings are grouped" trigger="Why two lists?">
  <p>
    Entries you add yourself sit in one list, and entries from a connected
    device sit in another, so you can always tell which is which.
  </p>
</Popover>

When to use it

Use it when

  • A short aside anchored to a control, such as a note explaining a label.
  • A couple of options or a small form fragment, such as renaming a list.
  • Anywhere dismissing without acting is a valid outcome, because the page behind stays live.

Do not use it when

  • The reader must answer before they carry on, so dismissing would leave the product guessing.Use dialog instead.
  • The content is a large form, or needs its own scroll and a docked layout.Use sheet instead.
  • You only want a short passive label, nothing to open or dismiss.Use term instead.

Anatomy

  1. PopoverBase UI's Popover.Root. Owns the open state, uncontrolled unless open is passed.Controlled by open
  2. Popover.TriggerThe supplied button, with its target floor, focus ring and aria-expanded.Controlled by trigger
  3. Popover.PopupThe floating panel, a role="dialog" through a portal, capped at a readable measure.Controlled by children
  4. Popover.TitleThe heading, rendered when title is passed and wired to aria-labelledby.Controlled by title
  5. Popover.ArrowA card-coloured diamond pointing to the trigger, bordered on the outward edges.
  • Popover 1renders no element
    • Popover.Trigger 1data-slot="popover-trigger"
    • Popover.Popup 1data-slot="popover-popup", role="dialog"
      • Popover.Arrow 1data-slot="popover-arrow", aria-hidden
      • Popover.Title 0..1data-slot="popover-title", optional

Examples

Anchored to a button

A short note anchored to a button, left uncontrolled. Escape or an outside press closes it and returns focus.

Theme
Density
Text
Status
popover-anchored-to-a-button · base base · style base-lyraOpen under the product theme

With a form fragment

A labelled input and a save button. open and onOpenChange take the state over, since a control inside closes it.

Theme
Density
Text
Status
popover-with-a-form-fragment · base base · style base-lyraOpen under the product theme

Content guidelines

Give every popover a title: the panel is a role="dialog", and the title is the name a screen reader announces. Keep the body to a sentence or two, and never put a clinical reading or verdict loose inside one.

Do

"How readings are grouped", titled, short, easily dismissed.

Don’t

A blood pressure reading and its status, untitled: no name, no range, wrong surface.

Accessibility

Audited against WCAG 2.2 AA on /view/base/base-lyra/component/popover, in a source pass and a rendered pass. Author-run, not independent; clinical review is pending.

Fixed. The trigger's hit area was floored on height alone, so an icon-only trigger could clear the AA 24px minimum while still missing the house width floor. A matching min-w at --opsin-target-minimum now floors it too, following the icon-button pattern.

Confirmed.

  • The panel is role="dialog", named by aria-labelledby; a missing title warns.
  • Focus moves into the panel on open and back to the trigger on close, by Base UI.
  • The arrow is aria-hidden; under prefers-reduced-motion only opacity crossfades.
  • No status colour, literal, ramp step or px size: neutral chrome, the overlay shadow token, and the body and headline steps only.

Residual. The wrapper takes no aria-label prop, so an icon-only trigger needs a name from the caller's trigger node; every shipped trigger already uses named text. Contrast pairs remain unmeasured, below.

KeyActionNotes
TabMoves focus to the triggerOne tab stop; the panel joins the tab order only when open.
Enter, SpaceOpens the panel from the triggerFocus moves onto its first focusable content.
EscapeCloses the panelFocus returns to the trigger.
Tab, Shift+TabMoves through the focusable content in the panelFocus is not trapped; past the last control it leaves the panel.
PairThemeAPCA LcWCAG 2.2Floor
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor
body text on the canvas materiallight104.717.96:1Pass
body text on the canvas materialdark-100.518.00:1Pass
body text on the inset materiallight98.116.32:1Pass
body text on the inset materialdark-100.117.18:1Pass
body text on the card materiallight104.717.96:1Pass
body text on the card materialdark-99.616.32:1Pass
body text on the raised materiallight104.717.96:1Pass
body text on the raised materialdark-97.513.77:1Pass
body text on the sheet material over the darkest backdroplight99.916.76:1Pass
body text on the sheet material over the darkest backdropdark-99.716.47:1Pass
body text on the sheet material over the lightest backdroplight104.717.96:1Pass
body text on the sheet material over the lightest backdropdark-80.86.07:1Pass
body text on the overlay material over the darkest backdroplight85.913.48:1Pass
body text on the overlay material over the darkest backdropdark-99.916.77:1Pass
body text on the overlay material over the lightest backdroplight104.717.96:1Pass
body text on the overlay material over the lightest backdropdark-54.22.69:1Below floor

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

API reference

Prop

Type

Generated from PopoverProps in registry/bases/base/popover.tsx.

trigger is the button's visible content; a button here would nest inside another. side defaults to bottom, a preference the primitive overrides on collision. className merges onto the panel and must resolve neither colour axis.

  • Dialog blocks the page until the reader answers.
  • Sheet docks to an edge, built for touch and larger forms.
  • Tooltip is a passive hover label; a phone has no hover.
  • Term shows a word's everyday meaning inline, nothing to open.

On this page