컴포넌트
Dialog
현재 화면 위에서 입력이나 작업을 수행합니다.
모션
97.5% 크기에서 300ms 동안 부드럽게 나타나고, 200ms 동안 작아지며 사라집니다. 중심 위치는 유지합니다. 움직임 감소 설정에서는 전환을 생략합니다. 공통 모션 규칙을 따릅니다.
설치
pnpm dlx ply-ui add dialog button input공통 설치 설정을 먼저 적용합니다.
사용법
현재 화면 위에서 입력하거나 작업할 때 사용합니다. Title로 목적을 설명하고 open과 onOpenChange로 열림을 제어합니다. 예제의 저장 동작은 로컬 상태만 변경합니다.
구성
| 구성 요소 | 역할 |
|---|---|
Root | 열림 상태를 관리합니다. open과 onOpenChange로 제어할 수 있습니다. |
Trigger | 대화상자를 여는 버튼입니다. |
Portal | 대화상자와 배경을 별도 영역에 렌더합니다. |
Backdrop | 뒤 화면을 덮는 배경입니다. |
Popup | 내용과 포커스를 담습니다. size로 너비를 지정합니다. |
Title | 대화상자의 접근 가능한 이름입니다. |
Description | 대화상자의 목적이나 입력 안내입니다. |
Body | 본문 여백을 구성합니다. Popup의 직접 자식으로 둡니다. |
Footer | 하단 작업 버튼의 여백과 정렬을 구성합니다. |
Close | 닫기 동작을 연결한 버튼입니다. |
CloseAffordance | Popup 오른쪽 위의 닫기 아이콘 버튼입니다. |
크기 SM
Popup size="sm"의 최대 너비는 320px입니다. 화면이 좁으면 화면 너비에 맞게 줄어듭니다.
크기 MD
Popup size="md"의 최대 너비는 480px입니다. 화면이 좁으면 화면 너비에 맞게 줄어듭니다. 기본값입니다.
크기 LG
Popup size="lg"의 최대 너비는 640px입니다. 화면이 좁으면 화면 너비에 맞게 줄어듭니다.
닫기 아이콘
CloseAffordance는 오른쪽 위 닫기 버튼입니다. aria-label로 작업 이름을 지정합니다.
속성
이 프로젝트에서 추가하거나 제한한 속성입니다.
Popup
속성
타입
구현 코드
components/ui/dialog.tsx
"use client";
import type { ComponentProps } from "react";
import { Dialog as Primitive } from "@base-ui/react/dialog";
import { cx, withClassName } from "../../lib/cx";
import { Icon } from "./icon";
import "./dialog.css";
// 동작과 접근성은 Base UI가 담당합니다. 이 파일은 스타일 연결만 담당합니다.
function DialogBackdrop({
className,
...props
}: ComponentProps<typeof Primitive.Backdrop>) {
return (
<Primitive.Backdrop
{...props}
className={withClassName("rbx-backdrop", className)}
/>
);
}
export type DialogPopupProps = ComponentProps<typeof Primitive.Popup> & {
/** 대화상자 너비입니다. @defaultValue "md" */
size?: "sm" | "md" | "lg";
};
function DialogPopup({ size = "md", className, ...props }: DialogPopupProps) {
return (
<Primitive.Popup
{...props}
data-size={size}
className={withClassName("rbx-dialog", className)}
/>
);
}
function DialogTitle({
className,
...props
}: ComponentProps<typeof Primitive.Title>) {
return (
<Primitive.Title
{...props}
className={withClassName("rbx-dialog-title", className)}
/>
);
}
function DialogDescription({
className,
...props
}: ComponentProps<typeof Primitive.Description>) {
return (
<Primitive.Description
{...props}
className={withClassName("rbx-description", className)}
/>
);
}
function DialogTrigger({
className,
...props
}: ComponentProps<typeof Primitive.Trigger>) {
return (
<Primitive.Trigger
{...props}
className={withClassName("rbx-button", className)}
/>
);
}
function DialogClose({
className,
...props
}: ComponentProps<typeof Primitive.Close>) {
return (
<Primitive.Close
{...props}
className={withClassName("rbx-button", className)}
/>
);
}
function DialogCloseAffordance({
className,
...props
}: ComponentProps<typeof Primitive.Close>) {
return (
<Primitive.Close
aria-label="Close"
{...props}
className={withClassName("rbx-dialog-close-affordance", className)}
>
<Icon name="icon-regular-x" size={20} />
</Primitive.Close>
);
}
/** Body와 Footer를 사용하면 Popup이 여백을 중복해서 만들지 않습니다. */
function DialogBody({ className, ...props }: ComponentProps<"div">) {
return <div {...props} className={cx("rbx-dialog-body", className)} />;
}
function DialogFooter({ className, ...props }: ComponentProps<"div">) {
return <div {...props} className={cx("rbx-dialog-footer", className)} />;
}
// Root/Portal 등 스타일 없는 파트와 제네릭 API는 원본을 그대로 보존합니다.
export const Dialog = {
...Primitive,
Body: DialogBody,
CloseAffordance: DialogCloseAffordance,
Footer: DialogFooter,
Backdrop: DialogBackdrop,
Popup: DialogPopup,
Title: DialogTitle,
Description: DialogDescription,
Trigger: DialogTrigger,
Close: DialogClose,
};
components/ui/dialog.css
@import "./button.css";
/* Foundation Web Dialog: size controls width and 16/20px body/footer insets. */
.rbx-dialog {
--rbx-dialog-inset: 20px;
}
.rbx-dialog[data-size="sm"] {
width: min(320px, calc(100vw - 24px));
--rbx-dialog-inset: 16px;
padding: var(--rbx-dialog-inset);
}
.rbx-dialog[data-size="lg"] {
width: min(640px, calc(100vw - 24px));
}
.rbx-dialog:has(> .rbx-dialog-body) {
padding: 0;
}
.rbx-dialog-body {
padding: var(--rbx-dialog-inset);
}
.rbx-dialog-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 0 var(--rbx-dialog-inset) var(--rbx-dialog-inset);
}
.rbx-dialog-title {
font: var(--rbx-typography-heading-small-font);
letter-spacing: var(--rbx-typography-heading-small-letter-spacing);
}
.rbx-dialog-close-affordance {
position: absolute;
top: 12px;
right: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
padding: 8px;
border: 0;
border-radius: 9999px;
background: var(--rbx-color-over-media-100);
color: var(--rbx-color-content-emphasis);
cursor: pointer;
}
.rbx-dialog-close-affordance:hover {
background-image: linear-gradient(
var(--rbx-color-state-hover),
var(--rbx-color-state-hover)
);
}
.rbx-dialog:has(.rbx-dialog-close-affordance) .rbx-dialog-title {
padding-right: 36px;
}