Pickers and overlays
Modal
A complete modal task composed from the Dialog primitives.
When to use it: Use this composition for short forms or focused tasks that must pause interaction with the page behind them.
Example
import { Button, Dialog, DialogContent, DialogTrigger } from "@comp0/react";
export function Example() {
return (
<Dialog>
<DialogTrigger className="rounded bg-teal-700 px-3 py-2.5 text-base text-white sm:py-2 sm:text-sm dark:bg-teal-400 dark:text-zinc-950">
Edit profile
</DialogTrigger>
<DialogContent
aria-labelledby="profile-modal-title"
className="m-auto w-[min(28rem,calc(100vw-2rem))] translate-y-0 rounded-[min(1vw,12px)] bg-white p-5 text-zinc-900 opacity-100 shadow-2xl ring-1 ring-zinc-950/10 transition-[opacity,translate] duration-150 ease-out starting:translate-y-1 starting:opacity-0 motion-reduce:transition-none backdrop:bg-zinc-950/40 dark:bg-zinc-900 dark:text-zinc-50 dark:shadow-none dark:ring-white/10"
>
<form method="dialog" className="grid gap-4">
<div className="grid gap-1">
<h2 id="profile-modal-title" className="text-lg font-semibold">
Edit profile
</h2>
<p className="text-base text-zinc-600 sm:text-sm dark:text-zinc-400">
Update the name shown to your teammates.
</p>
</div>
<label className="grid gap-1.5 text-base font-medium sm:text-sm">
Display name
<input
className="rounded border border-zinc-950/10 bg-white px-3 py-2.5 font-normal outline-teal-600 focus-visible:outline-2 sm:py-2 dark:border-white/10 dark:bg-zinc-950 dark:outline-teal-400"
defaultValue="Ada Lovelace"
name="displayName"
/>
</label>
<div className="flex justify-end gap-2 pt-1">
<Button
className="rounded border border-zinc-950/10 px-3 py-2.5 text-base sm:py-2 sm:text-sm dark:border-white/10"
type="submit"
>
Cancel
</Button>
<Button
className="rounded bg-teal-700 px-3 py-2.5 text-base text-white sm:py-2 sm:text-sm dark:bg-teal-400 dark:text-zinc-950"
type="submit"
>
Save
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}Right-side drawer
Present a focused modal task from the right edge of the viewport.
import { Button, Dialog, DialogContent, DialogTrigger } from "@comp0/react";
import { XMarkIcon } from "@heroicons/react/20/solid";
export function Example() {
return (
<Dialog>
<DialogTrigger className="rounded bg-teal-700 px-3 py-2.5 text-base text-white sm:py-2 sm:text-sm dark:bg-teal-400 dark:text-zinc-950">
Open drawer
</DialogTrigger>
<DialogContent
aria-labelledby="drawer-title"
className="fixed inset-y-0 right-0 left-auto m-0 h-dvh max-h-none w-[min(24rem,calc(100vw-2rem))] translate-x-0 border-0 bg-white p-5 text-zinc-900 opacity-100 shadow-2xl ring-1 ring-zinc-950/10 transition-[opacity,translate] duration-150 ease-out starting:translate-x-2 starting:opacity-0 motion-reduce:transition-none backdrop:bg-zinc-950/40 dark:bg-zinc-900 dark:text-zinc-50 dark:shadow-none dark:ring-white/10"
>
<div className="flex h-full flex-col gap-5">
<div className="flex items-start justify-between gap-4">
<div className="grid gap-1">
<h2 id="drawer-title" className="text-lg font-semibold">
Notifications
</h2>
<p className="text-base text-zinc-600 sm:text-sm dark:text-zinc-400">
Choose which updates should reach you.
</p>
</div>
<form method="dialog">
<Button
type="submit"
aria-label="Close notifications"
className="rounded p-2 text-zinc-500 outline-teal-600 hover:bg-zinc-100 focus-visible:outline-2 dark:text-zinc-400 dark:outline-teal-400 dark:hover:bg-zinc-800"
>
<XMarkIcon className="size-5" aria-hidden="true" />
</Button>
</form>
</div>
<div className="grid gap-3 text-base sm:text-sm">
<label className="flex items-center justify-between gap-4">
Product updates
<input type="checkbox" defaultChecked className="size-4 accent-teal-700" />
</label>
<label className="flex items-center justify-between gap-4">
Weekly digest
<input type="checkbox" className="size-4 accent-teal-700" />
</label>
</div>
</div>
</DialogContent>
</Dialog>
);
}Anatomy
Dashed frames are invisible state providers; shaded shapes own real DOM. Numbered pins match the list below.
A wireframe sketch of the assembled component. Each numbered marker matches a part in the list that follows.
Dialog
Wrapper-free provider for the composed modal state. Does not add a DOM element.
DialogTrigger
Button that opens the modal. Owns a DOM element.
DialogContent
Native modal surface containing the task. Owns a DOM element.
Step by step
- 1
Add the main part
Start with Dialog and a DialogTrigger.
- 2
Add the supporting parts
Compose the heading, content, and actions inside DialogContent.
- 3
Make the behavior clear
Name the content from its visible heading and provide an explicit close action.
Exampletsx <Dialog><DialogTrigger>Edit profile</DialogTrigger><DialogContent aria-labelledby="profile-title"><h2 id="profile-title">Edit profile</h2></DialogContent></Dialog>
Keyboard
- Esc
- Closes and restores trigger focus.
- ⇥
- Cycles inside the modal.
Forms and accessibility
Forms inside DialogContent submit normally; method=dialog closes the modal without navigation.
Accessibility checklist
- Connect DialogContent to a visible heading with aria-labelledby.
- Keep the task short enough to understand without the page behind it.
- Include an explicit close action even when Escape can dismiss the modal.
API reference
import { Dialog, DialogContent, DialogTrigger } from "@comp0/react";Dialog
Context onlyWrapper-free provider for the composed modal state.
| Prop | Type | Description |
|---|---|---|
open / defaultOpen | boolean | Controlled or initial open state. |
onToggle | (open: boolean) => void | Receives the next open state. |
DialogTrigger
DOM elementButton that opens the modal.
| Prop | Type | Description |
|---|---|---|
as | ElementType | Fragment | Fragment merges the trigger onto your own element child. |
DialogContent
DOM elementNative modal surface containing the task.
| Prop | Type | Description |
|---|---|---|
aria-labelledby | string | Points to the modal's visible heading. |
portal | boolean | Renders into document.body; on by default. |
closedby | "any" | "closerequest" | "none" | Native dismissal policy; any adds light dismiss where supported. |
Style hooks
Attributes that appear while a state is true. Target them with Tailwind data variants such as data-open:bg-zinc-100, or with any CSS selector.
[data-open]on DialogTrigger, DialogContent
- The modal is open.
:openon DialogContent
- Native pseudo-class equivalent.