bx

Popover

Displays rich content in a portal, triggered by a button.

tsx
<Popover>
  <PopoverTrigger for="dimensions">
    <Button variant="outline">Open popover</Button>
  </PopoverTrigger>
  <PopoverContent id="dimensions" className="w-80">
    <div className="grid gap-4">
      <div className="space-y-2">
        <h4 className="font-medium leading-none">Dimensions</h4>
        <p className="text-sm text-muted-foreground">
          Set the dimensions for the layer.
        </p>
      </div>
      <div className="grid gap-2">
        <div className="grid grid-cols-3 items-center gap-4">
          <Label htmlFor="width">Width</Label>
          <Input id="width" defaultValue="100%" className="col-span-2 h-8" />
        </div>
        <div className="grid grid-cols-3 items-center gap-4">
          <Label htmlFor="height">Height</Label>
          <Input id="height" defaultValue="25px" className="col-span-2 h-8" />
        </div>
      </div>
    </div>
  </PopoverContent>
</Popover>

Popover est un panel flottant ancré à un trigger, qui s'ouvre au clic. Contrairement à un Dialog (modal centré, backdrop bloquant), le popover est non-modal : le reste de la page reste interactif et le clic extérieur ferme le popover.

Le popover est la fondation d'un paquet d'autres composants qui héritent son pattern :

  • DropdownMenu — popover avec une liste d'items menu.
  • HoverCard — s'ouvre au hover au lieu du clic.
  • Tooltip — hover + contenu minimal texte.
  • Combobox — popover avec search input + liste filtrable.
  • DatePicker — popover autour d'un Calendar.

Architecture PRISM : le trigger porte data-ui="toggle-pop" data-target="#popover-id", le content est un <div data-overlay data-light hidden> avec l'id correspondant. Le data-light distingue les popovers ("light") des dialogs ("heavy") — les popovers ne bloquent pas le scroll body, se ferment au clic extérieur, et ne pilent pas l'overflow.

Utiliser pour : filtres avancés, éditeurs inline (couleur, dimensions), détails contextuels, forms courts, aperçus d'entités.

Installation

Terminal
$ bext ui add popover

Usage

tsx
import { Popover, PopoverTrigger, PopoverContent, Button } from "@bext-stack/ui/primitives"

<Popover>
  <PopoverTrigger for="dimensions">
    <Button variant="outline">Open popover</Button>
  </PopoverTrigger>
  <PopoverContent id="dimensions" className="w-80">
    {/* contenu libre : form, liste, détails */}
  </PopoverContent>
</Popover>

Anatomy

<Popover>
  ├── <PopoverTrigger for="pop-id">          // data-ui="toggle-pop"
  │   └── <Button>Open popover</Button>
  └── <PopoverContent id="pop-id">          // data-overlay data-light hidden
      └── ...contenu libre...               // form, liste, texte

API Reference

Popover

Prop Type Default Description
className string Wrapper (relative inline-block). Contient trigger + content.

PopoverTrigger

Prop Type Default Description
for string Match l'id du PopoverContent à ouvrir. Génère data-target="#for".
children ReactNode Élément déclencheur (bouton, lien, icône). Le clic dispatch au toggle.

PopoverContent

Prop Type Default Description
id string Match PopoverTrigger.for.
align "start" | "center" | "end" "start" Alignement horizontal du panel par rapport au trigger. start→left-0, center→center, end→right-0.
className string Classes CSS. Défaut : absolute top-full mt-2 w-72 rounded-md border bg-popover p-4 shadow-md.

Data Attributes

Attributs qu'on peut cibler en CSS custom pour override le style par état.

Attribute On Values Description
data-ui="toggle-pop" PopoverTrigger data-target="#id" Cliquer ouvre/ferme le popover.
data-overlay data-light PopoverContent hidden par défaut L'attribut data-light distingue les popovers des dialogs — l'island les ferme au clic extérieur sans piler le body overflow.

Keyboard

Key Action
Tab Focus le trigger. Une fois popover ouvert, Tab traverse les enfants intérieurs.
Enter / Space Sur trigger fermé : ouvre. Sur bouton intérieur : active.
Escape Ferme le popover ouvert. Focus revient au trigger.

Accessibility

PopoverTrigger devrait porter aria-haspopup="true" et aria-expanded="true|false". En PRISM ce n'est pas automatique — poser à la main sur le bouton enfant.

PopoverContent devrait porter role="dialog" quand le contenu est complexe (form, liste), role="tooltip" pour un contenu descriptif court, ou aucun rôle pour du texte simple.

Focus : contrairement à Dialog (focus trap), le popover ne trap pas le focus. Tab peut sortir. C'est intentionnel — le popover est non-modal. Si tu veux trap, utilise Dialog.

Utiliser Dialog à la place quand : le contenu est un vrai formulaire critique (2+ champs required), une action doit être bloquante, ou le contenu doit avoir un backdrop.

Examples

Contenu minimal (info)

tsx
<Popover>
  <PopoverTrigger for="info">
    <Button variant="outline">Info</Button>
  </PopoverTrigger>
  <PopoverContent id="info" className="w-64">
    <p className="text-sm text-muted-foreground">...</p>
  </PopoverContent>
</Popover>

Aligned end (à droite du trigger)

tsx
<PopoverContent id="menu" align="end" className="w-64">
  ...
</PopoverContent>
Try it live
Open in play

Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom — fine-grained reactivity, no virtual DOM.

src/app/page.tsx 2 files · signals · runs in your browser
Pure PRISM + real bext signals — fine-grained, no re-render.