VII Signals

Star on GitHubFeedback
VII SignalsNative HTML and a hookot-dialog

Dialog

The question, set large, with two plain answers. The page holds still behind it.

Report an issue with Dialog

Example

frame
Delete a note
14 attachments

Delete Spring notes?

The notes and their 14 attachments go for good. This can't be undone.

reply
Unsaved changes
3 edits

Discard your changes to Halden?

Your last three edits haven't been saved. Discarded, they can't be brought back.

ruled
Archive a note
Last opened 12 September

Archive Spring notes?

It leaves your notes and waits in the archive. Bring it back whenever you like.

frame, open
Delete a note
14 attachments

Delete Spring notes?

The notes and their 14 attachments go for good. This can't be undone.

reply, open
Unsaved changes
3 edits

Discard your changes to Halden?

Your last three edits haven't been saved. Discarded, they can't be brought back.

ruled, open
Archive a note
Last opened 12 September

Archive Spring notes?

It leaves your notes and waits in the archive. Bring it back whenever you like.

Install

Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base and scrollbar along.

npx shadcn@latest add https://thedirectors.agency/ui/r/dialog.json

Usage

This example also uses Button.

npx shadcn@latest add https://thedirectors.agency/ui/r/button.json
dialog-example.tsx
import { Button } from "@/components/ui/button"import {Dialog,DialogActions,DialogClose,DialogContent,DialogDescription,DialogMeta,DialogTitle,DialogTrigger,} from "@/components/ui/dialog"function Frame() {return (<><DialogMeta><span>Delete a note</span><hr /><span>14 attachments</span></DialogMeta><DialogTitle>Delete <span className="ot-yours">Spring notes</span>?</DialogTitle><DialogDescription>The notes and their 14 attachments go for good. This can&apos;t be undone.</DialogDescription><DialogActions><DialogClose asChild><Button variant="bracket" data-autofocus>Keep it</Button></DialogClose><DialogClose asChild><Button variant="statement">Delete</Button></DialogClose></DialogActions></>)}// Under reply the answers are bare DialogClose buttons: the dialog sets them in the italic.function Reply() {return (<><DialogMeta><span>Unsaved changes</span><hr /><span>3 edits</span></DialogMeta><DialogTitle>Discard your changes to <span className="ot-yours">Halden</span>?</DialogTitle><DialogDescription>Your last three edits haven&apos;t been saved. Discarded, they can&apos;t be brought back.</DialogDescription><DialogActions><DialogClose data-autofocus>Keep editing</DialogClose><DialogClose>Discard</DialogClose></DialogActions></>)}function Ruled() {return (<><DialogMeta><span>Archive a note</span><hr /><span>Last opened 12 September</span></DialogMeta><DialogTitle>Archive <span className="ot-yours">Spring notes</span>?</DialogTitle><DialogDescription>It leaves your notes and waits in the archive. Bring it back whenever you like.</DialogDescription><DialogActions><DialogClose asChild><Button variant="bracket" data-autofocus>Keep it</Button></DialogClose><DialogClose asChild><Button variant="statement">Archive</Button></DialogClose></DialogActions></>)}const variants = [{ variant: "frame", open: "Delete Spring notes", Body: Frame, alert: true },{ variant: "reply", open: "Discard changes", Body: Reply, alert: true },{ variant: "ruled", open: "Archive Spring notes", Body: Ruled, alert: false },] as constexport default function Example() {return (<div className="grid justify-items-start gap-x-10 gap-y-7 sm:grid-cols-3">{variants.map(({ variant, open, Body, alert }) => (<div key={variant} className="grid justify-items-start gap-4"><span className="ot-label">{variant}</span><Dialog alert={alert}><DialogTrigger asChild><Button variant="bracket">{open}</Button></DialogTrigger><DialogContent variant={variant}><Body /></DialogContent></Dialog></div>))}</div>)}// Pinned open in place: a plain, non-modal <dialog open>, so the row can show each surface without a click.

Contract

  • Underneath: native <dialog> with showModal(), plus a hook.
  • Anatomy: <dialog class="ot-dialog" closedby="any"> holding <form method="dialog">, the .ot-dialog-title question, .ot-dialog-body, then .ot-dialog-actions with a bracket (safe, autofocus) and a statement (the action).
  • It's set like a poster: corner marks instead of a box (ot-corners), an ot-meta row of facts along the top (.ot-dialog-meta), and the question set large, with the item as yours ("Delete Spring notes?"). The backdrop is paper at 86% with a blur. Opening is one short phrase: the corners open out into place, the meta rule draws from the left, and the question breathes out from weight 500 and width 88%.
  • Keyboard: focus goes to the safe answer; Escape closes; a click outside closes. Smooth scrolling pauses while it's open.
  • variant on DialogContent. reply (after "It has to be design."): the question is ours, heavy (800) and narrow (width 72%); the answers are yours, bare DialogClose buttons set large in the italic. The answer you point at, or that holds focus, inks and takes a full stop in the accent (content: "." / "", so it isn't read out); pointing at one takes the stop from the other, so only one is ever in view. Opening focuses the safe answer, so it opens with its stop. ruled (after "Less is more."): a grid of ink hairlines to the dialog's edges. The meta's two facts go to the top corners; the question is set at --ot-ff in the left cell, each of its lines standing on a hairline that draws across from the start as it opens; the detail sits ragged on the far side, set to the end; the answers take the cells below, the safe one at the start, the action at the far end. Below 40rem the grid folds to one column.
  • Alert dialog: for what can't be undone, role="alertdialog" closedby="closerequest" with aria-describedby on the body. It closes only with Escape or an answer, never a stray click. Any dialog can carry a form (the specimen's rename); [data-close] buttons close it without submitting.

Props

Props of Dialog
PropTypeDefaultWhat it does
open / defaultOpen / onOpenChangeboolean, (open) => voidControlled or not, like any shadcn dialog.
alertbooleanfalseA question that needs an answer: role alertdialog, and pointing outside doesn't close it. Escape still does.
variant"frame" | "reply" | "ruled""frame"On DialogContent. frame: corner marks, a row of facts, the question set large. reply: the question heavy and narrow, the answers set large in the italic because the answer is yours; the one you point at, or that has focus, takes a full stop in the accent. Give it bare DialogClose buttons, not Buttons. ruled: a grid of hairlines to the dialog's edges, the facts in the top corners, the question large on ruled lines that draw across as it opens, the detail ragged on the far side and the answers in the cells below.
DialogMetadivThe frame row over the question: what this is, a hairline, one fact.
data-autofocusattributePut it on the answer that should take focus when the dialog opens.
DialogSurfacedialogThe bare native dialog. Sheet and drawer build on it.
DialogTrigger / DialogClose asChildbooleanfalsePass the open or close behaviour to one child control instead of rendering another button.

Craft

Where its move comes from

Where its move comes from
ComponentReferenceMove
DialogSHAPES / GRADIENTS frameCorners and a metadata row
Reply dialog"It has to be design."Heavy roman question, the answers yours in large italic
Ruled dialog"Less is more."The question on ruled lines in a grid of hairlines

How it moves

How it moves
ComponentArticulationWhat moves
DialogBreath outThe corners open out, the rule draws, the question exhales
Reply dialogSpiccatoThe answer you point at inks and its accent full stop lands
Ruled dialogDrawnThe rules under the question draw across from the start