Dialog
The only component permitted to take the screen away from a clinician. That power is why it is rare, why it is small, and why almost every request for one is better served by something else.
Overview
A dialog interrupts. It steals focus, traps the keyboard, dims the record behind it and refuses to go away until answered. In consumer software that is a mild annoyance. In clinical software it is a deliberate decision to stand between a clinician and a patient's data.
So the bar is high: a dialog is justified only when continuing without a decision would be worse than the interruption. Everything else — status, findings, confirmations, errors — belongs in an Alert Banner, inline, or in a popover.
Rare
Every dialog spends the clinician's attention and the product's credibility. If two appear in one workflow, the workflow is wrong, not under-dialogued.
Self-contained
Everything needed to answer is inside it. A dialog that asks a question whose answer is hidden behind it forces a guess.
Never over an alarm
No dialog may obscure an active Critical or Urgent alarm. The interruption is always subordinate to the patient signal.
When to use
| Use a Dialog | Use something else |
|---|---|
| An irreversible action needs confirmation — discarding a draft report | A reversible action → act immediately, offer undo |
| A decision must be made before the workflow can continue — claiming or discarding another clinician's draft | Information the clinician should see but need not answer → Alert Banner |
| A short, focused input is needed out of context — a reason for emergency access | A form of more than about six fields → its own screen |
| An identity or context boundary is being crossed — switching user | A finding, however serious → Alert Banner. Alarms are not dialogs. |
It is tempting to make a STEMI detection a modal — it feels proportionate to the urgency. It is the wrong call. A modal alarm blocks the very screens the clinician needs to assess it, can only show one alarm at a time, and trains a reflex of dismissing modals fast. Alarms are persistent banners that can be read, ranked and acted on alongside the evidence. See Alert Banner.
Anatomy
| Element | Rule |
|---|---|
| Title | A question or a statement of the decision. Names the object — "Discard this draft report?", never "Are you sure?" |
| Description | What will happen, and to whom. Includes the patient identifier when the action is patient-scoped, because the header may be obscured. |
| Actions | Two, occasionally three. Each label names its outcome. Confirming action last, on the right; cancel first. |
| Scrim | Dims but never hides. The record behind stays legible enough to read an identifier. |
| Close affordance | Present only where dismissal is a safe default. Omitted where a decision is genuinely required. |
Variants
One of these three. Dialog builds confirm. It
cannot build decide or the single-action input shown below, and
this page used to publish a variant prop for all three that it does not have.
The patterns stand as design guidance; if you need one, build it, and read the warning
under decide first.
confirm
Two actions, cancel and proceed. What Dialog builds: both are required by its
type, and Escape takes cancel.
decide
No safe default exists, so there is no cancel and no close affordance — both options are real choices. Used sparingly; the identity-boundary case from Sign in / sign out is the canonical one.
Do not build this with Dialog. Passing the two options as
confirm and cancel looks like it works, and makes Escape choose the
second one: a default nobody chose, on exactly the question that has none.
input
A short focused form — at most about six fields. Anything larger is a screen, not a dialog,
because a long form inside a modal cannot be cross-referenced against the record behind it.
A form goes in Dialog’s children; the single-action example
below does not fit it, because its type requires a way out.
Note the ordering in that example: access is already granted when the dialog appears. A break-glass prompt that gates entry on typing a justification is a dialog standing between a clinician and a patient in an emergency. Capture the reason; never charge admission for it.
Behaviour
- Focus moves to the dialog itself on open, so its title and question are read first and no button is the default answer. Never to a destructive button — nor to any other: this line used to say the first interactive element.
- Focus is trapped while open and returns to the trigger on close.
- Escape takes the cancel action, always — the safe direction, and the reason a dialog with no safe default cannot be built with this component.
- Clicking the scrim does nothing, for any dialog. A stray click is not a decision.
- One at a time. Dialogs never stack. A second request replaces or queues, never layers.
- Alarms outrank dialogs. An arriving Critical alarm renders above the scrim and stays readable and pressable. It does not close the dialog and does not steal focus; a keyboard or screen-reader user, who is inside the dialog, hears it announced and reaches it with Escape, which cancels. This was true of nothing until the layout was measured: the scrim covered the alarm region, and the dialog made it inert.
A dialog outlives the thing it is about
A dialog is a pause. The record does not pause with it — a colleague files a result, a device posts a reading, the patient's status changes — and the question on screen was composed from data that may no longer be true when it is answered.
A dialog states the data its question rests on, and says so when that data changes. A confirmation whose subject moved underneath it is a decision recorded against something that is no longer the case.
- The values in the question are shown, not implied. "Give 6 units — glucose 7.8 mmol/L at 14:26, 45 g carbohydrate entered" can be checked. "Confirm dose?" cannot, and the reader has to trust that what they saw a minute ago still holds.
- A material change reopens the question rather than resolving it. If a new glucose arrives while the dialog is open, the dialog says so and asks again — it does not silently update the number under the button, which would be worse than either.
- An immaterial change is left alone. Reopening a confirmation on every unrelated record update trains the reader to dismiss it, which is the failure the rule exists to prevent.
- The decision records what was on screen, not what is true at commit. A clinician answered the question they were shown, and the record should be able to reconstruct that question — see Authorship & the record.
- A dialog whose subject has gone closes and says why. If the patient context changed or the order was withdrawn, the answer is not "confirm anyway".
If it scrolls, it is the wrong container
The common accommodation for a long dialog is a scrolling body with a sticky footer, so the actions stay reachable. That arrangement has a specific consequence: the confirm button is available while most of the content has not been seen.
The tempting fix — disable confirm until the reader scrolls to the bottom — is prohibited here. Editing the draft rejects it because it measures the scrollbar rather than the reading, and produces an audit trail of attention nobody paid. So the resolution is upstream:
- If answering the question requires reading content that must scroll, a dialog is the wrong container. Use a page or a panel, where the reader can move around, keep their place, and come back.
- A dialog may scroll for reference material the decision does not depend on — a long list of what will be affected, where the count and the consequence are already stated above the fold.
- The question and its actions fit without scrolling, always. If the headline, the values it rests on and the buttons do not fit at 320 px and 200 % zoom, the question is too big for this shape.
One at a time, and no suppression
- Never a dialog from within a dialog. Escape becomes ambiguous, focus return has two candidates, and the reader is two decisions deep with no map. A second question replaces the first or waits for it.
- No "don't show this again". Offered widely as a footer checkbox in place of a tertiary button, and refused here: it lets a clinician permanently silence a safety confirmation in the one moment they are most rushed, and the next person at that workstation inherits the silence without knowing it exists. If a confirmation is dismissable forever it was not needed; if it is needed it is not dismissable forever. Where a prompt genuinely fires too often, the fix is in the rate, not in a checkbox.
- No tabs inside a dialog. A dialog with tabs is an application in a box, and it contradicts the reason a dialog is allowed to block at all — that the task is short and singular.
- The scrim is not the only signal. It tells a sighted reader the content
behind is unavailable;
aria-modaland an inert background tell everyone else. A scrim alone leaves the page readable to exactly the people who cannot see that it is greyed out.
Writing the dialog
- Title names the object. "Discard this draft report?" not "Confirm".
- Description names the consequence and the patient. The header behind is dimmed — identity has to be restated inside.
- Buttons name outcomes, echoing the title. Never "Yes"/"No"/"OK".
- Say what is not affected. "The original ECG and the model finding are unaffected" prevents a clinician hesitating over a safe action.
- No apologies, no exclamation marks. Two to four words per button, 35 % expansion budget for translation.
Object named, patient restated, scope of loss bounded, labels readable out of context.
Sure about what, for whom, losing what? The destructive action is styled as primary and focused by default.
What the component enforces, and what it only asks for
This is one of the few components here that holds most of its own rules, because a dialog is behaviour before it is a box. The ones it does not hold are the ones about what the question is made of.
| Rule | Held by | How |
|---|---|---|
| A confirmation cannot be built without a way out | The component | confirm and cancel are both required. A dialog offering only a way forward is not expressible.Asserted: Dialog: cancel precedes confirm in reading order |
| A destructive confirm is disarmed on appearance | The component | The confirm button is disabled for ARM_DELAY_MS, so a press already in flight cannot land on it.Asserted: Dialog: confirmation is disarmed on appearance, then arms |
| Focus returns to whatever opened it | The component | The opener is captured on mount and focused on close — asserted in a real browser, because it was claimed for a long time and absent. Asserted: Dialog: focus returns to whatever opened it |
| Tab is trapped inside it | The component | Cycled at both ends. Without it a keyboard user tabs into content the scrim only appears to be blocking. Asserted: Dialog: Tab is trapped inside it |
| The background is inert, not merely covered | The component | Every sibling on the path from the scrim to <body> is marked inert while the dialog is open, and restored on close. Read from Chromium’s accessibility tree: the control that opened the dialog is absent from the tree while it is open and back when it closes. aria-modal alone did not do this — the page said “inert” for a long time while nothing set it.Asserted: AX tree: the open dialog is named by its heading, and what is behind it is gone |
| An alarm raised while the dialog is open stays on top | The component | In AppLayout’s alarm region it is drawn above the scrim, and the dialog leaves it, and any role="alert", out of the background it makes inert. Measured with the stylesheet a consumer has: the scrim was z-40 over an alarm region at z-20, and the inert background took the alarm with it, so an alarm raised during a confirmation could be neither seen nor pressed. A pointer can acknowledge it at once; a keyboard or screen-reader user hears it announced and reaches it with Escape, which takes the safe cancel.Asserted: Layout: an alarm raised while a dialog is open stays on top, and stays actionable |
| The question states the data it rests on | You | description is a string you compose. Nothing here knows the glucose it is quoting, or that a newer one arrived while the dialog was open. |
| A material change reopens the question | You | The component has no subscription. Detecting that the subject moved underneath it, and asking again rather than silently updating the number under the button, is yours. |
| It fits without scrolling | You | Content is children. If answering requires reading something that scrolls, the container is wrong, and only you can see that. |
| Never a dialog from within a dialog | You | Nothing prevents a second open. Escape then has two meanings and focus return has two candidates. |
A rule stated on a component page reads as a rule the component keeps. 5 of these 9 are; 4 are obligations this page was quietly handing to the reader. The mechanics are held here. What the question is about, and whether it is still true, is not — and a confirmation recorded against data that moved is the failure this page exists to prevent.
Do's and don'ts
Initial focus on the safe option. Pressing Enter reflexively loses nothing.
Focus on the destructive action, and no patient named. Enter deletes the report.
The alarm renders above the scrim and stays readable. The dialog waits its turn.
A modal over everything. A STEMI has just fired for another patient and nothing on screen says so.
Give 6 units?
Glucose 7.8 mmol/L at 14:26 · 45 g carbohydrate entered · 1.2 units still active
A newer glucose arrived at 14:31. Recalculate
The question shows what it rests on, and says when that changed. The clinician answers the question in front of them.
Confirm dose?
Yes · Cancel
Nothing to check. The reader is trusting that what they saw a minute ago still holds, and a newer glucose arrived while they read it.
A long consent summary opens as a panel — scrollable, keeps its place, can be left and returned to.
If answering needs content that must scroll, the container is wrong. A page can hold a long read; a dialog cannot.
The same summary in a dialog with a sticky footer. Confirm is reachable from the first line.
The button is available while the content is unread — and gating it on scroll is prohibited, because that measures the scrollbar rather than the reading.
The prompt fires less often, because the rule that raises it was narrowed.
Where a confirmation genuinely fires too often, the fix is the rate. See Messaging.
A safety confirmation silenced permanently in the moment the clinician is most rushed — and the next person at that workstation inherits the silence without knowing it exists.
Accessibility
- role="alertdialog" with
aria-modal, a focus trap, focus restored to whatever opened it, and an inert background. Written by hand rather than taken from Radix, because the registry ships with no runtime dependencies — this page previously credited a primitive the component does not use, and described focus behaviour it did not have until the interaction gate went looking. aria-labelledbyandaria-describedbywired to the title and description, so the whole question is announced on open.- Initial focus is never destructive. Enter must not delete anything.
- Reachable at 200 % zoom and 320 px: the dialog scrolls internally while the action row stays visible, rather than pushing buttons off-screen.
- Touch targets at
lg— 44 px minimum, with adjacent actions separated by at least 8 px so a mis-tap does not confirm a deletion. - Under
prefers-reduced-motionthe dialog appears without transition, never without the scrim.
Outcomes of use
What this contributes to, in the terms of Usability & context of use. These are attributes believed to contribute to an outcome; the outcome itself is settled by observing real use in a specified context, not by this page.
- Effectiveness — The decision recorded matches the decision intended. A useful signal is how often a confirmed action is reversed shortly afterwards.
- Efficiency — Every dialog costs an interruption and a context switch. The measure is dialogs encountered per completed workflow, and the target is close to zero.
- Satisfaction — Perceived control. A dialog appearing where a clinician expected none reduces perceived safety even when the question is reasonable.
Clinical safety notes
Trace these in your risk file (ISO 14971) and usability engineering file (IEC 62366-1). Which of these the component keeps and which are yours is stated in the guarantees table; a control the component does not keep is one your risk file must show you keep.
- Alarms render above the scrim and are never obscured. (you) Mitigates: a blocking interaction hiding a patient signal.
- Never used for alarms. (you) Mitigates: single-alarm tunnel vision, and a trained reflex of fast modal dismissal.
- Patient identifier restated inside. (you) Mitigates: confirming a destructive action against the wrong record while the header is dimmed.
- Initial focus is the dialog, never a button; Escape takes cancel; the scrim does nothing. (the component) Mitigates: accidental irreversible action. This bullet used to say Escape was disabled for destructive confirms, which the component never did — and which would have removed the safe way out.
- Never gates emergency access. (you) Mitigates: authentication or documentation standing between a clinician and a patient.
- Never stacks. (you) Mitigates: a decision made without the context of the one beneath it.
- Labels name outcomes. (you) Mitigates: confirmation blindness.
Implementation
$ npx shadcn@latest add https://md.notjustany.tech/r/dialog.json
<Dialog
open={open}
title="Discard this draft report?"
description={`Your interpretation for ${patient.display} will be lost. The
original ECG and the model finding are unaffected.`}
cancel={{ label: "Keep editing", onSelect: () => setOpen(false) }}
confirm={{ label: "Discard draft", onSelect: discard, destructive: true }}
/>
// Both 'confirm' and 'cancel' are required, so a dialog with only a way
// forward cannot be constructed. The confirm arms after a short delay;
// focus lands on the dialog itself, not on either button.
Props
What Dialog takes. This table used to list variant,
subjectId, onEscapeKeyDown and onPointerDownOutside
— the props of a primitive library this package does not use. None of them existed.
| Prop | Type | Default | Notes |
|---|---|---|---|
open | boolean | — | Required. The background is made inert while it is true. |
title | string | — | Required. The dialog’s name. Ask the question. |
description | string | — | The consequence, and the patient it applies to — restate the identifier here. |
confirm | { label, onSelect, destructive? } | — | Required. Disarmed for a moment after opening. |
cancel | { label, onSelect } | — | Required. What Escape does. |
children | ReactNode | — | A short form, where the decision needs one. |
A dialog rendered with priority or any --alarm-* token in its
subtree fails the build. Alarms are banners; this is enforced rather than documented, for the
same reason the alarm tokens are governed.
Related
- Alert Banner — for anything that is not a required decision.
- Button — action hierarchy and destructive styling.
- Sign in / sign out — the identity-boundary dialog.
- Clinician override — why disagreement gets no confirmation dialog.
- Messaging — the branch test, and why most notices that feel modal are not.