Skip to content

Type to search the docs, API, and blog.

↑↓ navigate↵ openesc close
Zazz 0.7 alpha
API navigation

API

Dialog

Modal dialogs on the native `<dialog>`, opened by invoker commands.

Installation

pnpm dlx zazz-ui add dialog
npx zazz-ui add dialog
yarn dlx zazz-ui add dialog
bunx zazz-ui add dialog

Run zazz-ui init first if the project has no zazz/ folder yet.

Examples

Default

Dialog title

This dialog was opened using an invoker command.

Set the size of the dialog with the data-dialog-size attribute.

<button data-ui="button" type="button" command="show-modal" commandfor="dialog-example-1">
  Dialog
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
    <rect width="256" height="256" fill="none" />
    <line
      x1="64"
      y1="192"
      x2="192"
      y2="64"
      fill="none"
      stroke="currentColor"
      stroke-linecap="round"
      stroke-linejoin="round"
      stroke-width="16"
    />
    <polyline
      points="88 64 192 64 192 168"
      fill="none"
      stroke="currentColor"
      stroke-linecap="round"
      stroke-linejoin="round"
      stroke-width="16"
    />
  </svg>
</button>
<dialog id="dialog-example-1" data-ui="dialog" closedby="any">
  <div data-dialog-slot="content">
    <header data-dialog-slot="header">
      <h2
        style="
          --font-size: var(--font-size-lg);
          --font-weight: strong;
          --font-family: var(--font-family-heading);
        "
      >
        Dialog title
      </h2>
      <p style="--text: var(--color-muted-foreground)">
        This dialog was opened using an
        <a
          href="https://developer.mozilla.org/en-US/docs/Web/API/Invoker_Commands_API"
          target="_blank"
          style="--text: var(--color-primary)"
          >invoker command</a
        >.
      </p>
    </header>
    <div data-dialog-slot="body">
      <p>Set the size of the dialog with the <code>data-dialog-size</code> attribute.</p>
    </div>
    <footer data-dialog-slot="footer">
      <button data-ui="button" type="button" commandfor="dialog-example-1" command="close">
        Cancel
      </button>
      <button
        data-ui="button"
        data-button-variant="primary"
        commandfor="dialog-example-1"
        command="close"
      >
        Accept
      </button>
    </footer>
  </div>
  <button
    data-ui="button"
    data-dialog-slot="close"
    data-button-size="icon"
    data-button-variant="ghost"
    type="button"
    commandfor="dialog-example-1"
    command="close"
  >
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
      <rect width="256" height="256" fill="none" />
      <line
        x1="200"
        y1="56"
        x2="56"
        y2="200"
        stroke="currentColor"
        stroke-linecap="round"
        stroke-linejoin="round"
        stroke-width="16"
      />
      <line
        x1="200"
        y1="200"
        x2="56"
        y2="56"
        stroke="currentColor"
        stroke-linecap="round"
        stroke-linejoin="round"
        stroke-width="16"
      />
    </svg>
  </button>
</dialog>
/**
 * dialog.css — Dialog ([data-ui~="dialog"] on native <dialog>; slots
 *             data-dialog-slot="content | header | body | footer | close")
 *
 * @layer      variables, reset, components
 * @requires   _layers.css, _variables.css, _properties.css (--_*-resolved)
 * @uses       <dialog>, ::backdrop — native modal + backdrop
 * @uses       @starting-style + allow-discrete — open/close opacity, transform, overlay
 * @uses       anchor-name, container-type — layout hooks for sized variants
 * @tokens     --ui-dialog-* (@layer variables; --ui-dialog-h defaults to a keyword, so it is read
 *             raw: a --h length, or a tier carrying a length, applies; a scale
 *             number applies only as a base utility)
 * @presets    data-dialog-size="article | container | screen"
 * @consumedby alert-dialog.css, mobile-menu.css
 */

@layer variables {
  :root {
    --ui-dialog-backdrop-color: var(--color-shade-900);
    --ui-dialog-transition-duration: var(--default-transition-duration);
    --ui-dialog-transition-timing-function: var(--default-transition-timing-function);
    --ui-dialog-backdrop-transition-duration: calc(var(--ui-dialog-transition-duration) - 100ms);
    --ui-dialog-backdrop-transition-timing-function: ease;
    --ui-dialog-backdrop-filter: none;
    --ui-dialog-shadow: var(--shadow-lg);
    --ui-dialog-border: 1px solid var(--color-border);
    --ui-dialog-w: min(var(--layout-md), 100% - var(--gutters) * 2);

    /* Reveal transitions/animations */
    --ui-dialog-opacity--start: 0;
    --ui-dialog-opacity--end: 1;
    --ui-dialog-scale--start: 0.98;
    --ui-dialog-scale--end: 1;
    --ui-dialog-translate--start: 0, 0, 0;
    --ui-dialog-translate--end: 0, 0, 0;
    --ui-dialog-transition-delay--start: 0ms;
    --ui-dialog-transition-delay--end: 150ms;
  }
}

/* Second variables block: overrides --ui-dialog-w above. */
@layer variables {
  :root {
    --ui-dialog-w: min(50ch, 100% - var(--space-md) * 2);
    --ui-dialog-h: fit-content;
    --ui-dialog-bg: var(--color-card);
    --ui-dialog-text: var(--color-card-foreground);
    --ui-dialog-rounded: var(--radius-lg);
  }
}

@layer reset {
  /* ===========================================================================
  DIALOG — native <dialog> open/close animation, sizing, backdrop fade
  - In the reset layer so the dialog component styling below wins.
  =========================================================================== */

  /* Spring-eased open/close */
  :where(dialog) {
    margin: auto;
    max-inline-size: calc(100% - var(--space-md));
    max-block-size: calc(100% - var(--space-md));
    border: var(--ui-dialog-border);
    contain: layout style;
    opacity: var(--ui-dialog-opacity--start);
    box-shadow: var(--ui-dialog-shadow);
    overscroll-behavior: contain;
    overflow: auto;
    transform: translate3d(var(--ui-dialog-translate--start)) scale(var(--ui-dialog-scale--start));
    transition:
      opacity var(--ui-dialog-transition-duration) var(--ui-dialog-transition-timing-function),
      transform var(--ui-dialog-transition-duration) var(--ui-dialog-transition-timing-function),
      padding var(--ui-dialog-transition-duration) var(--ui-dialog-transition-timing-function),
      block-size var(--ui-dialog-transition-duration) var(--ui-dialog-transition-timing-function),
      inline-size var(--ui-dialog-transition-duration) var(--ui-dialog-transition-timing-function),
      overlay var(--ui-dialog-transition-duration) var(--ui-dialog-transition-timing-function)
        allow-discrete,
      display var(--ui-dialog-transition-duration) var(--ui-dialog-transition-timing-function)
        allow-discrete;
    transition-delay: var(--ui-dialog-transition-delay--start);
  }

  :where(dialog[open]) {
    transform: translate3d(var(--ui-dialog-translate--end)) scale(var(--ui-dialog-scale--end));
    opacity: var(--ui-dialog-opacity--end);
    transition-delay: var(--ui-dialog-transition-delay--end);

    @starting-style {
      transition-delay: var(--ui-dialog-transition-delay--start);
      transform: translate3d(var(--ui-dialog-translate--start)) scale(var(--ui-dialog-scale--start));
      opacity: var(--ui-dialog-opacity--start);
    }
  }

  /* Backdrop fades via a discrete transition */
  :where(dialog)::backdrop {
    position: fixed;
    inline-size: 100%;
    block-size: 100%;
    inset: 0;
    z-index: 1000;
    opacity: 0;
    overscroll-behavior: contain;
    overflow: auto;
    background-color: var(--ui-dialog-backdrop-color);
    backdrop-filter: var(--ui-dialog-backdrop-filter);
    transition: opacity var(--ui-dialog-backdrop-transition-duration)
      var(--ui-dialog-backdrop-transition-timing-function);
  }

  :where(dialog[open])::backdrop {
    opacity: 1;

    @starting-style {
      opacity: 0;
    }
  }
}

/* Size privates: written by data-dialog-size, non-inheriting. */
@property --_dialog-w {
  syntax: "*";
  inherits: false;
}

@property --_dialog-h {
  syntax: "*";
  inherits: false;
}

@property --_dialog-rounded {
  syntax: "*";
  inherits: false;
}

@property --_dialog-shadow {
  syntax: "*";
  inherits: false;
}

@layer ui.components {
  :where([data-ui~="dialog"]) {
    /* Set the hook on the element (not :root) so a later consumer (alert-dialog)
       or a subtree hook still wins; --_dialog-shadow is written only by the
       screen preset. */
    --ui-dialog-shadow: var(--shadow-md);

    --_w-len: var(--_w-resolved, var(--_dialog-w, var(--ui-dialog-w)));
    --_w-num: var(--_w-resolved, var(--_dialog-w, var(--ui-dialog-w)));
    inline-size: calc(var(--_w-len) + var(--_w-num) * var(--spacing));
    block-size: var(--_h-resolved, var(--_dialog-h, var(--ui-dialog-h)));
    background-color: var(--_bg-resolved, var(--ui-dialog-bg));
    color: var(--_text-resolved, var(--ui-dialog-text));
    box-shadow: var(--_shadow-resolved, var(--_dialog-shadow, var(--ui-dialog-shadow)));
    border-radius: var(--_rounded-resolved, var(--_dialog-rounded, var(--ui-dialog-rounded)));

    anchor-name: --ui-dialog-root;
    container-type: inline-size;
    container-name: dialog;
    font-size: var(--font-size-md);
    isolation: isolate;
  }

  :where([data-ui~="dialog"][open]) {
    grid-template-rows: minmax(0, 1fr);
    min-block-size: 0;
  }

  :where([data-ui~="dialog"]) [data-dialog-slot~="close"] {
    position: absolute;
    inset-block-start: var(--space-xs);
    inset-inline-end: var(--space-xs);
    z-index: 50;
  }

  :where([data-ui~="dialog"]) [data-dialog-slot~="close"] > svg {
    inline-size: calc(var(--spacing) * 4);
    block-size: calc(var(--spacing) * 4);
    pointer-events: none;
    justify-content: center;
    align-items: center;
    display: flex;
    padding: 0;
  }

  :where([data-ui~="dialog"]) [data-dialog-slot~="content"] {
    display: grid;
    align-self: center;
    grid-template-rows: auto minmax(0, 1fr) auto;
    position: relative;
    min-block-size: 0;
    overflow: clip;
  }

  :where([data-ui~="dialog"]) [data-dialog-slot~="header"] {
    display: flex;
    flex-direction: column;
    padding-inline: var(--space-sm);
    padding-block-start: var(--space-sm);
  }

  :where([data-ui~="dialog"]) [data-dialog-slot~="header"]::after {
    content: "";
    display: block;
    position: absolute;
    inset-block-start: 100%;
    inline-size: 100%;
    block-size: var(--space-md);
    background: linear-gradient(to top, transparent, var(--ui-dialog-bg));
  }

  :where([data-ui~="dialog"]) [data-dialog-slot~="body"] {
    display: grid;
    padding-inline: var(--space-sm);
    padding-block: var(--space-md);
    min-block-size: 0;
    min-block-size: var(--space-xl);
    overflow-x: clip;
    overflow-y: auto;
    scrollbar-gutter: stable both-edges;
  }

  :where([data-ui~="dialog"]) [data-dialog-slot~="footer"] {
    block-size: fit-content;
    position: sticky;
    inset-block-end: 0;
    display: flex;
    justify-content: flex-end;
    align-self: end;
    flex-shrink: 0;
    gap: var(--space-xs);
    padding: var(--space-sm);
    border-block-start: 1px solid var(--color-border);
    background-color: var(--ui-dialog-bg);
  }

  :where([data-ui~="dialog"][data-dialog-size="article"]) {
    --_dialog-w: var(--article-md);
  }

  :where([data-ui~="dialog"][data-dialog-size="container"]) {
    --_dialog-w: var(--layout-md);
  }

  :where([data-ui~="dialog"][data-dialog-size="screen"]) {
    --_dialog-w: 100vi;
    --_dialog-h: 100svh;
    --_dialog-rounded: 0;
    --_dialog-shadow: none;
    max-inline-size: 100vi;
    max-block-size: 100svh;
  }
}

With form

New project

Add a project to your workspace. You can change these settings later.

Contact
We'll only use this to send receipts. Please enter a valid email address.
Billing
<button data-ui="button" type="button" command="show-modal" commandfor="form-dialog-1">
  New project
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
    <rect width="256" height="256" fill="none" />
    <line
      x1="40"
      y1="128"
      x2="216"
      y2="128"
      fill="none"
      stroke="currentColor"
      stroke-linecap="round"
      stroke-linejoin="round"
      stroke-width="16"
    />
    <line
      x1="128"
      y1="40"
      x2="128"
      y2="216"
      fill="none"
      stroke="currentColor"
      stroke-linecap="round"
      stroke-linejoin="round"
      stroke-width="16"
    />
  </svg>
</button>
<dialog id="form-dialog-1" data-ui="dialog" closedby="any" data-dialog-size="article">
  <div data-dialog-slot="content">
    <header data-dialog-slot="header">
      <h2
        style="
          --font-size: var(--font-size-lg);
          --font-weight: strong;
          --font-family: var(--font-family-heading);
        "
      >
        New project
      </h2>
      <p style="--text: var(--color-muted-foreground)">
        Add a project to your workspace. You can change these settings later.
      </p>
    </header>
    <form
      id="f-new-project-1"
      style="
        --display: flex-col;
        --gap: 6;
      "
      data-dialog-slot="body"
      method="dialog"
    >
      <fieldset data-ui="field-group">
        <legend data-ui="sr-only">Contact</legend>
        <div
          style="
            --display: grid;
            --grid-cols: 2;
            --gap: 4;
          "
        >
          <div data-ui="field">
            <label data-field-slot="label" for="fd-name">Name</label>
            <input
              data-ui="input"
              id="fd-name"
              name="name"
              type="text"
              inputmode="text"
              placeholder="Ada Lovelace"
              required
            />
          </div>
          <div data-ui="field">
            <label data-field-slot="label" for="fd-email">Email</label>
            <input
              data-ui="input"
              id="fd-email"
              name="email"
              type="email"
              inputmode="email"
              required
              placeholder="you@example.com"
              autocomplete="email"
              aria-describedby="fd-email-hint"
            />
            <div data-field-slot="description">
              <span data-field-slot="hint" id="fd-email-hint"
                >We'll only use this to send receipts.</span
              >
              <span data-field-slot="error" role="alert">Please enter a valid email address.</span>
            </div>
          </div>
          <label data-ui="field" style="--col-span: 2">
            <span data-field-slot="label">Repository</span>
            <div data-ui="input-group">
              <input
                data-ui="input"
                type="search"
                name="repo"
                id="fd-repo"
                inputmode="search"
                placeholder="Search repositories…"
              />
              <span data-input-group-slot="addon">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
                  <rect width="256" height="256" fill="none" />
                  <circle
                    cx="112"
                    cy="112"
                    r="80"
                    fill="none"
                    stroke="currentColor"
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    stroke-width="16"
                  />
                  <line
                    x1="168.57"
                    y1="168.57"
                    x2="224"
                    y2="224"
                    fill="none"
                    stroke="currentColor"
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    stroke-width="16"
                  />
                </svg>
              </span>
            </div>
          </label>
        </div>
      </fieldset>

      <fieldset data-ui="field-group">
        <div data-ui="field">
          <label data-field-slot="label" for="fd-message">Description</label>
          <textarea
            data-ui="textarea"
            id="fd-message"
            name="description"
            inputmode="text"
            placeholder="What is this project for?"
          ></textarea>
        </div>
      </fieldset>

      <fieldset data-ui="radio-group">
        <legend>Billing</legend>
        <label data-ui="field" data-field-orientation="horizontal">
          <input data-ui="radio" type="radio" name="billing" value="monthly" />
          <span data-field-slot="label">Monthly billing</span>
        </label>
        <label data-ui="field" data-field-orientation="horizontal">
          <input data-ui="radio" type="radio" name="billing" value="annual" checked />
          <span data-field-slot="label">Annual billing</span>
        </label>
      </fieldset>

      <label data-ui="field" data-field-orientation="horizontal">
        <input type="checkbox" name="terms" required />
        <span data-field-slot="label">Accept terms &amp; conditions</span>
      </label>
    </form>
    <footer data-dialog-slot="footer">
      <button
        data-ui="button"
        type="button"
        value="cancel"
        commandfor="form-dialog-1"
        command="close"
      >
        Cancel
      </button>
      <button
        data-ui="button"
        data-button-variant="primary"
        type="submit"
        form="f-new-project-1"
        value="save"
      >
        Create project
      </button>
    </footer>
    <button
      data-ui="button"
      data-dialog-slot="close"
      data-button-size="icon"
      data-button-variant="ghost"
      type="button"
      commandfor="form-dialog-1"
      command="close"
      aria-label="Close dialog"
    >
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
        <rect width="256" height="256" fill="none" />
        <line
          x1="200"
          y1="56"
          x2="56"
          y2="200"
          stroke="currentColor"
          stroke-linecap="round"
          stroke-linejoin="round"
          stroke-width="16"
        />
        <line
          x1="200"
          y1="200"
          x2="56"
          y2="56"
          stroke="currentColor"
          stroke-linecap="round"
          stroke-linejoin="round"
          stroke-width="16"
        />
      </svg>
    </button>
  </div>
</dialog>
/**
 * dialog.css — Dialog ([data-ui~="dialog"] on native <dialog>; slots
 *             data-dialog-slot="content | header | body | footer | close")
 *
 * @layer      variables, reset, components
 * @requires   _layers.css, _variables.css, _properties.css (--_*-resolved)
 * @uses       <dialog>, ::backdrop — native modal + backdrop
 * @uses       @starting-style + allow-discrete — open/close opacity, transform, overlay
 * @uses       anchor-name, container-type — layout hooks for sized variants
 * @tokens     --ui-dialog-* (@layer variables; --ui-dialog-h defaults to a keyword, so it is read
 *             raw: a --h length, or a tier carrying a length, applies; a scale
 *             number applies only as a base utility)
 * @presets    data-dialog-size="article | container | screen"
 * @consumedby alert-dialog.css, mobile-menu.css
 */

@layer variables {
  :root {
    --ui-dialog-backdrop-color: var(--color-shade-900);
    --ui-dialog-transition-duration: var(--default-transition-duration);
    --ui-dialog-transition-timing-function: var(--default-transition-timing-function);
    --ui-dialog-backdrop-transition-duration: calc(var(--ui-dialog-transition-duration) - 100ms);
    --ui-dialog-backdrop-transition-timing-function: ease;
    --ui-dialog-backdrop-filter: none;
    --ui-dialog-shadow: var(--shadow-lg);
    --ui-dialog-border: 1px solid var(--color-border);
    --ui-dialog-w: min(var(--layout-md), 100% - var(--gutters) * 2);

    /* Reveal transitions/animations */
    --ui-dialog-opacity--start: 0;
    --ui-dialog-opacity--end: 1;
    --ui-dialog-scale--start: 0.98;
    --ui-dialog-scale--end: 1;
    --ui-dialog-translate--start: 0, 0, 0;
    --ui-dialog-translate--end: 0, 0, 0;
    --ui-dialog-transition-delay--start: 0ms;
    --ui-dialog-transition-delay--end: 150ms;
  }
}

/* Second variables block: overrides --ui-dialog-w above. */
@layer variables {
  :root {
    --ui-dialog-w: min(50ch, 100% - var(--space-md) * 2);
    --ui-dialog-h: fit-content;
    --ui-dialog-bg: var(--color-card);
    --ui-dialog-text: var(--color-card-foreground);
    --ui-dialog-rounded: var(--radius-lg);
  }
}

@layer reset {
  /* ===========================================================================
  DIALOG — native <dialog> open/close animation, sizing, backdrop fade
  - In the reset layer so the dialog component styling below wins.
  =========================================================================== */

  /* Spring-eased open/close */
  :where(dialog) {
    margin: auto;
    max-inline-size: calc(100% - var(--space-md));
    max-block-size: calc(100% - var(--space-md));
    border: var(--ui-dialog-border);
    contain: layout style;
    opacity: var(--ui-dialog-opacity--start);
    box-shadow: var(--ui-dialog-shadow);
    overscroll-behavior: contain;
    overflow: auto;
    transform: translate3d(var(--ui-dialog-translate--start)) scale(var(--ui-dialog-scale--start));
    transition:
      opacity var(--ui-dialog-transition-duration) var(--ui-dialog-transition-timing-function),
      transform var(--ui-dialog-transition-duration) var(--ui-dialog-transition-timing-function),
      padding var(--ui-dialog-transition-duration) var(--ui-dialog-transition-timing-function),
      block-size var(--ui-dialog-transition-duration) var(--ui-dialog-transition-timing-function),
      inline-size var(--ui-dialog-transition-duration) var(--ui-dialog-transition-timing-function),
      overlay var(--ui-dialog-transition-duration) var(--ui-dialog-transition-timing-function)
        allow-discrete,
      display var(--ui-dialog-transition-duration) var(--ui-dialog-transition-timing-function)
        allow-discrete;
    transition-delay: var(--ui-dialog-transition-delay--start);
  }

  :where(dialog[open]) {
    transform: translate3d(var(--ui-dialog-translate--end)) scale(var(--ui-dialog-scale--end));
    opacity: var(--ui-dialog-opacity--end);
    transition-delay: var(--ui-dialog-transition-delay--end);

    @starting-style {
      transition-delay: var(--ui-dialog-transition-delay--start);
      transform: translate3d(var(--ui-dialog-translate--start)) scale(var(--ui-dialog-scale--start));
      opacity: var(--ui-dialog-opacity--start);
    }
  }

  /* Backdrop fades via a discrete transition */
  :where(dialog)::backdrop {
    position: fixed;
    inline-size: 100%;
    block-size: 100%;
    inset: 0;
    z-index: 1000;
    opacity: 0;
    overscroll-behavior: contain;
    overflow: auto;
    background-color: var(--ui-dialog-backdrop-color);
    backdrop-filter: var(--ui-dialog-backdrop-filter);
    transition: opacity var(--ui-dialog-backdrop-transition-duration)
      var(--ui-dialog-backdrop-transition-timing-function);
  }

  :where(dialog[open])::backdrop {
    opacity: 1;

    @starting-style {
      opacity: 0;
    }
  }
}

/* Size privates: written by data-dialog-size, non-inheriting. */
@property --_dialog-w {
  syntax: "*";
  inherits: false;
}

@property --_dialog-h {
  syntax: "*";
  inherits: false;
}

@property --_dialog-rounded {
  syntax: "*";
  inherits: false;
}

@property --_dialog-shadow {
  syntax: "*";
  inherits: false;
}

@layer ui.components {
  :where([data-ui~="dialog"]) {
    /* Set the hook on the element (not :root) so a later consumer (alert-dialog)
       or a subtree hook still wins; --_dialog-shadow is written only by the
       screen preset. */
    --ui-dialog-shadow: var(--shadow-md);

    --_w-len: var(--_w-resolved, var(--_dialog-w, var(--ui-dialog-w)));
    --_w-num: var(--_w-resolved, var(--_dialog-w, var(--ui-dialog-w)));
    inline-size: calc(var(--_w-len) + var(--_w-num) * var(--spacing));
    block-size: var(--_h-resolved, var(--_dialog-h, var(--ui-dialog-h)));
    background-color: var(--_bg-resolved, var(--ui-dialog-bg));
    color: var(--_text-resolved, var(--ui-dialog-text));
    box-shadow: var(--_shadow-resolved, var(--_dialog-shadow, var(--ui-dialog-shadow)));
    border-radius: var(--_rounded-resolved, var(--_dialog-rounded, var(--ui-dialog-rounded)));

    anchor-name: --ui-dialog-root;
    container-type: inline-size;
    container-name: dialog;
    font-size: var(--font-size-md);
    isolation: isolate;
  }

  :where([data-ui~="dialog"][open]) {
    grid-template-rows: minmax(0, 1fr);
    min-block-size: 0;
  }

  :where([data-ui~="dialog"]) [data-dialog-slot~="close"] {
    position: absolute;
    inset-block-start: var(--space-xs);
    inset-inline-end: var(--space-xs);
    z-index: 50;
  }

  :where([data-ui~="dialog"]) [data-dialog-slot~="close"] > svg {
    inline-size: calc(var(--spacing) * 4);
    block-size: calc(var(--spacing) * 4);
    pointer-events: none;
    justify-content: center;
    align-items: center;
    display: flex;
    padding: 0;
  }

  :where([data-ui~="dialog"]) [data-dialog-slot~="content"] {
    display: grid;
    align-self: center;
    grid-template-rows: auto minmax(0, 1fr) auto;
    position: relative;
    min-block-size: 0;
    overflow: clip;
  }

  :where([data-ui~="dialog"]) [data-dialog-slot~="header"] {
    display: flex;
    flex-direction: column;
    padding-inline: var(--space-sm);
    padding-block-start: var(--space-sm);
  }

  :where([data-ui~="dialog"]) [data-dialog-slot~="header"]::after {
    content: "";
    display: block;
    position: absolute;
    inset-block-start: 100%;
    inline-size: 100%;
    block-size: var(--space-md);
    background: linear-gradient(to top, transparent, var(--ui-dialog-bg));
  }

  :where([data-ui~="dialog"]) [data-dialog-slot~="body"] {
    display: grid;
    padding-inline: var(--space-sm);
    padding-block: var(--space-md);
    min-block-size: 0;
    min-block-size: var(--space-xl);
    overflow-x: clip;
    overflow-y: auto;
    scrollbar-gutter: stable both-edges;
  }

  :where([data-ui~="dialog"]) [data-dialog-slot~="footer"] {
    block-size: fit-content;
    position: sticky;
    inset-block-end: 0;
    display: flex;
    justify-content: flex-end;
    align-self: end;
    flex-shrink: 0;
    gap: var(--space-xs);
    padding: var(--space-sm);
    border-block-start: 1px solid var(--color-border);
    background-color: var(--ui-dialog-bg);
  }

  :where([data-ui~="dialog"][data-dialog-size="article"]) {
    --_dialog-w: var(--article-md);
  }

  :where([data-ui~="dialog"][data-dialog-size="container"]) {
    --_dialog-w: var(--layout-md);
  }

  :where([data-ui~="dialog"][data-dialog-size="screen"]) {
    --_dialog-w: 100vi;
    --_dialog-h: 100svh;
    --_dialog-rounded: 0;
    --_dialog-shadow: none;
    max-inline-size: 100vi;
    max-block-size: 100svh;
  }
}

Light dismiss

closedby="any" on the <dialog> lets a click on the backdrop close it, next to Esc. Chrome 134 and Firefox 141 do this natively. Safari 26 knows closedby but only its none and closerequest values, so any falls back to closerequest and the backdrop does nothing. The kit's dialog lifecycle script fills that gap: where any does not round-trip through the closedBy property, a press and release on the backdrop of a closedby="any" dialog calls requestClose(), which fires cancel as the native path does. Nothing in the markup changes, and the fallback stays idle where the browser handles it. A dialog that holds a form or a search still deserves a visible close button, for the people who find neither the backdrop nor Esc. closedby="none" (alert dialogs) and the default are left alone in both cases.

Attributes

dialog attributes
AttributeKindValuesNotes
data-ui="dialog"Identityfree valueIdentity token in data-ui.
data-dialog-sizePresetarticle container screenPreset: a named bundle of values.
data-dialog-slotSlotbody close content footer headerSlot: a part inside the primitive.

Hooks

Set a hook on :root, on a subtree, or inline on one element. A state hook (--…--hover) themes that state only; the primitive keeps its other states.

dialog hooks
HookDefaultState
--ui-dialog-backdrop-colorvar(--color-shade-900)—
--ui-dialog-backdrop-filternone—
--ui-dialog-backdrop-transition-durationcalc(var(--ui-dialog-transition-duration) - 100ms)—
--ui-dialog-backdrop-transition-timing-functionease—
--ui-dialog-bgvar(--color-card)—
--ui-dialog-border1px solid var(--color-border)—
--ui-dialog-hfit-content—
--ui-dialog-opacity--end1end
--ui-dialog-opacity--start0start
--ui-dialog-roundedvar(--radius-lg)—
--ui-dialog-scale--end1end
--ui-dialog-scale--start0.98start
--ui-dialog-shadowvar(--ui-alert-dialog-shadow)—
--ui-dialog-textvar(--color-card-foreground)—
--ui-dialog-transition-delay--end0msend
--ui-dialog-transition-delay--start0msstart
--ui-dialog-transition-durationvar(--default-transition-duration)—
--ui-dialog-transition-timing-functionvar(--default-transition-timing-function)—
--ui-dialog-translate--end0, 0, 0end
--ui-dialog-translate--start0, 0, 0start
--ui-dialog-wvar(--ui-alert-dialog-w)—