eo-n/ui

Dialog

Displays a modal window to capture user input or confirm actions.

import Image from "next/image";
 
import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
 
export function DialogDemo() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="outline">Open dialog</Button>} />
      <DialogContent className="sm:max-w-[425px]">
        <DialogHeader>
          <DialogTitle>Edit Content</DialogTitle>
          <DialogDescription>
            Update the details below and click save to apply changes.
          </DialogDescription>
        </DialogHeader>
        <div className="relative aspect-video w-full overflow-hidden">
          <Image
            src="/images/dialog-demo-image.jpg"
            alt="dialog-placeholder"
            priority
            className="rounded-md"
            objectFit="cover"
            fill
          />
        </div>
        <div className="flex flex-col gap-4">
          <div className="flex flex-col gap-3">
            <Label htmlFor="title">Title</Label>
            <Input id="title" value="Sample Title" className="col-span-3" />
          </div>
          <div className="flex flex-col gap-3">
            <Label htmlFor="description">Description</Label>
            <Input
              id="description"
              value="Sample Description"
              className="col-span-3"
            />
          </div>
        </div>
        <DialogFooter>
          <Button type="submit">Save changes</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

Installation

npx shadcn@latest add @eo-n/dialog

Usage

Import all parts and piece them together.

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
<Dialog>
  <DialogTrigger>Open</DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Are you sure you want to proceed?</DialogTitle>
      <DialogDescription>
        This action may have permanent effects. Please confirm if you want to
        continue.
      </DialogDescription>
    </DialogHeader>
  </DialogContent>
</Dialog>

Examples

With Custom Close Button

import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
 
export function DialogCustomCloseButton() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="outline">Open dialog</Button>} />
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Confirm Your Action</DialogTitle>
          <DialogDescription>
            This operation will update your settings and may affect your current
            configuration. Please confirm if you wish to proceed.
          </DialogDescription>
        </DialogHeader>
        <DialogFooter>
          <DialogClose render={<Button variant="outline">Cancel</Button>} />
          <Button variant="default">Confirm</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

Without Close Icon

import Link from "next/link";
import { Radius } from "lucide-react";
 
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
 
export function DialogWithNoXButton() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="default">Sign in</Button>} />
      <DialogContent hideCloseIcon className="sm:max-w-[415px]">
        <DialogHeader className="items-center px-10">
          <div className="mb-2 grid place-items-center rounded-full border-2 p-3">
            <Radius />
          </div>
          <DialogTitle>Welcome back</DialogTitle>
          <DialogDescription className="text-center">
            Enter your credentials to login to your account.
          </DialogDescription>
        </DialogHeader>
        <div className="flex flex-col gap-4">
          <div className="flex flex-col gap-3">
            <Label htmlFor="email">Email</Label>
            <Input
              id="email"
              autoComplete="off"
              placeholder="eo-n@gmail.com"
              className="col-span-3"
            />
          </div>
          <div className="flex flex-col gap-3">
            <Label htmlFor="password">Password</Label>
            <Input
              id="password"
              autoComplete="off"
              placeholder="*********"
              className="col-span-3"
            />
          </div>
          <div className="flex items-center justify-between">
            <div className="flex items-center space-x-2">
              <Checkbox id="remember-me" />
              <label
                htmlFor="remember-me"
                className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
              >
                Remember me
              </label>
            </div>
            <Link
              prefetch
              href="/"
              className="text-sm text-blue-400 underline underline-offset-4"
            >
              Forgot password?
            </Link>
          </div>
        </div>
        <DialogFooter className="gap-3 sm:flex-col">
          <Button variant="default" className="w-full">
            Sign in
          </Button>
          <div className="before:bg-border after:bg-border flex items-center gap-3 before:h-px before:flex-1 after:h-px after:flex-1">
            <span className="text-muted-foreground text-xs">Or</span>
          </div>
          <Button variant="outline" className="w-full [&_svg]:size-4">
            <svg
              xmlns="http://www.w3.org/2000/svg"
              viewBox="0 0 24 24"
              className="size-2"
            >
              <path
                d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
                fill="currentColor"
              />
            </svg>
            Login with Google
          </Button>
          <div className="text-muted-foreground text-center text-sm">
            Don&apos;t have an account?{" "}
            <Link
              prefetch
              href="/"
              className="text-blue-400 underline underline-offset-4"
            >
              Sign up
            </Link>
          </div>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

Nested Dialogs

import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
 
export function DialogNested() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="outline">Settings</Button>} />
      <DialogContent className="sm:max-w-[425px]">
        <DialogHeader>
          <DialogTitle>Account Settings</DialogTitle>
          <DialogDescription>
            Manage your account preferences.
          </DialogDescription>
        </DialogHeader>
        <div className="py-2">
          <div className="flex items-center justify-between">
            <span>Email notifications</span>
            <span>Enabled</span>
          </div>
        </div>
        <DialogFooter>
          <Dialog>
            <DialogTrigger render={<Button variant="ghost">Advanced</Button>} />
            <DialogContent className="sm:max-w-[425px]">
              <DialogHeader>
                <DialogTitle>Advanced Settings</DialogTitle>
                <DialogDescription>
                  Configure advanced options.
                </DialogDescription>
              </DialogHeader>
              <div className="py-2">
                <div className="flex items-center justify-between">
                  <span>Developer mode</span>
                  <span>Disabled</span>
                </div>
              </div>
              <DialogFooter>
                <DialogClose render={<Button>Save</Button>} />
              </DialogFooter>
            </DialogContent>
          </Dialog>
          <DialogClose render={<Button>Save</Button>} />
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

Scrollable Content

import { Button } from "@/components/ui/button";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
 
export function DialogScrollable() {
  return (
    <Dialog>
      <DialogTrigger
        render={<Button variant="outline">Terms of Service</Button>}
      />
      <DialogContent className="sm:max-w-[415px]" flush>
        <DialogHeader>
          <DialogTitle>Terms of Service</DialogTitle>
          <DialogDescription>
            Read the terms and conditions carefully.
          </DialogDescription>
        </DialogHeader>
        <ScrollArea className="my-2 mr-2 ml-4 flex-1">
          <p className="pb-4 text-sm font-semibold">1. Introduction</p>
          <p className="text-muted-foreground mb-4 text-sm leading-6">
            Welcome to our application. By accessing or using our service, you
            agree to be bound by these terms. If you do not agree to these
            terms, you may not use the service.
          </p>
          <p className="pb-4 text-sm font-semibold">2. User Accounts</p>
          <p className="text-muted-foreground mb-4 text-sm leading-6">
            You are responsible for maintaining the security of your account and
            password. The company cannot and will not be liable for any loss or
            damage from your failure to comply with this security obligation.
          </p>
          <p className="pb-4 text-sm font-semibold">3. Content</p>
          <p className="text-muted-foreground mb-4 text-sm leading-6">
            Our service allows you to post, link, store, share and otherwise
            make available certain information, text, graphics, videos, or other
            material. You are responsible for the content that you post to the
            service, including its legality, reliability, and appropriateness.
          </p>
          <p className="pb-4 text-sm font-semibold">4. Termination</p>
          <p className="text-muted-foreground mb-4 text-sm leading-6">
            We may terminate or suspend access to our service immediately,
            without prior notice or liability, for any reason whatsoever,
            including without limitation if you breach the Terms.
          </p>
          <p className="pb-4 text-sm font-semibold">5. Changes</p>
          <p className="text-muted-foreground text-sm leading-6">
            We reserve the right, at our sole discretion, to modify or replace
            these Terms at any time. If a revision is material we will try to
            provide at least 30 days notice prior to any new terms taking
            effect. What constitutes a material change will be determined at our
            sole discretion.
          </p>
        </ScrollArea>
        <DialogFooter>
          <Button type="submit">Accept terms</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

Open On Menu

In order to open a dialog using a menu, control the dialog state and open it imperatively using the onClick handler on the menu item.

Return Focus Properly

Make sure to also use the dialog’s finalFocus prop to return focus back to the menu trigger.

import * as React from "react";

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "../ui/dialog";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuPositioner,
  DropdownMenuTrigger,
} from "../ui/dropdown-menu";

export default function DropdownMenuRadio() {
  const menuTriggerRef = React.useRef<HTMLButtonElement>(null);
  const [dialogOpen, setDialogOpen] = React.useState(false);

  return (
    <React.Fragment>
      <DropdownMenu>
        {/* Set the trigger ref */}
        <DropdownMenuTrigger ref={menuTriggerRef}>Open</DropdownMenuTrigger>
        <DropdownMenuPositioner>
          <DropdownMenuContent>
            {/* Open the dialog when the menu item is clicked */}
            <DropdownMenuItem onClick={() => setDialogOpen(true)}>
              Open dialog
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenuPositioner>
      </DropdownMenu>
      {/* Control the dialog state */}
      <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
        {/* Return focus to the menu trigger when the dialog is closed */}
        <DialogContent finalFocus={menuTriggerRef}>
          <DialogHeader>
            <DialogTitle>Are you sure you want to proceed?</DialogTitle>
            <DialogDescription>
              This action may have permanent effects. Please confirm if you want
              to continue.
            </DialogDescription>
          </DialogHeader>
        </DialogContent>
      </Dialog>
    </React.Fragment>
  );
}

API Reference

Content

Contains content to be rendered in the open dialog.

PropTypeDefault
hideCloseIcon?
boolean
false
flush?
boolean
false