ComponentsCSS
Dialog · Tones
The same dialog with an icon header: a tone-tinted tile and a soft wash of the tone in the corner show its intent at a glance.
Installation
npx shadcn@latest add https://ui-collections.vercel.app/r/glass/dialog-tones.jsonAlso installs the theme, cn() and button, field. Packages: clsx, tailwind-merge, radix-ui.
Usage
dialog-tones-demo.tsx
"use client";
import { useState, type ReactNode } from "react";
import { Button } from "@/registry/kit/ui/button";
import {
Dialog,
DialogBody,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/registry/kit/ui/dialog";
import { Input, Textarea } from "@/registry/kit/ui/field";
// Two-tone icons: a faint fill under a solid shape, tinted by the header tone.
const icon = (children: ReactNode) => (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden>
{children}
</svg>
);
const icons = {
folder: icon(
<>
<path opacity="0.4" d="M3 7a2 2 0 0 1 2-2h4.6l2 2H19a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z" />
<path d="M11 11h2v2h2v2h-2v2h-2v-2H9v-2h2v-2Z" />
</>,
),
info: icon(
<>
<circle cx="12" cy="12" r="9.5" opacity="0.4" />
<rect x="11" y="10.5" width="2" height="6.5" rx="1" />
<circle cx="12" cy="7.75" r="1.25" />
</>,
),
warning: icon(
<>
<path opacity="0.4" d="M10.3 3.6a2 2 0 0 1 3.4 0l8 13.6A2 2 0 0 1 20 20.2H4a2 2 0 0 1-1.7-3l8-13.6Z" />
<rect x="11" y="8.5" width="2" height="6" rx="1" />
<circle cx="12" cy="17.2" r="1.2" />
</>,
),
trash: icon(
<>
<path opacity="0.4" d="M5.5 7h13l-1 12.2a2 2 0 0 1-2 1.8h-7a2 2 0 0 1-2-1.8L5.5 7Z" />
<path d="M9 3.5h6l.8 1.5H20v2H4V5h4.2L9 3.5Z" />
</>,
),
};
const timeline = [
["Queued", "12:04:11"],
["Installing dependencies", "12:04:13"],
["Building (Next.js 16, Turbopack)", "12:04:39"],
["Uploading 214 static files", "12:05:02"],
["Assigning domains", "12:05:06"],
["Ready on marketing-site.app", "12:05:07"],
];
export default function DialogTonesDemo() {
const [confirm, setConfirm] = useState("");
return (
<div className="flex flex-wrap items-center justify-center gap-3">
<Dialog>
<DialogTrigger asChild>
<Button variant="accent">New project</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader icon={icons.folder} tone="accent">
<DialogTitle>New project</DialogTitle>
<DialogDescription>Import a repository or start from a template.</DialogDescription>
</DialogHeader>
<DialogBody className="grid gap-4">
<Input label="Project name" defaultValue="marketing-site" />
<Textarea label="Description" rows={3} hint="Optional, shown on the project card." />
</DialogBody>
<DialogFooter>
<DialogClose asChild>
<Button variant="ghost">Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button variant="accent">Create project</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog>
<DialogTrigger asChild>
<Button>Deployment details</Button>
</DialogTrigger>
<DialogContent size="lg">
<DialogHeader icon={icons.info} tone="info">
<DialogTitle>Deployment #1284</DialogTitle>
<DialogDescription>Production · main · 3f9c2e1, 56 seconds end to end.</DialogDescription>
</DialogHeader>
<DialogBody>
<ol className="grid gap-1">
{timeline.map(([step, time], i) => (
<li key={step} className="flex items-center gap-3 rounded-field px-3 py-2.5 odd:bg-hover">
<span className="grid size-6 shrink-0 place-items-center rounded-full bg-success/15 text-[11px] font-semibold text-success">
{i + 1}
</span>
<span className="flex-1 text-sm">{step}</span>
<span className="font-mono text-xs text-muted">{time}</span>
</li>
))}
</ol>
</DialogBody>
<DialogFooter>
<DialogClose asChild>
<Button variant="ghost">Close</Button>
</DialogClose>
<Button>View logs</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost">Leave editor</Button>
</DialogTrigger>
<DialogContent size="sm">
<DialogHeader icon={icons.warning} tone="warning">
<DialogTitle>Unsaved changes</DialogTitle>
<DialogDescription>You edited three fields. Leave without saving?</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="ghost">Keep editing</Button>
</DialogClose>
<DialogClose asChild>
<Button variant="solid">Leave</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog onOpenChange={() => setConfirm("")}>
<DialogTrigger asChild>
<Button variant="danger">Delete project</Button>
</DialogTrigger>
<DialogContent size="sm">
<DialogHeader icon={icons.trash} tone="danger">
<DialogTitle>Delete project?</DialogTitle>
<DialogDescription>This removes marketing-site, its deployments and domains. It can't be undone.</DialogDescription>
</DialogHeader>
<DialogBody>
<Input label="Type the project name" value={confirm} onChange={(e) => setConfirm(e.target.value)} />
</DialogBody>
<DialogFooter>
<DialogClose asChild>
<Button variant="ghost">Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button variant="danger" disabled={confirm !== "marketing-site"}>
Delete
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}API
<DialogHeader icon>ReactNodeTurns on the toned header.
<DialogHeader tone>= "accent"
"accent" | "success" | "warning" | "info" | "danger"Create → accent, details → info, risk → warning or danger.
<Button variant="danger">variantPairs with the danger tone for destructive confirms.
Source
components/ui/dialog.tsx
"use client";
import { Dialog as DialogPrimitive } from "radix-ui";
import type { ComponentProps, HTMLAttributes } from "react";
import { cn } from "@/registry/kit/lib/utils";
export const Dialog = DialogPrimitive.Root;
export const DialogTrigger = DialogPrimitive.Trigger;
export const DialogClose = DialogPrimitive.Close;
export const DialogPortal = DialogPrimitive.Portal;
export function DialogOverlay({ className, ...props }: ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-bg-deep/45 backdrop-blur-[2px]",
"transition-opacity duration-200 ease-theme starting:opacity-0 data-[state=closed]:animate-fade-out",
className,
)}
{...props}
/>
);
}
export function DialogCloseButton({ className }: { className?: string }) {
return (
<DialogPrimitive.Close
aria-label="Close"
className={cn(
"absolute top-4 right-4 grid size-8 cursor-pointer place-items-center rounded-full text-muted transition-colors",
"hover:bg-fg/10 hover:text-fg focus-visible:outline-2 focus-visible:outline-accent",
className,
)}
>
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" aria-hidden>
<path d="M6 6l12 12M18 6L6 18" />
</svg>
</DialogPrimitive.Close>
);
}
const sizes = { sm: "max-w-sm", md: "max-w-lg", lg: "max-w-2xl", xl: "max-w-4xl" };
export interface DialogContentProps extends ComponentProps<typeof DialogPrimitive.Content> {
/** sm 24rem · md 32rem · lg 42rem · xl 56rem. */
size?: keyof typeof sizes;
/** Show the round close button in the corner. */
showClose?: boolean;
}
/**
* Centred raised panel: DialogHeader > DialogBody > DialogFooter. Header and
* footer stay put; only the body scrolls. The surface shell itself never
* scrolls (glass draws its rim with ::before).
*/
export function DialogContent({ className, size = "md", showClose = true, children, ...props }: DialogContentProps) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
className={cn(
"surface-raised fixed top-1/2 left-1/2 z-50 flex max-h-[calc(100dvh-2rem)] w-[calc(100vw-2rem)] -translate-1/2 flex-col overflow-hidden rounded-card text-fg outline-none",
"transition-[opacity,scale] duration-200 ease-theme starting:scale-[0.96] starting:opacity-0 data-[state=closed]:animate-pop-out",
sizes[size],
className,
)}
{...props}
>
{children}
{showClose && <DialogCloseButton />}
</DialogPrimitive.Content>
</DialogPortal>
);
}
const tones = {
accent: "var(--color-accent)",
success: "var(--color-success)",
warning: "var(--color-warning)",
info: "var(--color-info)",
danger: "var(--color-danger)",
};
export type DialogTone = keyof typeof tones;
export interface DialogHeaderProps extends HTMLAttributes<HTMLDivElement> {
/** Shows a tone-tinted tile beside the title and washes the corner in the same tone. */
icon?: React.ReactNode;
/** The dialog's intent: create → accent, details → info, risk → warning or danger. */
tone?: DialogTone;
}
export function DialogHeader({ icon, tone = "accent", className, style, children, ...props }: DialogHeaderProps) {
if (!icon) {
return (
<div className={cn("grid shrink-0 gap-2 px-6 pt-6 pb-4 pr-14 md:px-7 md:pt-7", className)} style={style} {...props}>
{children}
</div>
);
}
return (
<div
className={cn(
"flex shrink-0 items-start gap-4 px-6 pt-6 pb-4 pr-14 md:px-7 md:pt-7",
"bg-[radial-gradient(120%_140%_at_0%_0%,color-mix(in_oklab,var(--dialog-tone)_16%,transparent),transparent_62%)]",
className,
)}
style={{ "--dialog-tone": tones[tone], ...style } as React.CSSProperties}
{...props}
>
<span
aria-hidden
className="flex size-11 shrink-0 items-center justify-center rounded-control bg-[color-mix(in_oklab,var(--dialog-tone)_16%,transparent)] text-(--dialog-tone) ring-1 ring-[color-mix(in_oklab,var(--dialog-tone)_24%,transparent)] [&_svg]:size-5"
>
{icon}
</span>
<div className="grid min-w-0 gap-1.5 pt-0.5">{children}</div>
</div>
);
}
/** The scrolling middle of a dialog. */
export function DialogBody({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 py-2 md:px-7 [scrollbar-width:thin]", className)}
{...props}
/>
);
}
export function DialogFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("flex shrink-0 flex-col-reverse gap-2 px-6 pt-4 pb-6 sm:flex-row sm:flex-wrap sm:justify-end md:px-7 md:pb-7", className)}
{...props}
/>
);
}
export function DialogTitle({ className, ...props }: ComponentProps<typeof DialogPrimitive.Title>) {
return <DialogPrimitive.Title className={cn("font-display text-2xl leading-tight", className)} {...props} />;
}
export function DialogDescription({ className, ...props }: ComponentProps<typeof DialogPrimitive.Description>) {
return <DialogPrimitive.Description className={cn("text-sm leading-relaxed text-muted", className)} {...props} />;
}