Skip to content

Components

Dialog

A decision that needs the page stopped: a restrained scrim, trapped focus, and Escape or an outside click to leave.

Examples

import { DialogRoot, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter } from "@luc/ui";

Title, description, body, footer

The close affordance is always present and always labelled; the body is where the work happens.

A narrow confirm

`width="sm"` for a decision with no body — the button that opens it is already the sentence.

A long body scrolls inside

`width="lg"` with a `max-h` on the body: the page behind stays locked and the header and footer stay put.

Behaviour

What the foundation guarantees, from one component.

Focus

Focus moves into the dialog on open, is trapped while open, and returns to the trigger on close.

Dismissal

Escape and outside-click close it. Interactive content inside does not.

Scroll

The page behind is locked; long bodies scroll inside `DialogBody` with a fixed header and footer.

Motion

200ms entrance, 140ms exit. Under reduced motion both collapse to a fade.

Labelling

`DialogTitle` and `DialogDescription` are wired to the dialog's ARIA attributes automatically.

Layering

Scrim at z-overlay, content at z-modal. Nothing in a page can sit above a dialog by accident.

API

Component props
PropTypeDefaultNotes
DialogContent width"sm" | "md" | "lg""md"Max width; the dialog is always inset 1rem from the viewport.
DialogContent showClosebooleantrueBuilt-in, labelled close button.
DialogHeader iconReactNode—Optional 32px icon tile for orientation.
DialogRoot open / onOpenChangeboolean / (open: boolean) => void—Controlled usage; uncontrolled by default.
DialogTrigger asChildboolean—Attach the trigger to any focusable element.

Rules

Do

  • Use a dialog for a decision that needs context, not for every edit.
  • Put the primary action last in the footer.
  • Keep the title short enough to read in one glance.

Don't

  • Open a dialog from inside another dialog.
  • Hide the only way to continue behind a scroll.
  • Use alert() or a browser-native confirm.

Usage

import { DialogRoot, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogBody, DialogFooter, DialogClose } from "@luc/ui/components/dialog";
<DialogRoot>
  <DialogTrigger asChild>
    <Button>Open run detail</Button>
  </DialogTrigger>
  <DialogContent width="md">
    <DialogHeader>
      <DialogTitle>Run detail</DialogTitle>
    </DialogHeader>
    <DialogBody>…</DialogBody>
    <DialogFooter>
      <DialogClose asChild>
        <Button variant="ghost">Close</Button>
      </DialogClose>
    </DialogFooter>
  </DialogContent>
</DialogRoot>

Guidelines

Do

  • Use a modal dialog for a task that needs the page's context but not its controls, and put the dialog's own next step in the footer.
  • Set `width` rather than a `className` on the content; the width lands on the panel, while a class lands on the centring frame.

Don't

  • Open a dialog from a dialog when a second step would do; nested modals double the focus trap and the escape path.

Accessibility

`DialogContent` is a modal: it traps Tab, closes on Escape, restores focus to the trigger, and locks the page behind it. `DialogTitle` is required for the accessible name; without it the dialog announces as “dialog”.

Luc UI v1.0DialogBodydark theme