Skip to content
EMBOSS
Docs menu

Dialog

A modal plate at the highest float elevation; the page physically recesses behind it while focus is trapped.

@emboss/dialog

Installation

pnpm dlx shadcn@latest add @emboss/dialog

Usage

example.tsx
import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";

export function Example() {
  return (
    <Dialog>
      <DialogTrigger render={<Button>Open</Button>} />
      <DialogContent>
        <DialogTitle>Write preset</DialogTitle>
        <DialogDescription>This overwrites slot A.</DialogDescription>
        <DialogFooter>
          <DialogClose render={<Button variant="ghost">Cancel</Button>} />
          <Button variant="accent">Write</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

API reference

Dialog / DialogTrigger / DialogContent

Root state, trigger, and the modal plate. DialogTitle and DialogDescription are required for accessible naming; DialogClose closes from inside.

PropTypeDefaultDescription
open / defaultOpen / onOpenChangeboolean / boolean / (open) => voidControlled and uncontrolled open state.
showCloseButtonbooleantrueWhether DialogContent renders the corner close button.

Keyboard

KeysAction
EscCloses the dialog and returns focus to the trigger.
TabCycles focus inside the dialog while open.

Source

View source — dialog.tsx
dialog.tsx
"use client";

import { Dialog as BaseDialog } from "@base-ui/react/dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";

/**
 * A modal plate floating high above a darkened page. Focus is trapped,
 * scroll is locked, and Escape or the backdrop dismisses.
 *
 * @example
 * <Dialog>
 *   <DialogTrigger render={<Button>Open</Button>} />
 *   <DialogContent>
 *     <DialogTitle>Write preset</DialogTitle>
 *     <DialogDescription>This overwrites slot A.</DialogDescription>
 *   </DialogContent>
 * </Dialog>
 */
function Dialog(props: React.ComponentProps<typeof BaseDialog.Root>) {
  return <BaseDialog.Root {...props} />;
}

function DialogTrigger(props: React.ComponentProps<typeof BaseDialog.Trigger>) {
  return <BaseDialog.Trigger data-slot="dialog-trigger" {...props} />;
}

function DialogContent({
  className,
  children,
  showCloseButton = true,
  ...props
}: React.ComponentProps<typeof BaseDialog.Popup> & {
  /** Hide the built-in corner close button. */
  showCloseButton?: boolean;
}) {
  return (
    <BaseDialog.Portal>
      <BaseDialog.Backdrop
        data-slot="dialog-backdrop"
        className="fixed inset-0 z-50 bg-ink/40 transition-opacity duration-(--duration-press) ease-(--ease-press) data-ending-style:opacity-0 data-starting-style:opacity-0"
      />
      <BaseDialog.Popup
        data-slot="dialog-content"
        className={cn(
          "fixed top-1/2 left-1/2 z-50 flex w-[min(92vw,28rem)] -translate-x-1/2 -translate-y-1/2 flex-col gap-4 rounded-lg bg-surface-2 p-6 text-ink shadow-float-2 transition-[opacity,scale] duration-(--duration-press) ease-(--ease-press) data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0",
          className,
        )}
        {...props}
      >
        {children}
        {showCloseButton ? (
          <BaseDialog.Close
            aria-label="Close"
            data-slot="dialog-close-corner"
            className="absolute top-4 right-4 inline-flex size-7 cursor-pointer items-center justify-center rounded-sm text-ink-faint focus-ring transition-[box-shadow,color] hover:text-ink hover:shadow-flush"
          >
            <X className="size-4" aria-hidden />
          </BaseDialog.Close>
        ) : null}
      </BaseDialog.Popup>
    </BaseDialog.Portal>
  );
}

function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="dialog-header"
      className={cn("flex flex-col gap-1.5", className)}
      {...props}
    />
  );
}

function DialogTitle({
  className,
  ...props
}: React.ComponentProps<typeof BaseDialog.Title>) {
  return (
    <BaseDialog.Title
      data-slot="dialog-title"
      className={cn("text-lg leading-none font-semibold", className)}
      {...props}
    />
  );
}

function DialogDescription({
  className,
  ...props
}: React.ComponentProps<typeof BaseDialog.Description>) {
  return (
    <BaseDialog.Description
      data-slot="dialog-description"
      className={cn("text-sm text-ink-muted", className)}
      {...props}
    />
  );
}

function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="dialog-footer"
      className={cn("flex justify-end gap-2", className)}
      {...props}
    />
  );
}

function DialogClose(props: React.ComponentProps<typeof BaseDialog.Close>) {
  return <BaseDialog.Close data-slot="dialog-close" {...props} />;
}

export {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogClose,
};