Design system

Dialog

Modal surface for decisions and forms.

Dialog Examples

Copy-ready examples with live previews.

Basic usage

Externally controlled dialog with text, close button, and actions footer.

Live preview

Basic dialog

import { useState } from "react";
import Dialog from "@/packages/ui/src/_components/dialog/Dialog";
import { Title } from "@/packages/ui/src/_components/text/title/Title";
import { Paragraph } from "@/packages/ui/src/_components/text/paragraph/Paragraph";
import { Button } from "@/packages/ui/src/_components/buttons_variants/buttons/Button";
import Icon from "@/packages/ui/src/_components/icon/Icon";

function SafePress(props: { onPress: () => void; children: React.ReactNode }) {
  return (
    <span
      style={{ display: "inline-flex" }}
      onClickCapture={(e) => {
        e.preventDefault();
        e.stopPropagation();
        props.onPress();
      }}
    >
      {props.children}
    </span>
  );
}

export default function Example() {
  const [open, setOpen] = useState(false);

  return (
    <>
      {open ? null : (
        <Button variant="primary" ripple outline onClick={() => setOpen(true)}>
          Open dialog
        </Button>
      )}

      <Dialog
        open={open}
        variant="basic"
        size="md"
        headerVariant="basic"
        footerVariant="actions"
        closeBehavior="button"
        showCloseButton
        closeButton={
          <SafePress onPress={() => setOpen(false)}>
            <Button variant="transparent" variantRadio="sm" ripple ariaLabel="Close dialog">
              <Icon name="Close" variant="black" />
            </Button>
          </SafePress>
        }
        title={<Title size="h4">Basic dialog</Title>}
        actionsContent={
          <>
            <SafePress onPress={() => setOpen(false)}>
              <Button variant="secondary" ripple outline>Cancel</Button>
            </SafePress>
            <SafePress onPress={() => setOpen(false)}>
              <Button variant="primary" ripple outline>Confirm</Button>
            </SafePress>
          </>
        }
      >
        <Paragraph size="medium">Dialog is layout-only. Closing is controlled by your app.</Paragraph>
      </Dialog>
    </>
  );
}

Scroll + divider

Use divider and a long body to validate overflow behavior.

Live preview

Terms dialog

import { useMemo, useState } from "react";
import Dialog from "@/packages/ui/src/_components/dialog/Dialog";
import { Title } from "@/packages/ui/src/_components/text/title/Title";
import { Paragraph } from "@/packages/ui/src/_components/text/paragraph/Paragraph";
import { Button } from "@/packages/ui/src/_components/buttons_variants/buttons/Button";
import Icon from "@/packages/ui/src/_components/icon/Icon";

function SafePress(props: { onPress: () => void; children: React.ReactNode }) {
  return (
    <span
      style={{ display: "inline-flex" }}
      onClickCapture={(e) => {
        e.preventDefault();
        e.stopPropagation();
        props.onPress();
      }}
    >
      {props.children}
    </span>
  );
}

export default function Example() {
  const [open, setOpen] = useState(false);

  const paragraphs = useMemo(
    () => Array.from({ length: 18 }).map((_, i) => (
      <Paragraph key={i} size="medium">Paragraph {i + 1}: filler text to validate scrolling.</Paragraph>
    )),
    []
  );

  return (
    <>
      {open ? null : (
        <Button variant="tertiary" ripple outline onClick={() => setOpen(true)}>
          Open (scroll + divider)
        </Button>
      )}

      <Dialog
        open={open}
        variant="basic"
        size="md"
        divider
        headerVariant="basic"
        footerVariant="actions"
        closeBehavior="both"
        showCloseButton
        closeButton={
          <SafePress onPress={() => setOpen(false)}>
            <Button variant="transparent" variantRadio="sm" ripple>
              <Icon name="Close" variant="black" />
            </Button>
          </SafePress>
        }
        title={<Title size="h4">Terms</Title>}
        actionsContent={
          <>
            <SafePress onPress={() => setOpen(false)}>
              <Button variant="secondary" ripple outline>Reject</Button>
            </SafePress>
            <SafePress onPress={() => setOpen(false)}>
              <Button variant="primary" ripple outline>Accept</Button>
            </SafePress>
          </>
        }
      >
        <div style={{ display: "grid", gap: 12 }}>{paragraphs}</div>
      </Dialog>
    </>
  );
}

Size and variant

Toggle size ("sm" | "md" | "lg") and variant ("basic" | "full").

Live preview

Size/variant toggles

import { useState } from "react";
import Dialog from "@/packages/ui/src/_components/dialog/Dialog";
import { Title } from "@/packages/ui/src/_components/text/title/Title";
import { Paragraph } from "@/packages/ui/src/_components/text/paragraph/Paragraph";
import { Button } from "@/packages/ui/src/_components/buttons_variants/buttons/Button";
import Icon from "@/packages/ui/src/_components/icon/Icon";

function SafePress(props: { onPress: () => void; children: React.ReactNode }) {
  return (
    <span
      style={{ display: "inline-flex" }}
      onClickCapture={(e) => {
        e.preventDefault();
        e.stopPropagation();
        props.onPress();
      }}
    >
      {props.children}
    </span>
  );
}

export default function Example() {
  const [open, setOpen] = useState(false);
  const [size, setSize] = useState<"sm" | "md" | "lg">("sm");
  const [variant, setVariant] = useState<"basic" | "full">("basic");

  return (
    <>
      <div style={{ display: "flex", gap: 12, flexWrap: "wrap", alignItems: "center" }}>
        {open ? null : (
          <Button variant="success" ripple outline onClick={() => setOpen(true)}>Open</Button>
        )}

        <Button variant="secondary" ripple outline onClick={() => setSize((s) => (s === "sm" ? "md" : s === "md" ? "lg" : "sm"))}>
          Size: {size}
        </Button>

        <Button variant="secondary" ripple outline onClick={() => setVariant((v) => (v === "basic" ? "full" : "basic"))}>
          Variant: {variant}
        </Button>
      </div>

      <Dialog
        open={open}
        variant={variant}
        size={size}
        headerVariant={variant === "full" ? "full" : "basic"}
        footerVariant="actions"
        closeBehavior="button"
        showCloseButton
        closeButton={
          <SafePress onPress={() => setOpen(false)}>
            <Button variant="transparent" variantRadio="sm" ripple>
              <Icon name="Close" variant="black" />
            </Button>
          </SafePress>
        }
        title={<Title size="h4">Size + Variant</Title>}
        actionsContent={
          <>
            <SafePress onPress={() => setOpen(false)}>
              <Button variant="secondary" ripple outline>Cancel</Button>
            </SafePress>
            <SafePress onPress={() => setOpen(false)}>
              <Button variant="primary" ripple outline>Confirm</Button>
            </SafePress>
          </>
        }
      >
        <Paragraph size="medium">Validate layout presets quickly.</Paragraph>
      </Dialog>
    </>
  );
}

Header/Footer overrides

Provide full custom nodes using header and footer.

Live preview

Custom header + custom footer

import { useState } from "react";
import Dialog from "@/packages/ui/src/_components/dialog/Dialog";
import { Title } from "@/packages/ui/src/_components/text/title/Title";
import { Paragraph } from "@/packages/ui/src/_components/text/paragraph/Paragraph";
import { Button } from "@/packages/ui/src/_components/buttons_variants/buttons/Button";
import Icon from "@/packages/ui/src/_components/icon/Icon";

function SafePress(props: { onPress: () => void; children: React.ReactNode }) {
  return (
    <span
      style={{ display: "inline-flex" }}
      onClickCapture={(e) => {
        e.preventDefault();
        e.stopPropagation();
        props.onPress();
      }}
    >
      {props.children}
    </span>
  );
}

export default function Example() {
  const [open, setOpen] = useState(false);

  return (
    <>
      {open ? null : (
        <Button variant="info" ripple outline onClick={() => setOpen(true)}>
          Open (override)
        </Button>
      )}

      <Dialog
        open={open}
        variant="basic"
        size="md"
        closeBehavior="button"
        header={
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
            <div style={{ display: "grid", gap: 2 }}>
              <Title size="h5">Custom header</Title>
              <span style={{ opacity: 0.75 }}>Replaces default header.</span>
            </div>

            <SafePress onPress={() => setOpen(false)}>
              <Button variant="transparent" variantRadio="sm" ripple>
                <Icon name="Close" variant="black" />
              </Button>
            </SafePress>
          </div>
        }
        footer={
          <div style={{ display: "flex", justifyContent: "flex-end", gap: 10, flexWrap: "wrap" }}>
            <SafePress onPress={() => setOpen(false)}>
              <Button variant="secondary" ripple outline>Dismiss</Button>
            </SafePress>
            <SafePress onPress={() => setOpen(false)}>
              <Button variant="primary" ripple outline>Save</Button>
            </SafePress>
          </div>
        }
      >
        <Paragraph size="medium">Full control over header/footer layout.</Paragraph>
      </Dialog>
    </>
  );
}

Class hooks

Pass class names for targeting CSS and animations.

Live preview

Internal class hooks

import { useState } from "react";
import Dialog from "@/packages/ui/src/_components/dialog/Dialog";
import { Title } from "@/packages/ui/src/_components/text/title/Title";
import { Paragraph } from "@/packages/ui/src/_components/text/paragraph/Paragraph";
import { Button } from "@/packages/ui/src/_components/buttons_variants/buttons/Button";
import Icon from "@/packages/ui/src/_components/icon/Icon";

function SafePress(props: { onPress: () => void; children: React.ReactNode }) {
  return (
    <span
      style={{ display: "inline-flex" }}
      onClickCapture={(e) => {
        e.preventDefault();
        e.stopPropagation();
        props.onPress();
      }}
    >
      {props.children}
    </span>
  );
}

export default function Example() {
  const [open, setOpen] = useState(false);

  return (
    <>
      {open ? null : (
        <Button variant="secondary" ripple outline onClick={() => setOpen(true)}>
          Open (class hooks)
        </Button>
      )}

      <Dialog
        open={open}
        variant="basic"
        size="md"
        headerVariant="basic"
        footerVariant="actions"
        closeBehavior="button"
        showCloseButton
        closeButton={
          <SafePress onPress={() => setOpen(false)}>
            <Button variant="transparent" variantRadio="sm" ripple>
              <Icon name="Close" variant="black" />
            </Button>
          </SafePress>
        }
        title={<Title size="h4">Styling hooks</Title>}
        actionsContent={
          <>
            <SafePress onPress={() => setOpen(false)}>
              <Button variant="secondary" ripple outline>Cancel</Button>
            </SafePress>
            <SafePress onPress={() => setOpen(false)}>
              <Button variant="primary" ripple outline>Confirm</Button>
            </SafePress>
          </>
        }
        className="docs-dialog-demo"
        scrimClassName="docs-dialog-demo__scrim"
        surfaceClassName="docs-dialog-demo__surface"
        bodyClassName="docs-dialog-demo__body"
        footerClassName="docs-dialog-demo__footer"
      >
        <Paragraph size="medium">Use data-open / is-open for animations.</Paragraph>
      </Dialog>
    </>
  );
}

On this page

Dialog Props

Layout-only dialog (scrim + surface) with header/body/footer slots. It is externally controlled via `open` and does not implement built-in close handlers.

Header priority: header → (iconName + text + closeButton). closeButton is rendered only when showCloseButton=true and closeButton is provided.

Footer priority: footer → actionsContent → nothing.

No built-in close behavior is implemented (e.g., scrim click does not close). Wire close with your own buttons and state.

CSS hooks: root=cssnt-dialog, open flag=is-open + data-open, scrim=cssnt-dialog__scrim, surface=cssnt-dialog__surface, body=cssnt-dialog__body.

Dialog props
PropTypeRequiredDefaultDescription
openbooleanYesn/aControls open state (adds "is-open" and data-open="true|false").
childrenReact.ReactNodeNon/aBody content.
titleReact.ReactNodeNon/aTitle used by the default header composition (when header is not provided).
iconNamestringNon/aOptional icon name used by the default header composition.
iconVariantstringNon/aReserved (currently not used by Dialog.tsx).
headerReact.ReactNodeNon/aFull header override (replaces default header composition).
showCloseButtonbooleanNofalseWhen true, allows rendering `closeButton` inside the default header.
closeButtonReact.ReactNodeNon/aClose button node for the header (rendered only when showCloseButton is true).
closeAriaLabelstringNon/aReserved (currently not used by Dialog.tsx).
footerReact.ReactNodeNon/aFull footer override (has priority over actionsContent).
actionsContentReact.ReactNodeNon/aQuick footer slot used when `footer` is not provided.
variant"basic" | "full"No"basic"Visual variant (maps to modifier classes). (Values: basic, full)
size"sm" | "md" | "lg"No"md"Size preset (maps to modifier classes). (Values: sm, md, lg)
dividerbooleanNofalseAdds a divider modifier class between header/body and/or body/footer.
headerVariant"none" | "basic" | "full"Non/aHeader layout preset (maps to modifier classes). (Values: none, basic, full)
footerVariant"none" | "actions"Non/aFooter layout preset (maps to modifier classes). (Values: none, actions)
closeBehavior"none" | "overlay" | "button" | "both"Non/aClose layout preset (maps to modifier classes). (Values: none, overlay, button, both)
classNamestringNon/aExtra classes for the root element.
customStylesReact.CSSPropertiesNon/aInline styles for the root element.
scrimClassNamestringNon/aExtra classes for the scrim element (.cssnt-dialog__scrim).
surfaceClassNamestringNon/aExtra classes for the surface element (.cssnt-dialog__surface).
bodyClassNamestringNon/aExtra classes for the body element (.cssnt-dialog__body).
footerClassNamestringNon/aExtra classes for the footer element (.cssnt-dialog__footer).