Dialog

A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.

Download runnable sandbox
import { createUniqueId } from 'solid-js';
import { Dialog } from '@radix-solid/primitives';
import { Cross2Icon } from '@radix-solid/icons';
import styles from './styles.module.css';
export default function DialogDemo() {
const name = createUniqueId();
const username = createUniqueId();
return <Dialog.Root>
<Dialog.Trigger asChild>{target => target('button', { class: `${styles.Button} violet`, children: 'Edit profile' })}</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay class={styles.Overlay} />
<Dialog.Content class={styles.Content}>
<Dialog.Title class={styles.Title}>Edit profile</Dialog.Title>
<Dialog.Description class={styles.Description}>Make changes to your profile here. Click save when you're done.</Dialog.Description>
<fieldset class={styles.Fieldset}><label class={styles.Label} for={name}>Name</label><input class={styles.Input} id={name} value="Pedro Duarte" /></fieldset>
<fieldset class={styles.Fieldset}><label class={styles.Label} for={username}>Username</label><input class={styles.Input} id={username} value="@peduarte" /></fieldset>
<div style={{ display: 'flex', 'margin-top': '25px', 'justify-content': 'flex-end' }}>
<Dialog.Close asChild>{target => target('button', { class: `${styles.Button} green`, children: 'Save changes' })}</Dialog.Close>
</div>
<Dialog.Close asChild>{target => target('button', { class: styles.IconButton, 'aria-label': 'Close', children: <Cross2Icon /> })}</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>;
}

Features

  • Supports modal and non-modal modes.

  • Focus is automatically trapped within modal.

  • Can be controlled or uncontrolled.

  • Manages screen reader announcements with Title and Description components.

  • Esc closes the component automatically.

Anatomy

Import all parts and piece them together.

import { Dialog } from "@radix-solid/primitives";
export default () => (
<Dialog.Root>
<Dialog.Trigger />
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title />
<Dialog.Description />
<Dialog.Close />
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);

For a custom trigger, asChild takes a target factory, not an already-created JSX child. The target attaches the primitive's props, ref, and event composition to the selected host.

<Dialog.Trigger asChild>
{target => target("button", { class: "custom-trigger", children: "Open dialog" })}
</Dialog.Trigger>

API Reference

Root

Contains all the parts of a dialog.

PropTypeDefault
defaultOpenboolean—
openboolean—
onOpenChangefunction—
modalbooleantrue

Trigger

The button that opens the dialog.

PropTypeDefault
asChildbooleanfalse
Data attributeValues
[data-state]"open" | "closed"

Portal

When used, portals your overlay and content parts into the body.

PropTypeDefault
forceMountboolean—
containerHTMLElementdocument.body

Overlay

A layer that covers the inert portion of the view when the dialog is open.

PropTypeDefault
asChildbooleanfalse
forceMountboolean—
Data attributeValues
[data-state]"open" | "closed"

Content

Contains content to be rendered in the open dialog.

PropTypeDefault
asChildbooleanfalse
forceMountboolean—
onOpenAutoFocusfunction—
onCloseAutoFocusfunction—
onEscapeKeyDownfunction—
onPointerDownOutsidefunction—
onInteractOutsidefunction—
Data attributeValues
[data-state]"open" | "closed"

Close

The button that closes the dialog.

PropTypeDefault
asChildbooleanfalse

Title

An accessible title to be announced when the dialog is opened.

If you want to hide the title, use our Visually Hidden utility: <VisuallyHidden.Root asChild>{target => target(Dialog.Title, { children: "Title" })}</VisuallyHidden.Root>.

PropTypeDefault
asChildbooleanfalse

Description

An optional accessible description to be announced when the dialog is opened.

If you want to hide the description, use our Visually Hidden utility: <VisuallyHidden.Root asChild>{target => target(Dialog.Description, { children: "Description" })}</VisuallyHidden.Root>. To remove the description entirely, omit this part and pass aria-describedby={undefined} to Dialog.Content.

PropTypeDefault
asChildbooleanfalse

Examples

Close after asynchronous form submission

Use the controlled props to programmatically close the Dialog after an async operation has completed.

import { createSignal } from "solid-js";
import { Dialog } from "@radix-solid/primitives";
const wait = () => new Promise((resolve) => setTimeout(resolve, 1000));
export default () => {
const [open, setOpen] = createSignal(false);
return (
<Dialog.Root open={open()} onOpenChange={setOpen}>
<Dialog.Trigger>Open</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<form onSubmit={(event) => { wait().then(() => setOpen(false)); event.preventDefault(); }} >
{/** some inputs */}
<button type="submit">Submit</button>
</form>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
};

Scrollable overlay

Move the content inside the overlay to render a dialog with overflow.

// index.tsx
import { Dialog } from "@radix-solid/primitives";
import "./styles.css";
export default () => {
return (
<Dialog.Root>
<Dialog.Trigger />
<Dialog.Portal>
<Dialog.Overlay class="DialogOverlay">
<Dialog.Content class="DialogContent">...</Dialog.Content>
</Dialog.Overlay>
</Dialog.Portal>
</Dialog.Root>
);
};
/* styles.css */
.DialogOverlay {
background: rgba(0 0 0 / 0.5);
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: grid;
place-items: center;
overflow-y: auto;
}
.DialogContent {
min-width: 300px;
background: white;
padding: 30px;
border-radius: 4px;
}

Custom portal container

Customise the element that your dialog portals into.

import { createSignal } from "solid-js";
import { Dialog } from "@radix-solid/primitives";
export default () => {
const [container, setContainer] = createSignal<HTMLDivElement | null>(null);
return (
<div>
<Dialog.Root>
<Dialog.Trigger />
<Dialog.Portal container={container()}>
<Dialog.Overlay />
<Dialog.Content>...</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
<div ref={node => { setContainer(node); }} />
</div>
);
};

Accessibility

Adheres to the Dialog WAI-ARIA design pattern.

Keyboard Interactions

KeyDescription
Space
Opens/closes the dialog.
Enter
Opens/closes the dialog.
Tab
Moves focus to the next focusable element.
Shift + Tab
Moves focus to the previous focusable element.
Esc
Closes the dialog and moves focus to Dialog.Trigger.

Custom APIs

Create your own API by abstracting the primitive parts into your own component.

Abstract the overlay and the close button

This example abstracts the Dialog.Overlay and Dialog.Close parts.

Usage

import { Dialog, DialogTrigger, DialogContent } from "./your-dialog";
export default () => (
<Dialog>
<DialogTrigger>Dialog trigger</DialogTrigger>
<DialogContent>Dialog Content</DialogContent>
</Dialog>
);

Implementation

// your-dialog.tsx
import { omit } from "solid-js";
import type { ComponentProps } from "@solidjs/web";
import { Dialog as DialogPrimitive } from "@radix-solid/primitives";
import { Cross1Icon } from "@radix-solid/icons";
export function DialogContent(props: Extract<ComponentProps<typeof DialogPrimitive.Content>, { asChild?: false }>) {
return (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay />
<DialogPrimitive.Content {...omit(props, "children")}>
{props.children}
<DialogPrimitive.Close aria-label="Close">
<Cross1Icon />
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
);
}
export const Dialog = DialogPrimitive.Root;
export const DialogTrigger = DialogPrimitive.Trigger;