Dialog

A modal dialog that overlays the page content, typically used for important actions or information.

Docs
import {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogCloseTrigger,
  Button,
} from "~/components/dialog";

export default function DialogBasicDemo() {
  return (
    <Dialog>
      <DialogTrigger asChild={(props) => <Button {...props()}>About Ark UI</Button>} />
      <DialogContent>
        <DialogHeader>
          <DialogTitle>About Ark UI</DialogTitle>
          <DialogDescription>
            Ark UI is a headless, accessible component library that works with Solid, Vue, and
            React.
          </DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <DialogCloseTrigger variant="outline">Close</DialogCloseTrigger>
          <Button>Learn More</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

Installation

CLI

Run the following command to add the component to your project:

npx @ark-preset/cli@latest add dialog

Manual

Create the recipe file at src/components/recipes/dialog.ts:

import { tv, type VariantProps } from "tailwind-variants";

export const dialogVariants = tv({
  slots: {
    backdrop:
      "fixed inset-0 z-50 bg-background/10 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=open]:duration-300 data-[state=closed]:duration-200",
    positioner: "fixed inset-0 z-50 flex items-center justify-center",
    content:
      "relative z-50 grid w-full max-w-lg gap-4 border border-border bg-background p-4 shadow-lg rounded-2xl mx-2 sm:mx-0 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=open]:duration-300 data-[state=closed]:duration-200",
    header: "flex flex-col space-y-1.5 text-center sm:text-left",
    footer:
      "flex flex-col-reverse sm:flex-row sm:justify-end gap-2 bg-muted/50 p-4 -mx-4 -mb-4 rounded-b-2xl border-t border-border",
    title: "text-lg font-semibold tracking-tight",
    description: "text-sm text-muted-foreground",
    closeTrigger:
      "absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 p-1",
  },
});

export type DialogVariants = VariantProps<typeof dialogVariants>;

Create the component directory and files.

src/components/dialog/dialog.base.tsx:

import { Dialog as ArkDialog } from "@ark-ui/solid/dialog";
import { splitProps, type Component } from "solid-js";
import { buttonVariants, dialogVariants, type ButtonVariants } from "../recipes/dialog";
import { HTMLProps } from "@ark-ui/solid";

const styles = dialogVariants();

const DialogRoot = ArkDialog.Root;
const DialogRootProvider = ArkDialog.RootProvider;
const DialogTrigger: Component<ArkDialog.TriggerProps & ButtonVariants> = (props) => {
  const [local, others] = splitProps(props, ["class", "variant", "size"]);
  return (
    <ArkDialog.Trigger
      class={buttonVariants({ variant: local.variant, size: local.size, class: local.class })}
      {...others}
    />
  );
};

const DialogBackdrop: Component<ArkDialog.BackdropProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkDialog.Backdrop class={styles.backdrop({ class: local.class })} {...others} />;
};

const DialogPositioner: Component<ArkDialog.PositionerProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkDialog.Positioner class={styles.positioner({ class: local.class })} {...others} />;
};

const DialogContent: Component<ArkDialog.ContentProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkDialog.Content class={styles.content({ class: local.class })} {...others} />;
};

const DialogCloseTrigger: Component<ArkDialog.CloseTriggerProps & ButtonVariants> = (props) => {
  const [local, others] = splitProps(props, ["class", "variant", "size"]);
  return (
    <ArkDialog.CloseTrigger
      class={buttonVariants({ variant: local.variant, size: local.size, class: local.class })}
      {...others}
    />
  );
};

const DialogIconCloseTrigger: Component<ArkDialog.CloseTriggerProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <Dialog.UnstyledCloseTrigger class={styles.closeTrigger({ class: local.class })} {...others} />
  );
};

const DialogTitle: Component<ArkDialog.TitleProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkDialog.Title class={styles.title({ class: local.class })} {...others} />;
};

const DialogDescription: Component<ArkDialog.DescriptionProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkDialog.Description class={styles.description({ class: local.class })} {...others} />;
};

const DialogHeader: Component<HTMLProps<"div">> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <div class={styles.header({ class: local.class })} {...others} />;
};

const DialogFooter: Component<HTMLProps<"div">> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <div class={styles.footer({ class: local.class })} {...others} />;
};

export const Dialog = {
  Root: DialogRoot,
  RootProvider: DialogRootProvider,
  Trigger: DialogTrigger,
  Backdrop: DialogBackdrop,
  Positioner: DialogPositioner,
  Content: DialogContent,
  CloseTrigger: DialogCloseTrigger,
  UnstyledCloseTrigger: ArkDialog.CloseTrigger,
  IconCloseTrigger: DialogIconCloseTrigger,
  Title: DialogTitle,
  Description: DialogDescription,
  Header: DialogHeader,
  Footer: DialogFooter,
};

src/components/dialog/index.tsx:

import { Dialog as ArkDialog } from "@ark-ui/solid/dialog";
import { Portal } from "solid-js/web";
import { splitProps, type Component } from "solid-js";
import { Dialog as DialogBase } from "./dialog.base";

const DialogContent: Component<ArkDialog.ContentProps> = (props) => {
  const [local, others] = splitProps(props, ["class", "children"]);
  return (
    <Portal>
      <DialogBase.Backdrop />
      <DialogBase.Positioner>
        <DialogBase.Content class={local.class} {...others}>
          {local.children}
          <DialogBase.IconCloseTrigger>
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="24"
              height="24"
              viewBox="0 0 24 24"
              fill="none"
              stroke="currentColor"
              stroke-width="2"
              class="size-4"
            >
              <path d="M18 6L6 18" />
              <path d="M6 6l12 12" />
            </svg>
          </DialogBase.IconCloseTrigger>
        </DialogBase.Content>
      </DialogBase.Positioner>
    </Portal>
  );
};

const Dialog = DialogBase.Root;
const DialogTrigger = DialogBase.Trigger;
const DialogHeader = DialogBase.Header;
const DialogTitle = DialogBase.Title;
const DialogDescription = DialogBase.Description;
const DialogFooter = DialogBase.Footer;
const DialogCloseTrigger = DialogBase.CloseTrigger;
const DialogUnstyledCloseTrigger = ArkDialog.CloseTrigger;
const DialogIconCloseTrigger = DialogBase.IconCloseTrigger;
export {
  Dialog,
  DialogTrigger,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogContent,
  DialogCloseTrigger,
  DialogUnstyledCloseTrigger,
  DialogIconCloseTrigger,
  DialogBase,
};

export { dialogVariants, type DialogVariants } from "../recipes/dialog";

Note: Make sure your project has the Tailwind CSS theme variables set up (--background, --foreground, --ring, --border, etc.) or override the utility classes to match your design system.

Dependencies: This component imports shared recipes or sub-components from other packages. Make sure the following are also installed: button.

Usage

Basic Usage

A basic modal dialog with a trigger and dismissible content.

import {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogCloseTrigger,
  Button,
} from "~/components/dialog";

export default function DialogBasicDemo() {
  return (
    <Dialog>
      <DialogTrigger asChild={(props) => <Button {...props()}>About Ark UI</Button>} />
      <DialogContent>
        <DialogHeader>
          <DialogTitle>About Ark UI</DialogTitle>
          <DialogDescription>
            Ark UI is a headless, accessible component library that works with Solid, Vue, and
            React.
          </DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <DialogCloseTrigger variant="outline">Close</DialogCloseTrigger>
          <Button>Learn More</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

Simple Form

Dialogs are commonly used for forms or data entry without leaving the current page.

import {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogCloseTrigger,
  Button,
  Input,
} from "~/components/dialog";

export default function DialogFormDemo() {
  return (
    <Dialog>
      <DialogTrigger asChild={(props) => <Button {...props()}>Add User</Button>} />
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Add New User</DialogTitle>
          <DialogDescription>Enter the details for the new user below.</DialogDescription>
        </DialogHeader>
        <div class="space-y-4 py-4">
          <Input label="Name" placeholder="John Doe" />
          <Input label="Username" placeholder="@johndoe" />
        </div>
        <DialogFooter>
          <DialogCloseTrigger variant="outline">Cancel</DialogCloseTrigger>
          <Button>Save User</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

Controlled Open

Control the dialog open state programmatically.

import { createSignal } from "solid-js";
import { Dialog, DialogTrigger, DialogContent, DialogTitle, Button } from "~/components/dialog";

export default function DialogControlledDemo() {
  const [open, setOpen] = createSignal(false);

  return (
    <div class="flex items-center gap-4">
      <Button onClick={() => setOpen(true)}>Open Dialog</Button>
      <Dialog open={open()} onOpenChange={(e) => setOpen(e.open)}>
        <DialogTrigger>Trigger</DialogTrigger>
        <DialogContent>
          <DialogTitle>Controlled Dialog</DialogTitle>
        </DialogContent>
      </Dialog>
    </div>
  );
}

Advanced Usage

Root Provider

Use DialogBase.RootProvider when you need to access the dialog state outside of the component tree. This pattern uses the useDialog hook from Ark UI to create a shared context that both the component and external elements can reference.

Open: false
import { useDialog } from "@ark-ui/solid/dialog";
import {
  DialogContent,
  DialogTrigger,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogBase,
} from "~/components/dialog";
import { Button } from "~/components/dialog";

export default function DialogRootProviderDemo() {
  const dialog = useDialog({ defaultOpen: false });

  return (
    <div class="space-y-4">
      <output class="block text-sm text-muted-foreground">
        Open: {JSON.stringify(dialog().open)}
      </output>

      <div class="flex gap-2">
        <Button onClick={() => dialog().setOpen(true)} variant="secondary">
          Open Dialog From Outside
        </Button>
      </div>

      <DialogBase.RootProvider value={dialog}>
        <DialogTrigger>Open Dialog</DialogTrigger>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Externally Controlled Dialog</DialogTitle>
            <DialogDescription>
              This dialog is controlled via <code>useDialog</code>. The buttons and output above
              manage the state from outside the dialog tree.
            </DialogDescription>
          </DialogHeader>
          <DialogFooter>
            <Button variant="outline" onClick={() => dialog().setOpen(false)}>
              Cancel
            </Button>
            <Button onClick={() => dialog().setOpen(false)}>Confirm</Button>
          </DialogFooter>
        </DialogContent>
      </DialogBase.RootProvider>
    </div>
  );
}

The key difference:

  • Dialog — manages its own state internally. Use for simple, self-contained usage.
  • DialogBase.RootProvider — accepts a pre-created context via useDialog. Use when you need to read or control the dialog state from outside the component tree.

API Reference

See the Ark UI Dialog documentation.