mode_comment

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

Dialog — Props
PropTypeRequiredDefaultDescription
openbooleanYesControls open state (adds "is-open" and data-open="true|false").
childrenReact.ReactNodeNoBody content.
titleReact.ReactNodeNoTitle used by the default header composition (when header is not provided).
iconNamestringNoOptional icon name used by the default header composition.
iconVariantstringNoReserved (currently not used by Dialog.tsx).
headerReact.ReactNodeNoFull header override (replaces default header composition).
showCloseButtonbooleanNofalseWhen true, allows rendering `closeButton` inside the default header.
closeButtonReact.ReactNodeNoClose button node for the header (rendered only when showCloseButton is true).
closeAriaLabelstringNoReserved (currently not used by Dialog.tsx).
footerReact.ReactNodeNoFull footer override (has priority over actionsContent).
actionsContentReact.ReactNodeNoQuick footer slot used when `footer` is not provided.
variant"basic" | "full"No"basic"Visual variant (maps to modifier classes).
size"sm" | "md" | "lg"No"md"Size preset (maps to modifier classes).
dividerbooleanNofalseAdds a divider modifier class between header/body and/or body/footer.
headerVariant"none" | "basic" | "full"NoHeader layout preset (maps to modifier classes).
footerVariant"none" | "actions"NoFooter layout preset (maps to modifier classes).
closeBehavior"none" | "overlay" | "button" | "both"NoClose layout preset (maps to modifier classes).
classNamestringNoExtra classes for the root element.
customStylesReact.CSSPropertiesNoInline styles for the root element.
scrimClassNamestringNoExtra classes for the scrim element (.cssnt-dialog__scrim).
surfaceClassNamestringNoExtra classes for the surface element (.cssnt-dialog__surface).
bodyClassNamestringNoExtra classes for the body element (.cssnt-dialog__body).
footerClassNamestringNoExtra classes for the footer element (.cssnt-dialog__footer).