# Modal
> Generated from modal.contract.json — do not edit by hand.

A focused, blocking dialog in two types — a Confirmation (yes/no decision) and an Action (a task surface) — rendered through the shared Modal.

Tier: **composite-antd**. Frameworks: HTML (paste-and-run, no build step) · React · Vue 3.

Surfaces: editor, dashboard, settings.

## Props
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `open` | boolean | `false` | Controls visibility |
| `title` | string | `—` | Dialog heading |
| `onOk` | () => void | `—` | Confirm handler |
| `onCancel` | () => void | `—` | Dismiss handler |
| `confirmLoading` | boolean | `false` | Spinner on the confirm button |
| `width` | string | `modalWidth('simple')` | Dialog width — pass modalWidth(size): min(target px, calc(100vw − 32px)) so it's the px width on desktop and near-full-width on mobile. Targets: simple 504 / complexity 720 / rich 1280 |
| `styles` | object | `modalStyles('simple')` | Height cap + body scroll for the size — modalStyles(size) |
| `footer` | ReactNode | `—` | DS footer: Learn-more link (left, a --aha-text-link anchor — external-link glyph only when it leaves AhaSlides) + Cancel + Apply (right) |
| `centered` | boolean | `false` | Vertically centre the dialog in the viewport |
| `closable` | boolean | `true` | Show the top-right close (X) |
| `mask` | boolean | `true` | Render the dimming overlay behind the dialog |

## Visual standard (measured)
- Content: white surface · radius 8
- Title: ink #1A1A1A · 18 · SemiBold
- Footer: brand primary confirm + secondary cancel
- Overlay: always-on mask (ink rgba(26,26,46,.7)) · portals to body · locks page scroll · click mask / Esc / ✕ to close (destructive: mask not closable)
- Motion: antd's built-in zoom enter/leave (kept)
- Types: Confirmation (status icon + copy) · Action (task surface)
- Confirmation: contexts: default · confirm · warning · info · danger (status icon left of title)
- Action sizes: simple 504 · complexity 720 · rich 1280 (px width) × height 75/80/90vh · divider on/off
- Viewport cap: width={modalWidth(size)} = min(px, calc(100vw − 32px)) — px on desktop, near-full-width on mobile; styles={modalStyles(size)} caps height, body scrolls past it

## When to use
- **Modal** — the user must confirm or complete a focused task before continuing
- **Drawer** — a longer edit form or detail panel that keeps page context
- **Popconfirm** — a lightweight yes/no on a single control

Two types. A CONFIRMATION modal is a focused yes/no — one of five contexts (default · confirm · warning · info · danger) puts a status icon left of the title, with a short line of copy, a Learn-more link, and Cancel + Apply; it's always `simple` size. An ACTION modal is a task surface (form, picker, editor) at one of three sizes — simple / complexity / rich — with an optional header/footer divider. Both are controlled `<Modal>`s, never the static Modal.confirm(). Every modal opens as a real overlay: it portals to `<body>` with an always-on mask, locks page scroll while open, and closes on a mask click / Esc / the ✕ — EXCEPT a destructive confirmation, which overrides `mask={{ closable: false }}` so a stray backdrop click can't trigger the action (Esc + ✕ only). Name the primary button the action ('Delete team', not 'OK'). A modal must never grow bigger than the screen in either axis: set `width={modalWidth(size)}` (resolves to `min(<target px>, calc(100vw − 32px))` — the px width on desktop, near-full-width on mobile with a 16px gutter; antd centres the dialog by its width, so the cap lives on the prop, not inner styles) and `styles={modalStyles(size)}` (height `auto` up to the cap, then the body scrolls while title + footer stay pinned). Anything bigger than `rich` belongs in its own page.
