Components · Feedback & status

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.

Stable · v1.0 shadcn/ui WCAG 2.2 AA

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 DialogUse 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.
An alarm is never a dialog

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

Destructive confirmation
ElementRule
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

What the shipped component builds

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.

variant: decide · no safe default

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.

variant: input · emergency access reason
Access first, reason second

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

Critical alarm arriving over an open dialog

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.

The rule

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.

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:

One at a time, and no suppression

Writing the dialog

Do
Discard this draft report?
Your interpretation for HARLAND, A. · MRN 44 812 907 will be permanently lost. The ECG is unaffected.

Object named, patient restated, scope of loss bounded, labels readable out of context.

Don't
Are you sure?
This action cannot be undone.

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.

RuleHeld by How
A confirmation cannot be built without a way outThe 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 appearanceThe 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 itThe 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 itThe 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 coveredThe 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 topThe 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 onYou 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 questionYou 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 scrollingYou 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 dialogYou Nothing prevents a second open. Escape then has two meanings and focus return has two candidates.
Why this is written down

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

Do
Discard this draft report?
Your interpretation for HARLAND, A. · MRN 44 812 907 will be permanently lost.

Initial focus on the safe option. Pressing Enter reflexively loses nothing.

Don't
Discard this draft report?
Your interpretation will be permanently lost.

Focus on the destructive action, and no patient named. Enter deletes the report.

Do
STEMI pattern detected — OKONKWO, B.
renders above the scrim
Switch user?
S. Whitfield's session will be locked.

The alarm renders above the scrim and stays readable. The dialog waits its turn.

Don't
Switch user?
S. Whitfield's session will be locked.

A modal over everything. A STEMI has just fired for another patient and nothing on screen says so.

Do

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.

Don't

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.

Do

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.

Don't

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.

Do

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.

Don't

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

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.

Clinical safety notes

Risk controls this page specifies

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.

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.

PropTypeDefaultNotes
openboolean— Required. The background is made inert while it is true.
titlestring— Required. The dialog’s name. Ask the question.
descriptionstring— 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.
childrenReactNode— A short form, where the decision needs one.
Lint rule

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.

NotJustAnyMed.Tech Design System · Dialog · v1.0 · draft for review
Reference applications named in this system are fictional; all patient data shown is fabricated.