A modal takes the screen until the reader has answered it. Reach for it when there is a
decision with two outcomes, delete or keep, accept or decline, name or cancel, and not for
anything the reader should be able to keep working beside: details and filters belong in a
[Drawer](/components/overlays/drawer), which leaves the page in view. The dialog sits in the
middle of the viewport, as wide as its size allows, and the page behind it stays visible,
dimmed and still.

On the platform the same props present the platform's own modal, with the platform's way out
standing in for <kbd>Escape</kbd>. See [React Native](/platforms/native).

```tsx demo="overlays/modal-preview"
```

## Usage

```tsx
import { Button, Modal } from "@asheeui/web";
import { useState } from "react";

export function DeleteInvoice() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <Button onClick={() => setIsOpen(true)}>Delete invoice</Button>
      <Modal
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        size="sm"
        aria-labelledby="delete-title">
        <div className="p-6">
          <h2 id="delete-title">Delete invoice INV-1041?</h2>
          <p>The invoice is removed for everyone on the account.</p>
          <Button color="danger" onClick={() => setIsOpen(false)}>
            Delete invoice
          </Button>
        </div>
      </Modal>
    </>
  );
}
```

`children` is the whole surface: the component supplies the box, the backdrop, the focus trap and
its own close control, not a header, a body or a footer, and the content box has no padding of its
own. Padding and actions are yours, which is why every example here opens its content with a
padded stack. The close control sits in the top-right corner, is first in the tab order, and is
named by `closeLabel`, `"Close dialog"` unless you state otherwise.

There is no `title` prop. The dialog is named by the consumer, either with `aria-label` or
with `aria-labelledby` pointing at a heading inside it, the component supplies the role and
the trap, and cannot know what the dialog is about.

## Position and size

Two options decide the shape. `size` is a maximum width on the modal's own scale, `sm` 24rem,
`md` 32rem, `lg` 42rem, `xl` 56rem, and `full` the viewport less a two-rem gutter, and
`position` is where the dialog sits vertically: `center` for a decision, `top` for a short
message that should not cover the page, `bottom` where the platform's sheets live. The
horizontal axis is always centred, so `position` moves the dialog up and down only.

`width` and `height` are the escape hatches, and neither is the size scale in another unit.
`width` replaces the resolved size outright and takes any CSS width (`"500px"`, `"80%"`);
`height` states an inline height on the content box. The height is still capped at 90% of the
viewport, and the box scrolls inside that cap, so a long form never makes the dialog taller
than the screen. A stated `width` is still capped at the viewport, so a wide dialog shrinks on a
narrow screen instead of overflowing a page whose scroll is locked.

```tsx demo="overlays/modal-size"
```

`radius` rounds the corners and resolves from the application's `defaultRadius`, the
framework's own default is `xs`, so a dialog is boxy until the product says otherwise, and
falls back to `lg` only when the application states no default at all. A product that rounds
its buttons rounds its dialogs with them. `animated` (registered `true`) is what makes the box
and the backdrop appear and disappear; a closing dialog stays mounted for the 200 ms exit
before it leaves the DOM.

## Dismissal

Two switches, `closeOnBackdropClick` and `closeOnEscape`, both registered `true`. They are not
the same gesture. <kbd>Escape</kbd> is the reader asking to cancel; a backdrop press is a click
that missed as often as it is a decision, so a dialog that holds typed input usually turns the
first off and keeps the second.

The backdrop is `aria-hidden` and out of the tab order, so it carries no keyboard route:
turning `closeOnBackdropClick` off takes away a pointer gesture and nothing else, because the
close control the component draws stays, and so does <kbd>Escape</kbd>. <kbd>Escape</kbd> is
answered by the topmost layer through a shared stack, so a modal opened over a drawer closes only
itself, and it works wherever focus is inside the dialog.

Both switches call `onClose`. It is optional in the type, but a modal without one is presented
non-modally, no scroll lock, no focus trap, no `aria-modal`, and logs a warning, because a layer
that cannot close must not hold the page. Wire it to the state that controls `isOpen`.

```tsx demo="overlays/modal-dismissal"
```

## Props

```props Modal
```

The registered defaults are `size` `md`, `position` `center`, `animated` `true`, and both
dismissal switches `true`; state any of them once under `components.modal` and every modal in
the product follows. `radius` is deliberately not registered: it resolves from the
application's `defaultRadius`, which the framework states as `xs`, and takes the modal's own
`lg` only when no application default is present. Rounding is therefore a product decision
rather than a dialog's.

`width`, `height`, `isOpen`, `onClose`, `closeLabel`, `children`, `overlay` and `content` are
instance-only: how large one dialog is, what it says and what its close control is called are
statements about one screen, not about the design. `overlay.className` is merged with the
backdrop's classes and `content.className` with the content box's, so each adds or replaces
individual utilities and keeps the rest of the surface.

## Accessibility

- The content box is `role="dialog"` with `aria-modal="true"` and `tabIndex={-1}`, the
  `aria-modal` is omitted when there is no `onClose`, because the modal is then presented
  non-modally. The backdrop is decorative, so the reader meets the dialog once rather than a
  backdrop and then a dialog.
- The name is the consumer's. Pass `aria-label`, or `aria-labelledby` naming a heading inside
  the dialog; a dialog with no name is announced as "dialog" and nothing else.
- Focus moves to the dialog box when it opens, and is trapped inside it while it is open; a modal
  with no `onClose` has no trap, because it is not allowed to hold a page it cannot release.
  Closing returns focus to whatever held it before. The built-in close control is the first tab
  stop, so one <kbd>Tab</kbd> reaches the way out. <kbd>Escape</kbd> closes it while
  `closeOnEscape` is on, and only the topmost layer answers that press.
- Everything the reader needs belongs inside the dialog: while the trap is on, a control left
  behind it cannot be reached.

Focus, the scroll lock, the portal and the two dismissal switches are the
[shared overlay contract](/concepts/focus-and-dismissal); this page states only what the modal
adds to it.

## Related

```related drawer, tooltip, button
```
