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
Invite a teammate
They'll get an email to join Kaveri Studio.
"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.