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

Loading example…
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.

Loading example…
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
openbooleanControlled open state.
defaultOpenbooleanInitial 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.

DialogTrigger

Style hookMeaning
[data-open]The modal is open.

DialogContent

Style hookMeaning
[data-open]The modal is open.
:openNative pseudo-class equivalent.

Keep exploring