bx

Dialog

A window overlaid on either the primary window or another dialog window.

tsx
<Dialog>
  <DialogTrigger for="edit-profile">
    <Button variant="outline">Edit Profile</Button>
  </DialogTrigger>
  <DialogContent id="edit-profile">
    <DialogHeader>
      <DialogTitle>Edit profile</DialogTitle>
      <DialogDescription>
        Make changes to your profile here. Click save when you're done.
      </DialogDescription>
    </DialogHeader>
    <div className="grid gap-4 py-2">
      <div className="grid gap-2">
        <Label htmlFor="name">Name</Label>
        <Input id="name" defaultValue="Sofia Davis" />
      </div>
      <div className="grid gap-2">
        <Label htmlFor="user">Username</Label>
        <Input id="user" defaultValue="@sofia" />
      </div>
    </div>
    <DialogFooter>
      <DialogClose>
        <Button>Save changes</Button>
      </DialogClose>
    </DialogFooter>
  </DialogContent>
</Dialog>

Dialog est une modale centrée qui interrompt le flow utilisateur pour une action précise (édition d'un profil, confirmation d'une opération, formulaire compact). Contrairement à un Sheet ou Drawer qui glissent depuis le bord, la Dialog apparaît au centre de l'écran avec un backdrop bloquant.

Architecture PRISM : le trigger est un simple bouton avec data-ui="toggle-pop" data-target="#dlg-id". Le content est un <div data-overlay hidden> avec l'id correspondant. L'island ui.js intercepte le clic sur le trigger, retire hidden, applique overflow:hidden au body. Escape / clic backdrop / data-ui="close" referme.

Le lien trigger↔content passe par un id partagé, pas par un context React. C'est plus verbeux mais permet plusieurs Dialog sur la même page sans conflit d'état.

Composition idiomatique :

  • DialogHeader — titre + description au-dessus du corps.
  • DialogTitle<h2> semantique, rôle heading pour les AT.
  • DialogDescription — paragraphe muted, lu par les lecteurs d'écran après le titre.
  • DialogFooter — barre d'actions en bas (typiquement Cancel + Save alignés à droite).
  • DialogClose — wrapper qui ferme le dialog au clic sur son enfant (utilise data-ui="close").

Pour une confirmation destructive (Delete account, Discard changes), préférer AlertDialog qui force une action explicite (pas de fermeture par clic backdrop).

Installation

Terminal
$ bext ui add dialog

Usage

tsx
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from "@bext-stack/ui/primitives"
import { Button } from "@bext-stack/ui/primitives"

<Dialog>
  <DialogTrigger for="edit-profile">
    <Button variant="outline">Edit Profile</Button>
  </DialogTrigger>
  <DialogContent id="edit-profile">
    <DialogHeader>
      <DialogTitle>Edit profile</DialogTitle>
      <DialogDescription>
        Make changes to your profile here. Click save when you're done.
      </DialogDescription>
    </DialogHeader>
    {/* form fields */}
    <DialogFooter>
      <DialogClose>
        <Button>Save changes</Button>
      </DialogClose>
    </DialogFooter>
  </DialogContent>
</Dialog>

Anatomy

<Dialog>
  ├── <DialogTrigger for="dlg-id">                    // data-ui="toggle-pop"
  │   └── <Button>Edit Profile</Button>
  └── <DialogContent id="dlg-id">                     // data-overlay hidden
      ├── <DialogHeader>
      │   ├── <DialogTitle>Edit profile</DialogTitle>
      │   └── <DialogDescription>...</DialogDescription>
      ├── <div>...form fields...</div>                // corps libre
      └── <DialogFooter>
          └── <DialogClose>
              └── <Button>Save</Button>

API Reference

Dialog

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

DialogTrigger

Prop Type Default Description
for string Doit matcher l'id du DialogContent à ouvrir. Génère data-target="#for".
children ReactNode Bouton ou lien qui déclenche l'ouverture.

DialogContent

Prop Type Default Description
id string Doit matcher DialogTrigger.for. Sert au data-target du toggle-pop.
className string Classes CSS. Défaut : centered fixed, w-full max-w-lg, bg-background rounded-lg border shadow-lg.

DialogHeader

Prop Type Default Description
className string flex flex-col gap-1.5 text-center sm:text-left, au-dessus du corps.

DialogTitle

Prop Type Default Description
className string

stylé text-lg font-semibold leading-none tracking-tight.

DialogDescription

Prop Type Default Description
className string

stylé text-sm text-muted-foreground.

DialogFooter

Prop Type Default Description
className string Barre d'actions bas. flex-col-reverse sur mobile, flex-row justify-end sur sm+.

DialogClose

Prop Type Default Description
children ReactNode Wrap un bouton/lien avec data-ui="close" pour fermer le dialog.

Data Attributes

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

Attribute On Values Description
data-ui="toggle-pop" DialogTrigger data-target="#dlg-id" Cliquer ouvre l'overlay ciblé.
data-overlay DialogContent hidden par défaut Marqueur pour l'island : à retirer/ajouter le hidden au toggle. body.overflow=hidden quand pas de data-light.
data-ui="close" DialogClose Cliquer ferme le premier ancêtre [data-overlay].

Keyboard

Key Action
Tab / Shift+Tab Focus trap dans le dialog — cycle uniquement les éléments focusables intérieurs.
Enter Activate le bouton focus (par défaut le premier submit / bouton primary du footer).
Escape Ferme le dialog. Le focus revient au trigger d'origine.

Accessibility

Le DialogContent pose role="dialog" aria-modal="true". Il faut aussi lier aria-labelledby vers l'id du DialogTitle et aria-describedby vers l'id du DialogDescription pour que le screen reader annonce correctement l'ouverture.

Focus trap : quand le dialog ouvre, le focus est déplacé sur le premier élément focusable intérieur. Tab reste piégé à l'intérieur (cycle back sur le premier). Escape ou fermeture ramène le focus au trigger d'origine.

Backdrop : par défaut, cliquer en dehors ferme le dialog. Pour un formulaire non-triviale ou une action critique, préférer AlertDialog qui force une décision explicite.

Nested dialogs : possible mais déconseillé — préférer un flow linéaire (Dialog → refermer → Dialog suivant).

Examples

Confirmation simple (single button)

tsx
<Dialog>
  <DialogTrigger for="confirm-delete">
    <Button variant="outline">Delete item</Button>
  </DialogTrigger>
  <DialogContent id="confirm-delete">
    <DialogHeader>
      <DialogTitle>Delete this item ?</DialogTitle>
      <DialogDescription>This action cannot be undone.</DialogDescription>
    </DialogHeader>
    <DialogFooter>
      <DialogClose><Button variant="outline">Cancel</Button></DialogClose>
      <DialogClose><Button variant="destructive">Delete</Button></DialogClose>
    </DialogFooter>
  </DialogContent>
</Dialog>

Header sans description

tsx
<DialogContent id="invite">
  <DialogHeader>
    <DialogTitle>Invite team members</DialogTitle>
  </DialogHeader>
  <Input placeholder="colleague@example.com" />
  <DialogFooter>
    <DialogClose><Button>Send invite</Button></DialogClose>
  </DialogFooter>
</DialogContent>
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.