Bỏ qua đến nội dung

Hộp thoại

Một cửa sổ bật lên dùng để xác nhận, điền biểu mẫu nhanh và hiển thị thông báo cảnh báo trước các thao tác có thể gây mất dữ liệu — hiển thị chồng lên trang với viền dày và bóng đổ đậm.

import { Button } from "@/components/ui/button"
import {
  Dialog,

Dialog mở một cửa sổ modal nằm trên trang — focus chuyển vào bên trong, phần nội dung phía sau bị vô hiệu hóa, và khi đóng lại bạn quay về đúng chỗ cũ. Được xây dựng trên primitive Base UI Dialog và tạo kiểu theo công thức neobrutalist: viền dày, bóng đổ đậm và kiểu chữ đậm.

Nên dùng khi:

  • Xác nhận hành động — xóa, đăng xuất, hạ cấp gói; bất cứ thao tác phá hủy nào cũng đáng được chặn lại một nhịp.
  • Biểu mẫu nhanh — sửa hồ sơ, đổi tên dự án, mời đồng đội mà không phải rời khỏi trang.
  • Xem chi tiết tập trung — xem trước ảnh, nội dung giấy phép, ghi chú phát hành; những thứ không cần một route riêng.

Cài đặt

pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/dialog.json
npx shadcn@latest add https://neobrutalism.com/r/base/dialog.json
yarn dlx shadcn@latest add https://neobrutalism.com/r/base/dialog.json
bunx --bun shadcn@latest add https://neobrutalism.com/r/base/dialog.json

Cài đặt các phụ thuộc sau:

pnpm add @base-ui/react
npm install @base-ui/react
yarn add @base-ui/react
bun add @base-ui/react

Sao chép và dán đoạn mã sau vào dự án của bạn.

components/ui/dialog.tsx
"use client"

import * as React from "react"
import { XIcon } from "lucide-react"
import { Dialog as DialogPrimitive } from "radix-ui"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"

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 isolate z-50 bg-foreground/20 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-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 w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded border-2 bg-popover p-4 text-sm text-popover-foreground shadow-md duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
          className
        )}
        {...props}
      >
        {children}
        {showCloseButton && (
          <DialogPrimitive.Close data-slot="dialog-close" asChild>
            <Button
              variant="ghost"
              className="absolute top-2 right-2"
              size="icon-sm"
            >
              <XIcon />
              <span className="sr-only">Close</span>
            </Button>
          </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", className)}
      {...props}
    />
  )
}

function DialogFooter({
  className,
  showCloseButton = false,
  children,
  ...props
}: React.ComponentProps<"div"> & {
  showCloseButton?: boolean
}) {
  return (
    <div
      data-slot="dialog-footer"
      className={cn(
        "-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t-2 bg-muted/50 p-4 sm:flex-row sm:justify-end",
        className
      )}
      {...props}
    >
      {children}
      {showCloseButton && (
        <DialogPrimitive.Close asChild>
          <Button variant="outline">Close</Button>
        </DialogPrimitive.Close>
      )}
    </div>
  )
}

function DialogTitle({
  className,
  ...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
  return (
    <DialogPrimitive.Title
      data-slot="dialog-title"
      className={cn(
        "font-head text-base leading-none font-medium",
        className
      )}
      {...props}
    />
  )
}

function DialogDescription({
  className,
  ...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
  return (
    <DialogPrimitive.Description
      data-slot="dialog-description"
      className={cn(
        "text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
        className
      )}
      {...props}
    />
  )
}

export {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogOverlay,
  DialogPortal,
  DialogTitle,
  DialogTrigger,
}

Cập nhật các đường dẫn import cho phù hợp với thiết lập dự án của bạn.

Cách dùng

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
<Dialog>
  <DialogTrigger>Open</DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Are you absolutely sure?</DialogTitle>
      <DialogDescription>
        This action cannot be undone. This will permanently delete your account
        and remove your data from our servers.
      </DialogDescription>
    </DialogHeader>
  </DialogContent>
</Dialog>

Kết hợp

Dùng cách kết hợp sau để xây dựng một Dialog:

Dialog
├── DialogTrigger
└── DialogContent
    ├── DialogHeader
    │   ├── DialogTitle
    │   └── DialogDescription
    └── DialogFooter

Ví dụ

Nút đóng tùy chỉnh

Thay thế điều khiển đóng mặc định bằng nút của riêng bạn.

import { Button } from "@/components/ui/button"
import {
  Dialog,

Không có nút đóng

Dùng showCloseButton={false} để ẩn nút đóng.

import { Button } from "@/components/ui/button"
import {
  Dialog,

Chân trang cố định

Giữ các hành động hiển thị trong khi nội dung cuộn.

import { Button } from "@/components/ui/button"
import {
  Dialog,

Nội dung có thể cuộn

Nội dung dài có thể cuộn trong khi tiêu đề vẫn hiển thị.

import { Button } from "@/components/ui/button"
import {
  Dialog,

RTL

Để bật hỗ trợ RTL trong Neobrutalism, hãy xem hướng dẫn cấu hình RTL.

"use client"

import {

Khả năng truy cập

Dialog tuân theo mẫu Dialog (Modal) của WAI-ARIA: phần nội dung render với role="dialog" cùng aria-modal, được gắn nhãn bởi DialogTitle và mô tả bởi DialogDescription, focus bị giữ lại bên trong khi dialog đang mở, và khi đóng thì focus trở về trigger.

Tương tác bàn phím:

PhímHành động
Space / EnterMở dialog từ trigger đang được focus
Tab / Shift + TabChuyển focus đến phần tử kế tiếp / trước đó bên trong dialog (focus bị giữ lại)
EscĐóng dialog và trả focus về trigger

Tham chiếu API

Xem tài liệu Base UI để biết thêm thông tin.