Overlays

Dialog.

A modal window over a dimmed backdrop, for short forms and focused reading. Focus is trapped inside and Escape closes it.

Installation#

With the CLI

$npx lanterncn add dialog

This 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

  1. Install the dependencies

    npm install radix-ui lucide-react
  2. Copy 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,
    };
  3. Update the import paths to match your project

    The source imports cn from @/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