ComponentProduct
Dialog
Modal dialog with a framed panel, focus management, Escape dismissal, and a scrollable overlay.
$ npx context-ds add dialogUsage
Use it for
Confirmations, short forms, pickers. Title h5, body padded 20px, actions right-aligned over a light line.
Avoid
- Centered dialogs
- Dialogs for long workflows (use a page)
Dependencies: surface, @radix-ui/react-dialog, lucide-react
Example
examples/dialog.tsx
"use client";import { useState } from "react";import { Button } from "@/components/ui/button";import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";import { Input } from "@/components/ui/input";import { Field } from "@/components/ui/field";import { SecretField } from "@/components/ui/secret-field";export function Product() { const [open, setOpen] = useState(false); const [name, setName] = useState(""); const [created, setCreated] = useState(""); return ( <div className="grid w-full max-w-sm gap-4"> <Dialog open={open} onOpenChange={setOpen}> <DialogTrigger asChild><Button variant="brand">Create key</Button></DialogTrigger> <DialogContent> <form onSubmit={(event) => { event.preventDefault(); if (!name.trim()) return; setCreated(name.trim()); setOpen(false); }}> <DialogHeader><DialogTitle>Create an API key</DialogTitle><DialogDescription>Keys work for every endpoint on your plan.</DialogDescription></DialogHeader> <DialogBody><Field label="Name" htmlFor="key-name" help="Only you see this." required><Input id="key-name" value={name} onChange={(event) => setName(event.target.value)} placeholder="Production" /></Field></DialogBody> <DialogFooter><DialogClose asChild><Button variant="outline">Cancel</Button></DialogClose><Button type="submit" variant="brand" disabled={!name.trim()}>Create</Button></DialogFooter> </form> </DialogContent> </Dialog> {created && <div className="grid gap-2"><p role="status" className="text-body-sm">{created} key created in this example.</p><SecretField secret="ctx_example_9f2c41b7e0a84d6c" /></div>} </div> );}Source
components/ui/dialog.tsx
"use client";import * as React from "react";import * as DialogPrimitive from "@radix-ui/react-dialog";import { X } from "lucide-react";import { usePortalContainer } from "@/lib/surface";import { cn } from "@/lib/utils";// The dashboard's dialog: a white panel in a 6px frame that floats on a hairline and a soft shadow, rising 4px as it// fades in. Radix adds the focus trap, scroll lock and Escape.function Dialog(props: React.ComponentProps<typeof DialogPrimitive.Root>) { return <DialogPrimitive.Root data-slot="dialog" {...props} />;}function DialogTrigger(props: React.ComponentProps<typeof DialogPrimitive.Trigger>) { return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;}function DialogClose(props: React.ComponentProps<typeof DialogPrimitive.Close>) { return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;}function DialogPortal(props: React.ComponentProps<typeof DialogPrimitive.Portal>) { const container = usePortalContainer(); return <DialogPrimitive.Portal data-slot="dialog-portal" container={container} {...props} />;}function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) { return <DialogPrimitive.Overlay data-slot="dialog-overlay" className={cn("fixed inset-0 z-50 flex items-start justify-center overflow-y-auto overscroll-contain bg-scrim px-4 pt-[12vh] pb-6 max-md:pt-4", className)} {...props} />;}const WIDTHS = { md: "max-w-lg", lg: "max-w-3xl", xl: "max-w-5xl", "2xl": "max-w-6xl" } as const;function DialogContent({ className, children, size = "md", showCloseButton = true, ...props}: React.ComponentProps<typeof DialogPrimitive.Content> & { size?: keyof typeof WIDTHS; showCloseButton?: boolean }) { return ( <DialogPortal> <DialogOverlay> <DialogPrimitive.Content data-slot="dialog-content" className={cn( "relative z-50 w-full shrink-0 animate-content-in rounded-shell bg-surface p-(--ds-space-frame) shadow-floating outline-none", WIDTHS[size], className, )} {...props} > <div className="relative rounded-shell bg-surface"> {children} {showCloseButton && ( <DialogPrimitive.Close aria-label="Close" className="absolute top-3.5 right-3 grid size-8 place-items-center rounded-control text-fg-subtle transition-colors duration-150 ease-out outline-none hover:bg-wash-hover hover:text-fg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus-ring [&_svg]:size-4" > <X strokeWidth={1.5} /> </DialogPrimitive.Close> )} </div> </DialogPrimitive.Content> </DialogOverlay> </DialogPortal> );}function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { return <div data-slot="dialog-header" className={cn("flex flex-col gap-1.5 px-5 pt-5 pr-15", className)} {...props} />;}function DialogBody({ className, ...props }: React.ComponentProps<"div">) { return <div data-slot="dialog-body" className={cn("px-5 pt-5 pb-5 text-body-sm text-fg", className)} {...props} />;}function DialogFooter({ className, divider = true, ...props }: React.ComponentProps<"div"> & { divider?: boolean }) { return <div data-slot="dialog-footer" className={cn("flex items-center justify-end gap-2 px-5", divider ? "border-t border-line-subtle py-3.5" : "pb-5", className)} {...props} />;}function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) { return <DialogPrimitive.Title data-slot="dialog-title" className={cn("text-h5 leading-snug font-normal tracking-normal text-fg", className)} {...props} />;}function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) { return <DialogPrimitive.Description data-slot="dialog-description" className={cn("text-body-sm text-fg-muted", className)} {...props} />;}export { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger };Reference: context-dashboard c0ddf7a src/components/ui/dialog.tsx.