Dialog
A modal dialog that overlays the page content, typically used for important actions or information.
Docsimport {
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 dialogManual
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.
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 viauseDialog. Use when you need to read or control the dialog state from outside the component tree.
API Reference
See the Ark UI Dialog documentation.