Overlays
Dialog.
A modal window over a dimmed backdrop, for short forms and focused reading. Focus is trapped inside and Escape closes it.
"use client";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export default function DialogDemo() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="secondary">Edit profile</Button>
</DialogTrigger>
<DialogContent>
<form className="grid gap-5" onSubmit={(event) => event.preventDefault()}>
<DialogHeader>
<DialogTitle>
Edit profile<span className="text-primary">.</span>
</DialogTitle>
<DialogDescription>Change how your name and hub appear on the Lantern network.</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="profile-name">Display name</Label>
<Input id="profile-name" defaultValue="turtlewright" />
</div>
<div className="grid gap-2">
<Label htmlFor="profile-hub">Hub address</Label>
<Input id="profile-hub" defaultValue="lantern://turtlewright.hub" />
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="secondary">
Cancel
</Button>
</DialogClose>
<Button type="submit">Save changes</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}Installation#
With the CLI
$npx lanterncn add dialogThis also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/dialog. See Installation if your project is not set up yet.
Manually
Install the dependencies
npm install radix-ui lucide-reactCopy the source into your project
components/ui/dialog.tsx"use client"; import * as React from "react"; import { Dialog as DialogPrimitive } from "radix-ui"; import { XIcon } from "lucide-react"; import { cn } from "@/lib/utils"; 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 DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) { return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />; } function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) { return <DialogPrimitive.Close data-slot="dialog-close" {...props} />; } function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) { return ( <DialogPrimitive.Overlay data-slot="dialog-overlay" className={cn( "fixed inset-0 z-50 bg-[#080b0a]/75 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0", className, )} {...props} /> ); } function DialogContent({ className, children, showCloseButton = true, ...props }: React.ComponentProps<typeof DialogPrimitive.Content> & { showCloseButton?: boolean }) { return ( <DialogPortal> <DialogOverlay /> <DialogPrimitive.Content data-slot="dialog-content" className={cn( "fixed top-1/2 left-1/2 z-50 grid max-h-[calc(100dvh-2rem)] w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-5 overflow-y-auto rounded-lg border bg-popover p-6 text-popover-foreground shadow-block-sm duration-200 outline-none sm:max-w-lg", "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className, )} {...props} > {children} {showCloseButton && ( <DialogPrimitive.Close data-slot="dialog-close" className="absolute top-4 right-4 inline-flex size-7 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors outline-none hover:bg-secondary hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-popover disabled:pointer-events-none [&_svg]:size-4" > <XIcon /> <span className="sr-only">Close</span> </DialogPrimitive.Close> )} </DialogPrimitive.Content> </DialogPortal> ); } function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="dialog-header" className={cn("flex flex-col gap-2 pr-8 text-left", className)} {...props} /> ); } function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="dialog-footer" className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)} {...props} /> ); } function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) { return ( <DialogPrimitive.Title data-slot="dialog-title" className={cn("font-display text-xl leading-tight font-medium tracking-tight", className)} {...props} /> ); } function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) { return ( <DialogPrimitive.Description data-slot="dialog-description" className={cn("text-sm leading-relaxed text-muted-foreground", className)} {...props} /> ); } export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, };Update the import paths to match your project
The source imports
cnfrom@/lib/utils.
Usage#
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";<Dialog>
<DialogTrigger asChild>
<Button>Open</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit profile<span className="text-primary">.</span></DialogTitle>
<DialogDescription>Change how you appear on the network.</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>The orange period after the title is a Lantern touch you add in the title content. It is not forced by the component.
Pass showCloseButton={false} to DialogContent to hide the corner close button.
Examples#
Scrollable content
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
const rules = [
["Be kind in guestbooks", "Every hub has a guestbook. Sign it the way you would want yours signed."],
["No griefing turtles", "Turtles you do not own are off limits. Do not reprogram, trap or break them."],
["Keep servers light", "Pages should load on a basic computer. Avoid loops that never yield."],
["Credit your sources", "If you copy a program or a page layout, link back to the original hub."],
["One name per player", "Register a single display name. Alternate accounts are removed."],
["Report broken links", "Found a dead page? Flag it from the directory so the owner gets a note."],
["Mark adult content", "Hubs must be suitable for everyone on the server. Anything else is unlisted."],
["No spam hubs", "Hubs that only link to other hubs, or repeat the same page, are hidden."],
["Respect uptime", "Do not flood the relay. Scripts that ping more than once a second are throttled."],
["Have fun", "It is a block game. Build something odd and share it."],
];
export default function DialogScrollable() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="secondary">Read network rules</Button>
</DialogTrigger>
<DialogContent className="grid-rows-[auto_minmax(0,1fr)_auto] overflow-hidden p-0">
<DialogHeader className="border-b p-6 pr-14">
<DialogTitle>
Network rules<span className="text-primary">.</span>
</DialogTitle>
<DialogDescription>Read these before you publish your first hub.</DialogDescription>
</DialogHeader>
<ol className="grid gap-5 overflow-y-auto px-6">
{rules.map(([title, body], index) => (
<li key={title} className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1">
<span className="font-mono text-[11px] text-primary tabular-nums">{String(index + 1).padStart(2, "0")}</span>
<span className="text-sm font-medium">{title}</span>
<span className="col-start-2 text-sm leading-relaxed text-muted-foreground">{body}</span>
</li>
))}
</ol>
<DialogFooter className="border-t p-6 pt-4">
<DialogClose asChild>
<Button>I understand</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}