# Modal

A centered dialog over a scrim for a short interruption: an icon and title, a body, and a footer of actions.

Block: `smbk-modal`. Status: experimental. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `size` | `sm`, `md`, `lg` | `sm` | The greatest width the panel takes. |
| `inline` | on / off | off | Shown in place with no scrim, for previews and documentation. |

Parts: `smbk-modal__header`, `smbk-modal__icon`, `smbk-modal__title`, `smbk-modal__close`, `smbk-modal__body`, `smbk-modal__footer`

## Markup

```html
<dialog
  open=""
  class="smbk-modal smbk-modal--size-sm smbk-modal--inline"
  aria-labelledby="modal-1-title"
>
  <div class="smbk-modal__header">
    <i
      class="ph ph-paper-plane-tilt smbk-icon smbk-icon--size-md smbk-modal__icon"
      aria-hidden="true"
    ></i>
    <h2 id="modal-1-title" class="smbk-modal__title">Publish changes?</h2>
    <button
      type="button"
      class="smbk-btn smbk-btn--tone-ghost smbk-btn--size-md smbk-btn--icon-only smbk-modal__close"
      aria-label="Close"
    >
      <i class="ph ph-x smbk-icon smbk-icon--size-md smbk-btn__icon" aria-hidden="true"></i>
    </button>
  </div>
  <div
    class="smbk-modal__body"
    data-scrollbar=""
  >Your changes to Website redesign go live for everyone on the team. You can roll back from the history at any time.</div>
  <hr class="smbk-divider smbk-divider--orientation-horizontal"/>
  <div class="smbk-modal__footer">
    <button type="button" class="smbk-btn smbk-btn--tone-outline smbk-btn--size-md">
      <span class="smbk-btn__label">Cancel</span>
    </button>
    <button type="button" class="smbk-btn smbk-btn--tone-brand smbk-btn--size-md">
      <span class="smbk-btn__label">Publish</span>
    </button>
  </div>
</dialog>
```

## React

```tsx
import { Modal } from './design-system/components/modal/modal.js';
import { Button } from './design-system/components/button/button.js';

<Modal size="sm" inline open icon="PaperPlaneTilt" title="Publish changes?" actions={<Button tone="outline">Cancel</Button>
<Button tone="brand">Publish</Button>}>Your changes to Website redesign go live for everyone on the team. You can roll back from the history at any time.</Modal>
```

## Stylesheet

```css
/* smbk-modal — Modal. Figma "Modal" 4212:26125 (Size: sm, md, lg).
 * A centered dialog over a scrim, built on the native dialog element. The three
 * widths are the modal size tokens, carried from Figma's frames, used as
 * maximums so the panel shrinks with the window. The sections are spaced with
 * the extra-large stack step, as Figma binds them.
 * The inline flag shows the panel in place, for previews and documentation.
 * The panel fades and rises into place over a fading scrim (motion rules at the end). */

.smbk-modal {
  box-sizing: border-box;
  position: fixed;
  inset: 0;
  z-index: var(--smbk-layer-modal);
  display: none;
  flex-direction: column;
  gap: var(--smbk-space-stack-xl);
  width: 100%;
  max-width: var(--smbk-size-modal-sm);
  max-height: 100%;
  margin: auto;
  padding: var(--smbk-space-inset-2xl);
  border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-rim);
  border-radius: var(--smbk-radius-container-lg);
  background: var(--smbk-color-surface-default);
  color: var(--smbk-color-text-default);
  box-shadow: var(--smbk-elevation-overlay);
}

.smbk-modal[open] {
  display: flex;
}

/* The icon and the title on one line, as Figma draws them. The title keeps
   clear of the close button, which overlaps the row's end (see below). */
.smbk-modal__header {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--smbk-space-inline-lg);
  padding-inline-end: calc(var(--smbk-size-control-md) - var(--smbk-space-inset-2xl) + var(--smbk-space-inset-lg));
}

.smbk-modal__icon {
  flex-shrink: 0;
  font-size: var(--smbk-size-icon-lg);
}

/* The alert dialog (alert-dialog.css, drawn by this stylesheet) warns in the danger color while its confirm action is
   the danger one, as its Figma drawing has it; with a brand confirm action its icon keeps the modal's color. */
.smbk-alert-dialog:has(.smbk-btn--tone-danger) .smbk-modal__icon {
  color: var(--smbk-color-feedback-danger-icon);
}

.smbk-modal__title {
  margin: 0;
  font-family: var(--smbk-typography-heading-2xs-bold-font-family);
  font-size: var(--smbk-typography-heading-2xs-bold-font-size);
  font-weight: var(--smbk-typography-heading-2xs-bold-font-weight);
  line-height: var(--smbk-typography-heading-2xs-bold-line-height);
  letter-spacing: var(--smbk-typography-heading-2xs-bold-letter-spacing);
}

/* The close button sits in the modal's corner, the large inset from its top and end edges, over the
   padding, as Figma places it: the header sits the modal's own padding (and its hairline
   edge) in from those edges, so the button is moved back out by the difference. */
.smbk-modal__close {
  position: absolute;
  top: calc(var(--smbk-space-inset-lg) - var(--smbk-space-inset-2xl) - var(--smbk-border-width-hairline));
  inset-inline-end: calc(var(--smbk-space-inset-lg) - var(--smbk-space-inset-2xl) - var(--smbk-border-width-hairline));
}

.smbk-modal__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  /* The system's scrollbar: thin, in the subtle border color, on a clear track. */
  scrollbar-width: thin;
  scrollbar-color: var(--smbk-color-border-subtle) transparent;
  color: var(--smbk-color-text-subtle);
  font-family: var(--smbk-typography-body-sm-font-family);
  font-size: var(--smbk-typography-body-sm-font-size);
  font-weight: var(--smbk-typography-body-sm-font-weight);
  line-height: var(--smbk-typography-body-sm-line-height);
  letter-spacing: var(--smbk-typography-body-sm-letter-spacing);
}

/* The first thing in it, after the scrollbar the script adds (scrollbar.ts). */
.smbk-modal__body > :first-child,
.smbk-modal__body > .smbk-scrollbar:first-child + * {
  margin-top: 0;
}

.smbk-modal__body > :last-child {
  margin-bottom: 0;
}

.smbk-modal__footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--smbk-space-inline-lg);
}

/* ---- sizes ---- */

.smbk-modal--size-md {
  max-width: var(--smbk-size-modal-md);
}

.smbk-modal--size-lg {
  max-width: var(--smbk-size-modal-lg);
}

/* ---- inline: shown in place, no scrim ---- */

.smbk-modal--inline {
  position: relative;
  inset: auto;
  z-index: auto;
  margin: 0;
}

/* ---- over the page: the scrim, the motion (the panel fades in and rises a spacing step into place over a scrim
   that fades in, and fades out dropping a smaller step; under reduced motion it fades in place) and the page held
   still behind it are the dialogs' own, shared with the drawer and the command palette: dialog/dialog.css. Inline
   modals do not move. ---- */
```

## Tokens it reads

```json
{
  "color": {
    "overlay": {
      "scrim": {
        "$type": "color",
        "$value": "{{color.alpha.black.50}}",
        "$description": "Dimmed backdrop behind dialogs and drawers."
      }
    },
    "border": {
      "rim": {
        "$type": "color",
        "$value": "{{color.alpha.white.90}}",
        "$description": "The light rim that makes a surface look lit: fields, chosen pills, cards."
      },
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.300}}",
        "$description": "Dividers and quiet edges."
      }
    },
    "surface": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.100}}",
        "$description": "Standard surface: cards, panels, the header."
      }
    },
    "text": {
      "default": {
        "$type": "color",
        "$value": "{{color.grey.900}}",
        "$description": "Main text."
      },
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Secondary text: descriptions, metadata."
      }
    },
    "feedback": {
      "danger": {
        "icon": {
          "$type": "color",
          "$value": "{{color.red.600}}",
          "$description": "The danger tone’s own icon (an alert sign), so the tone is never shown by colour alone."
        }
      }
    }
  },
  "motion": {
    "exit": {
      "duration": {
        "$type": "duration",
        "$value": "{{duration.200}}"
      },
      "easing": {
        "$type": "cubicBezier",
        "$value": "{{easing.expo-in}}"
      }
    },
    "enter": {
      "duration": {
        "$type": "duration",
        "$value": "{{duration.320}}"
      },
      "easing": {
        "$type": "cubicBezier",
        "$value": "{{easing.expo-out}}"
      }
    }
  },
  "space": {
    "8": {
      "$type": "dimension",
      "$value": "8px",
      "$description": "Raw space step, 8 px. Only for fixed distances inside a component; spacing that follows density uses space/inset, stack or inline."
    },
    "16": {
      "$type": "dimension",
      "$value": "16px",
      "$description": "Raw space step, 16 px. Only for fixed distances inside a component; spacing that follows density uses space/inset, stack or inline."
    },
    "stack": {
      "xl": {
        "$type": "dimension",
        "$value": "{{space.24}}",
        "$description": "Gap between things stacked one above another, extra large."
      }
    },
    "inset": {
      "2xl": {
        "$type": "dimension",
        "$value": "{{space.24}}",
        "$description": "Padding inside a component, 2× large."
      },
      "lg": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Padding inside a component, large."
      }
    },
    "inline": {
      "lg": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Gap between things side by side, large."
      }
    }
  },
  "layer": {
    "modal": {
      "$type": "number",
      "$value": 1300
    }
  },
  "size": {
    "modal": {
      "sm": {
        "$type": "dimension",
        "$value": "400px"
      },
      "md": {
        "$type": "dimension",
        "$value": "560px"
      },
      "lg": {
        "$type": "dimension",
        "$value": "800px"
      }
    },
    "control": {
      "md": {
        "$type": "dimension",
        "$value": "{{size.40}}",
        "$description": "Height of a medium control, where controls insert by default: 40 comfortable, 32 compact. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      }
    },
    "icon": {
      "lg": {
        "$type": "dimension",
        "$value": "{{size.24}}",
        "$description": "Icon box, large."
      }
    }
  },
  "border-width": {
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    }
  },
  "radius": {
    "container": {
      "lg": {
        "$type": "dimension",
        "$value": "{{radius.16}}",
        "$description": "Corners of cards, panels and dialogs, large."
      }
    }
  },
  "elevation": {
    "overlay": {
      "$type": "shadow",
      "$value": {
        "light": "0px 12px 16px 2px #373d431a, 0px 2px 8px 0px #373d4326",
        "dark": "0px 12px 16px 2px #00000026, 0px 2px 8px 0px #00000033"
      }
    }
  },
  "typography": {
    "heading-2xs-bold": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Funnel Display, Funnel Display Fallback, system-ui, sans-serif",
        "fontSize": {
          "min": "18px",
          "max": "24px"
        },
        "fontWeight": 700,
        "lineHeight": 1.2,
        "letterSpacing": "0px"
      },
      "$description": "Heading 2XS in bold: drawer and modal titles."
    },
    "body-sm": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    }
  }
}
```

## Purpose

Stops the reader for one short thing: a confirmation, a small form, a choice that must be made before going on. The page behind is dimmed and cannot be used until the modal closes. A drawer is for longer tasks beside the page; a modal is for a moment in the middle of it.

## Anatomy

- The block, a native dialog element centered over a scrim.
- The header part: an optional icon part and the title part on one line, and the close part in the modal's top corner, the large inset from its top and end edges, overlapping the end of that line; the title keeps clear of it.
- The body part, which scrolls when tall.
- A divider and the footer part, optional, holding the actions aligned to the end.

## When to use

- Confirming something that cannot be undone.
- A form of two or three fields that belongs to the page behind.
- A choice the reader must make before the page can continue.

## When not to use

- Anything the reader may want to read the page during. Use a [drawer](drawer.md).
- Success messages. Use a [toast](toast.md).
- Long content. Use a page.

## Behavior

Opens modally. Escape, the close button and a click on the scrim all ask to close: a modal that holds its own open state closes itself, and one the page holds waits for the page to close it. A click anywhere on the panel, its padding and edge included, never asks, nor does a press that starts on the panel and ends on the scrim, as when selecting words. With a popover, a dropdown or a list open inside the modal, Escape and a click on the scrim close that first, and the modal stays. The size axis sets the greatest width; the panel shrinks with the window. It fades in and rises a spacing step into place over a scrim that fades in, and fades out a little faster as it closes; under reduced motion it still fades in and out, in place, without rising or dropping. Inline modals do not move.

The inline flag renders the panel in place with no scrim, so it can be seen in the Showroom and in documentation.

The React wrapper opens and closes it with the dialog's own modal open method, as other layers open: given `open`, the page holds the open state; without it, the modal holds its own, closed unless given `defaultOpen`. Asking to close calls `onOpenChange(false)`. Do not also set the dialog's open attribute.

## Accessibility

### What it does for you

- It is a native dialog opened modally: focus moves inside, is held there, and returns to the opener on close.
- It is labeled by its title, a second-level heading unless `headingLevel` gives another, for a modal opened from deeper in the page's outline.
- The close button is icon-only and named "Close". Every word it says by itself, such as the close button's name, keeps its English unless the page gives its own, so a page in another language can change it.
- Escape, the close button and a click on the scrim ask to close; the page decides by handling the request.

### What you must do

- Give the modal a title, and set `headingLevel` where the second level does not fit the page's outline.
- If the close button is hidden, put an action in the footer that closes the modal.
- Do not open a modal without the reader having done something.
- Give the close button's name in the page's own language when the page is not in English.

### How to test

- Press the control that opens it: focus moves into the modal. Tab and Shift + Tab stay inside it, and Escape asks to close it. After it closes, focus is back on the control that opened it.
- With a screen reader, it is announced as a dialog by its title, and the close button is read as "Close".
- With text at 200% and the page 320 pixels wide, the panel shrinks with the window and its content stays reachable.
- In a forced-color mode, the focus ring on its buttons and the panel's edge stay visible.

### Keyboard

| Key | What it does |
|---|---|
| Tab or Shift + Tab | Moves focus between the controls inside the modal and holds it there. |
| Escape | Asks to close the modal. Inside it, an open popover, dropdown or list closes first. |
| Enter or Space | On the close button or a footer button, presses it. |

## Usage rules

### Do

- Name the primary action after what it does: "Delete", not "OK".
- Put the primary action last and give it the brand tone, or the danger tone for a destructive one.

### Don't

- Stack modals.
- Use a modal to show a picture. Use the page.
