콘텐츠로 건너뛰기

대화상자

확인, 간단한 양식, 그리고 영구적인 작업에 대한 안내를 위한 모달 창 — 두꺼운 테두리와 뚜렷한 그림자가 적용된 상태로 페이지 위에 겹쳐져 있습니다.

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

다이얼로그는 페이지 위에 모달 창을 엽니다. 포커스는 안으로 이동하고, 뒤쪽 콘텐츠는 비활성화되며, 닫으면 원래 위치로 돌아옵니다. Base UI Dialog 프리미티브를 기반으로 하며, 네오브루탈리즘 공식(두꺼운 테두리, 선명한 그림자, 굵은 서체)에 맞춰 스타일을 입혔습니다.

다음과 같은 경우에 사용합니다.

  • 확인 — 삭제, 로그아웃, 플랜 다운그레이드처럼 한 번 더 멈춰야 하는 파괴적인 작업.
  • 짧은 폼 — 프로필 수정, 프로젝트 이름 바꾸기, 팀원 초대처럼 페이지를 떠나지 않고 끝내는 작업.
  • 집중형 상세 보기 — 이미지 미리보기, 라이선스 문구, 릴리스 노트처럼 별도 라우트가 필요 없는 내용.

설치

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

다음 의존성을 설치합니다.

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

다음 코드를 복사해 프로젝트에 붙여넣습니다.

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,
}

임포트 경로를 프로젝트 구성에 맞게 수정합니다.

사용법

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>

구성

Dialog 를 구축하려면 다음 구성을 사용합니다.

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

예제

커스텀 닫기 버튼

기본 닫기 컨트롤을 직접 만든 버튼으로 교체합니다.

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

닫기 버튼 없음

showCloseButton={false} 를 사용하면 닫기 버튼을 숨길 수 있습니다.

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

고정 푸터

콘텐츠가 스크롤되어도 작업을 표시된 상태로 유지합니다.

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

스크롤 가능한 콘텐츠

긴 콘텐츠는 헤더가 보이는 상태에서 스크롤할 수 있습니다.

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

RTL

Neobrutalism에서 RTL 지원을 활성화하는 방법은 RTL 설정 가이드를 참고하세요.

"use client"

import {

접근성

다이얼로그는 WAI-ARIA Dialog (Modal) 패턴을 따릅니다. 콘텐츠는 role="dialog"aria-modal로 렌더링되고, DialogTitle로 레이블되며 DialogDescription으로 설명됩니다. 열려 있는 동안 포커스는 내부에 가두어지고, 닫으면 트리거로 돌아옵니다.

키보드 상호작용:

동작
Space / Enter포커스된 트리거에서 다이얼로그 열기
Tab / Shift + Tab다이얼로그 안 다음/이전 요소로 포커스 이동(포커스 트랩)
Esc다이얼로그를 닫고 트리거로 포커스 복귀

API 참조

자세한 내용은 Base UI 문서를 참고하세요.