# Modal

```tsx
import { Modal } from '@tangible/ui';
```

Interactive docs and examples: [Modal in Storybook](https://storybook-tangible-ui.pages.dev/?path=/docs/overlays-modal--docs).

## Modal props

| Prop | Type | Default | Description |
| - | - | - | - |
| `onClose` | `() => void` | | |
| `open` | `boolean` | | |
| `aria-describedby?` | `string` | | Identifies the element (or elements) that describes the object. |
| `aria-labelledby?` | `string` | | Identifies the element (or elements) that labels the current element. ID of the element that labels this dialog. Omitting this leaves the dialog without an accessible name, which violates WCAG 4.1.2 (Name, Role, Value). |
| `children?` | `React.ReactNode` | | |
| `className?` | `string` | | Additional class names applied to the dialog element (`.tui-modal__dialog`). Use for layout tweaks, custom widths, or context-specific theming. |
| `closeLabel?` | `string` | `'Close'` | |
| `closeOnBackdropClick?` | `boolean` | | Close when clicking the backdrop. Default `true` for `role="dialog"`, `false` for `role="alertdialog"` (see `role`). Passing it explicitly always wins. |
| `closeOnEscape?` | `boolean` | `true` | |
| `container?` | `Element \| null` | | |
| `initialFocusSelector?` | `string` | | |
| `preventScrollOnRestore?` | `boolean` | `false` | When true, prevents the browser from scrolling to the trigger element when focus is restored on close. Useful when the trigger may be off-screen inside a scrollable container. Default: false. |
| `restoreFocusTo?` | `React.RefObject<HTMLElement \| null> \| (() => HTMLElement \| null)` | | Where focus goes on close, read at close time. Defaults to whatever had focus when the modal opened. Set it when that element can be gone by then: a "Delete" in a list row whose confirm removes the row. Point it at the next row, the list heading or the "Add" button. A function suits a target that only exists by close time (the row after the deleted one). If neither this nor the captured element can take focus, focus falls to <body> and a dev warning names this prop (WCAG 2.4.3). |
| `role?` | `"alertdialog" \| "dialog"` | `'dialog'` | ARIA role of the dialog element. Default `'dialog'`. `'alertdialog'` is for a dialog that interrupts the user's workflow to demand a response — a destructive-action confirm is the textbook case. Focus trap, `aria-modal`, Escape and focus restore behave identically for both roles; what changes is the contract around them: the APG alert dialog pattern expects BOTH `aria-labelledby` (the heading) and `aria-describedby` (the message) — a dev warning fires if either is missing — and the backdrop stops dismissing on click unless `closeOnBackdropClick` says otherwise, since a stray click outside is not a response. Escape still closes: it is the keyboard "cancel". Initial focus lands on the first focusable element in DOM order, skipping the built-in close button (unless `initialFocusSelector` says otherwise), and the APG wants that to be the safest action — so put Cancel before the destructive action in `Modal.Foot`. If the visual order must differ (`Modal.Foot justify="between"` with a destructive action first), point `initialFocusSelector` at the safe one. <https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/> |
| `showCloseButton?` | `boolean` | | |
| `size?` | `"lg" \| "md" \| "sm" \| "xl"` | `'md'` | |
| `stickyFoot?` | `boolean` | | |
| `stickyHead?` | `boolean` | | |

Also accepts the standard attributes of `HTMLAttributes` (`data-*`, `aria-*`, event handlers), passed to the element it renders.

### Modal.Head

| Prop | Type | Default | Description |
| - | - | - | - |
| `children?` | `React.ReactNode` | | |
| `className?` | `string` | | |
| `icon?` | `React.ReactNode` | | Optional icon displayed to the left of the title/subtitle block. |
| `subtitle?` | `React.ReactNode` | | Optional subtitle displayed below the title in smaller, muted text. |

Also accepts the standard attributes of `HTMLAttributes` (`data-*`, `aria-*`, event handlers), passed to the element it renders.

### Modal.Body

| Prop | Type | Default | Description |
| - | - | - | - |
| `children?` | `React.ReactNode` | | |
| `className?` | `string` | | |
| `scrollLabel?` | `string` | | Accessible label for the scrollable region. Passed to the inner scroll container. When the body content overflows, screen readers announce this as the region name. Omitting this on a scrollable modal body leaves the scroll region unlabelled. |

Also accepts the standard attributes of `HTMLAttributes` (`data-*`, `aria-*`, event handlers), passed to the element it renders.

### Modal.Foot

| Prop | Type | Default | Description |
| - | - | - | - |
| `children?` | `React.ReactNode` | | |
| `className?` | `string` | | |
| `justify?` | `"between" \| "end" \| "start"` | `'end'` | How the actions distribute along the row. `'end'` (default) keeps them together at the inline end, so reading order is visual order and the primary action comes last; `'start'` keeps them together at the inline start; `'between'` separates the first action from the rest, which stay grouped at the end — the place for a destructive or tertiary action you want visibly apart from the primary. Visual order follows DOM order, and so does the dialog's initial focus: a destructive action placed first will be focused first unless the Modal's `initialFocusSelector` points at a safer one. Published as `data-justify` on `.tui-modal__foot`. |

Also accepts the standard attributes of `HTMLAttributes` (`data-*`, `aria-*`, event handlers), passed to the element it renders.

### Modal.Close

| Prop | Type | Default | Description |
| - | - | - | - |
| `className?` | `string` | | |
| `label?` | `string` | `'Close'` | |

## Examples

- [Basic](https://storybook-tangible-ui.pages.dev/?path=/story/overlays-modal--basic): Basic modal with head, body, and foot slots.

- [With Close Button](https://storybook-tangible-ui.pages.dev/?path=/story/overlays-modal--with-close-button): Modal with built-in close button via `showCloseButton` prop.

- [Manual Close Button](https://storybook-tangible-ui.pages.dev/?path=/story/overlays-modal--manual-close-button): Custom close button placement using `Modal.Close` compound component.

- [Sticky Header Footer](https://storybook-tangible-ui.pages.dev/?path=/story/overlays-modal--sticky-header-footer): Scrollable modal with sticky header and footer.

- [Sizes](https://storybook-tangible-ui.pages.dev/?path=/story/overlays-modal--sizes): Modal size variants: sm, md, lg.

- [Blocking](https://storybook-tangible-ui.pages.dev/?path=/story/overlays-modal--blocking): Blocking modal that requires explicit action to close.

- [Alert Dialog](https://storybook-tangible-ui.pages.dev/?path=/story/overlays-modal--alert-dialog): `role="alertdialog"` — a confirm that interrupts the user to approve a destructive action.
  Same focus trap, `aria-modal`, Escape and focus restore as `role="dialog"`; what changes is the
  contract: both `aria-labelledby` **and** `aria-describedby` are expected (a dev warning fires if
  either is missing), and the backdrop stops dismissing on click by default — a stray click outside
  is not a response. Escape still closes: it's the keyboard "cancel". Order the actions so the
  least destructive one is first, which is where initial focus lands.

  `role` is fixed to `alertdialog` here. Flipping the `role` control to `alertdialog` on any other
  story fires the dev warning instead, since none of them pass `aria-describedby` — that is the
  guard rail working, not a bug.

- [Foot Justify](https://storybook-tangible-ui.pages.dev/?path=/story/overlays-modal--foot-justify): `Modal.Foot` `justify`: `end` (default) gathers actions at the inline end, `start` at the inline
  start, `between` separates the first action from the rest, which stay grouped at the end. Opens on
  `between` because that is the one that looks different; close it to try the others. Visual order is
  DOM order — so is initial focus, which is why a destructive action placed first belongs behind an
  `initialFocusSelector` in an alert dialog.

- [With Subtitle](https://storybook-tangible-ui.pages.dev/?path=/story/overlays-modal--with-subtitle): Modal.Head with subtitle text below the title.

- [With Icon](https://storybook-tangible-ui.pages.dev/?path=/story/overlays-modal--with-icon): Modal.Head with a left-aligned icon.

- [With Icon And Subtitle](https://storybook-tangible-ui.pages.dev/?path=/story/overlays-modal--with-icon-and-subtitle): Modal.Head with both icon and subtitle.

- [Focus Trap Interaction](https://storybook-tangible-ui.pages.dev/?path=/story/overlays-modal--focus-trap-interaction): Focus moves into the modal on open and the focus trap cycles Tab through focusable elements.

- [Focus Restore Interaction](https://storybook-tangible-ui.pages.dev/?path=/story/overlays-modal--focus-restore-interaction): Focus restores to the trigger element when the modal is closed.

- [Inert Interaction](https://storybook-tangible-ui.pages.dev/?path=/story/overlays-modal--inert-interaction): Sibling content is inerted while the modal is open.

- [Scroll Lock Interaction](https://storybook-tangible-ui.pages.dev/?path=/story/overlays-modal--scroll-lock-interaction): Body scroll lock class is applied while modal is open.

- [Nested Modal Interaction](https://storybook-tangible-ui.pages.dev/?path=/story/overlays-modal--nested-modal-interaction): Nested modal replaces the first modal; closing restores the parent.

## Component API tokens

Read via fallback, never declared. Set one on an ancestor to restyle every instance under it.

- `--tui-modal-z`
- `--tui-modal-max`
- `--tui-modal-spacing`
- `--tui-modal-offset-top`
- `--tui-modal-offset-bottom`
- `--tui-modal-offset-start`
- `--tui-modal-offset-end`
- `--tui-modal-backdrop`
- `--tui-modal-bg`
- `--tui-modal-border`
- `--tui-modal-radius`
- `--tui-modal-padding`
- `--tui-modal-shadow`
- `--tui-modal-foot-gap`

---

Interactive version: https://storybook-tangible-ui.pages.dev/?path=/docs/overlays-modal--docs
