Skip to content

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.

On this page

Example

Edit profile

Update the name shown to your teammates.

Modal.tsxtsx
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.

Notifications

Choose which updates should reach you.

modal.drawer.tsxtsx
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.

  1. Dialog

    Wrapper-free provider for the composed modal state. Does not add a DOM element.

  2. DialogTrigger

    Button that opens the modal. Owns a DOM element.

  3. DialogContent

    Native modal surface containing the task. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Start with Dialog and a DialogTrigger.

  2. 2

    Add the supporting parts

    Compose the heading, content, and actions inside DialogContent.

  3. 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

Importtsx
import { Dialog, DialogContent, DialogTrigger } from "@comp0/react";

Dialog

Context only

Wrapper-free provider for the composed modal state.

PropTypeDescription
open / defaultOpenbooleanControlled or initial open state.
onToggle(open: boolean) => voidReceives the next open state.

DialogTrigger

DOM element

Button that opens the modal.

PropTypeDescription
asElementType | FragmentFragment merges the trigger onto your own element child.

DialogContent

DOM element

Native modal surface containing the task.

PropTypeDescription
aria-labelledbystringPoints to the modal's visible heading.
portalbooleanRenders 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.
:open

on DialogContent

Native pseudo-class equivalent.

Keep exploring