Patterns · composition guide
Overlays pattern
How to build a modal, drawer, or popover — surface choice, safety-critical dismiss behaviour, state reset, drawer placement/width, the async-confirm pattern, inline error surfacing, and danger-styled confirms — built from existing components.
◆ generated from guidelines/overlays.json + parts/overlays.guide.md — do not edit by hand
Based on
The rationale, worked examples, and the full assertion set live in the design skill — this pattern distils the enforceable subset and links each rule back to it.
aha-design:aha-design-overlays
judge aha-design:aha-design-overlays-judge
Choose the surface
| Surface | Use for | Example |
|---|---|---|
| Modal | a focused decision or short flow that should block the page | confirmation, destructive action, one-time config |
| Drawer | a detail view or medium-complexity panel that supplements the page without replacing it | a record's report, distribution settings |
| Popover | a small, anchored, contextual snippet — not multi-step flows | quick info, a compact input, an upsell |
Composed of
What a compliant overlays surface reuses from this design system — the pattern's link into the component graph.
| Reuses | Kind | For | In DS? |
|---|---|---|---|
button | component | the confirm/cancel CTAs; destructive confirm is the danger Button (okButtonProps danger + loading), danger on the button never the title/body | available |
icon | component | the X close glyph and footer action glyphs — functional icons only, via the shared Icon | available |
--aha-color-error | token | danger confirm colour — never hardcode a hex | available |
modal | component | the blocking-decision surface (confirmations, destructive actions) — AntD v6 Modal with mask/keyboard/destroyOnHidden props wired per these rules | missing |
drawer | component | the detail/panel surface — AntD v6 Drawer with placement=right, responsive width, footer actions, destroyOnClose | missing |
popover | component | the anchored contextual snippet surface — AntD v6 Popover | missing |
alert | component | the inline in-overlay error banner (AhaAlert / DS V3 Alert, owned by aha-design-feedback) — never a bare AntD Alert | missing |
Rules
The shippable checklist — each rule traces to an assertion in aha-design:aha-design-overlays.
| Rule | Skill assertion |
|---|---|
| Pick the surface by the job: Modal for a blocking decision or short flow, Drawer for a detail/medium-complexity panel that supplements the page, Popover for a small anchored snippet — never a multi-step flow in a popover. | OVERLAY-01 |
| Destructive overlays MUST NOT dismiss on outside/mask click — set mask={{ closable: false }} on the Modal, leaving Esc (keyboard) and the X button as the only dismiss paths. Non-destructive overlays may allow mask-close. | OVERLAY-02 |
| While an async action is in flight (busy), disable cancel and suppress onCancel/onClose so the user cannot dismiss mid-operation. | OVERLAY-03 |
| Always set destroyOnHidden (Modal) / destroyOnClose (Drawer) so transient state — form input, error text, busy flag — resets between opens. If the instance can stay mounted, also clear local error and busy on open; don't rely on unmount alone. | OVERLAY-04 |
| Drawers default to placement="right" with responsive width width={isMobile ? '100%' : 720} via useIsMobile() — full-bleed on mobile, fixed panel on desktop. The width is a prop, not gated through CSS. | OVERLAY-05 |
| Put navigation/actions (prev/next, retry, position indicator) in the drawer footer, and use styles={{ body: { padding: 0 } }} when the body renders its own padded content. | OVERLAY-05 |
| For confirmations, the modal title is a question ("Delete this survey permanently?"). | OVERLAY-06 |
| A destructive confirm uses okButtonProps={{ danger: true, loading: busy }} — the danger styling lives on the action button, never the title or body — and cancel is disabled while busy (cancelButtonProps={{ disabled: busy }}). | OVERLAY-06 |
| onConfirm returns a promise: guard re-entry (if (busy) return), set busy, clear prior error, then await the action. On rejection stay open, surface the error inline, and clear busy; on success the caller closes. Never close a destructive overlay before its action resolves, and never fire-and-forget. | OVERLAY-07 |
| Surface failures inside the overlay with an inline DS V3 Alert (AhaAlert type="error", styling owned by aha-design-feedback) — never a bare AntD Alert, never a nested modal stacked on top, never a downgrade to a transient toast. The overlay stays open on failure so the user can retry without re-opening. | OVERLAY-08 |
Composition code
Doc-only — this pattern ships no wrapper from this repo. Doc-only pattern — ships no composition wrapper from this repo. The canonical overlay references live in the product (DeleteSurveyConfirmModal.tsx for the destructive modal, RespondentReportDrawer.tsx for the detail drawer); the AhaAlert banner is owned by aha-design-feedback and useIsMobile() by aha-design-shared-components. If a shared overlay wrapper graduates into this DS, add a reuse block and it is gated like a composite.
Guide
Overlays — composition guide
aha-design-overlays. The DS now owns this build ruleset — it is the single source of truth for how an overlay is constructed, and the plugin skill is generated from it. The rationale, worked examples, and the judge/evals stay in the skill; this guide is the shippable checklist. When the two ever disagree, regenerate this file from the owning ruleset.Overlays — modals, drawers, popovers — are transient surfaces layered over the page, and they are where a stray backdrop click can destroy data. They reuse existing components — the shared Button, the AhaAlert banner, useIsMobile() — so this pattern adds no new control; it defines the house rules that keep dismiss behaviour, state reset, and error handling consistent and safe across every AhaSlides product surface. Overlays are built on AntD v6 Modal / Drawer / Popover.
Pick the surface first
| Surface | Use for |
|---|---|
| Modal | A focused decision or short flow that should block the page — confirmations, destructive actions, one-time config |
| Drawer | A detail view or medium-complexity panel that supplements the page without fully replacing it — a record's report, distribution settings |
| Popover | A small, anchored, contextual snippet — quick info, a compact input, an upsell. Not for multi-step flows |
Dismiss behaviour (safety-critical)
- Destructive overlays MUST NOT dismiss on outside/mask click. Set
mask={{ closable: false }}on the Modal. Keepkeyboard(Esc) and the X button as the only dismiss paths, so a stray backdrop click can't trigger destruction. - Non-destructive overlays may allow mask-close.
- While an async action is in flight (
busy), disable cancel and suppressonCancel/onCloseso the user can't dismiss mid-operation.
State reset
- Always set
destroyOnHidden(Modal) /destroyOnClose(Drawer) so transient state — form input, error text, busy flag — resets between opens. - On open, also clear local error and busy state explicitly — don't rely on unmount alone if the instance can stay mounted.
Drawer conventions
- Default
placement="right". - Responsive width:
width={isMobile ? '100%' : 720}viauseIsMobile()— full-bleed on mobile, fixed panel on desktop. Do not gate this through CSS; the width is a prop. - Put navigation/actions (prev/next, retry, position indicator) in the drawer
footer. - Use
styles={{ body: { padding: 0 } }}when the body renders its own padded content.
Modal conventions
- For confirmations, the title is a question — "Delete this survey permanently?".
- Destructive confirm button:
okButtonProps={{ danger: true, loading: busy }}— the danger styling lives on the action button, never the title or body. - Disable cancel while busy:
cancelButtonProps={{ disabled: busy }}.
Async confirm pattern
onConfirm returns a promise. The overlay:
1. Guards re-entry (if (busy) return), sets busy, clears prior error. 2. awaits the action. 3. On rejection, stays open and surfaces the error inline (see below), then clears busy. 4. On success, the caller closes the overlay.
Never close a destructive overlay before its action resolves, and never fire-and-forget.
Errors stay inline
- Surface failures inside the overlay with an inline DS V3 Alert (
AhaAlert type="error"— the banner component and its styling are owned byaha-design-feedback; do not drop in a bare AntD<Alert>). This pattern owns *where* the error goes (inline, in the overlay); feedback owns *what it looks like*. - Never stack a second modal on top, and never downgrade the error to a transient toast.
- The overlay remains open on failure so the user can retry without re-opening.
*Full detail, the DeleteSurveyConfirmModal / RespondentReportDrawer references, and the
OVERLAY-01..08 assertions live in the aha-design-overlays skill. Self-check any built overlay with aha-design-overlays-judge and fix every FAIL before shipping.*