Skip to content
All Taste components

Overlays

Modal dialog

A focused task over a dimmed page, with a title, a short form and clear actions.

title · description · close button · form body · footer actions

"use client";
import { useEffect, useRef } from "react";
import { X } from "lucide-react";

/**
 * Built on the native <dialog>: showModal() traps focus, Escape closes it,
 * and ::backdrop dims the page — no portal or focus-trap library needed.
 */
export function Modal({ open, onClose, title, description, children, footer }: {
  open: boolean;
  onClose: () => void;
  title: string;
  description?: string;
  children: React.ReactNode;
  footer?: React.ReactNode;
}) {
  const ref = useRef<HTMLDialogElement>(null);

  useEffect(() => {
    const d = ref.current;
    if (!d) return;
    if (open && !d.open) d.showModal();
    if (!open && d.open) d.close();
  }, [open]);

  return (
    <dialog
      ref={ref}
      onClose={onClose}
      onClick={(e) => { if (e.target === ref.current) onClose(); }}
      aria-labelledby="modal-title"
      aria-describedby={description ? "modal-desc" : undefined}
      className="w-[calc(100%-32px)] max-w-[400px] rounded-lg border border-border bg-bg p-0 text-fg shadow-2xl backdrop:bg-neutral-950/45"
    >
      <div className="grid gap-1 pl-5 pr-[52px] pt-5">
        <h2 id="modal-title" className="text-base font-semibold tracking-tight">{title}</h2>
        {description && <p id="modal-desc" className="text-[13.5px] text-fg-muted">{description}</p>}
      </div>
      <button type="button" onClick={onClose} aria-label="Close"
        className="absolute right-3 top-3 grid size-8 place-items-center rounded text-fg-subtle hover:bg-surface-2 hover:text-fg">
        <X className="size-4" aria-hidden />
      </button>
      <div className="grid gap-3.5 px-5 pb-5 pt-4">{children}</div>
      {footer && <div className="flex flex-wrap justify-end gap-2 border-t border-border bg-surface px-5 py-3">{footer}</div>}
    </dialog>
  );
}

// <Modal open={open} onClose={() => setOpen(false)} title="Invite a teammate"
//   description="They'll get an email to join Kaveri Studio."
//   footer={<><Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button><Button>Send invite</Button></>}>
//   <TextField label="Email" type="email" />
// </Modal>

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.