Dialog
화면 중앙에 떠서 사용자의 주의를 모으는 다이얼로그입니다. 제목, 본문, 푸터와 닫기 버튼을 갖추어 폼이나 스크롤되는 콘텐츠 등 풍부한 내용을 담을 때 사용합니다.
import { HStack, Text } from "@seed-design/react";
import {
DialogAction,
DialogBody,
DialogContent,
DialogFooter,
DialogRoot,
DialogTrigger,
} from "seed-design/ui/dialog";
import { ActionButton } from "seed-design/ui/action-button";
const DialogPreview = () => {
return (
<DialogRoot>
<DialogTrigger asChild>
<ActionButton variant="neutralSolid">Open Dialog</ActionButton>
</DialogTrigger>
<DialogContent title="제목" description="설명을 작성할 수 있어요">
<DialogBody>
<Text textStyle="articleBody">
본문에는 사용자가 확인해야 할 내용이나 추가 입력 폼을 배치할 수 있습니다.
</Text>
</DialogBody>
<DialogFooter>
<HStack gap="x2" justify="flex-end">
<DialogAction variant="neutralWeak">취소</DialogAction>
<DialogAction variant="neutralSolid">확인</DialogAction>
</HStack>
</DialogFooter>
</DialogContent>
</DialogRoot>
);
};
export default DialogPreview;Alert Dialog
사용자의 확인이나 경고가 목적이라면 Alert Dialog를 사용하세요.
Installation
Default
화면 중앙에 떠서 제목, 본문, 푸터를 담는 기본 Dialog 컴포넌트를 포함합니다.
npx @seed-design/cli@latest add ui:dialogpnpm dlx @seed-design/cli@latest add ui:dialogyarn dlx @seed-design/cli@latest add ui:dialogbun x @seed-design/cli@latest add ui:dialog의존성 설치
npm install @karrotmarket/react-monochrome-icon @seed-design/reactyarn add @karrotmarket/react-monochrome-icon @seed-design/reactpnpm add @karrotmarket/react-monochrome-icon @seed-design/reactbun add @karrotmarket/react-monochrome-icon @seed-design/react아래 코드를 복사 후 붙여넣고 사용하세요
/**
* @file ui:dialog
* @requires @seed-design/react@^2.1.0
* @requires @seed-design/css@^2.3.0
**/
"use client";
import IconXmarkLine from "@karrotmarket/react-monochrome-icon/IconXmarkLine";
import { ContentDialog, Icon } from "@seed-design/react";
import { forwardRef } from "react";
import { ActionButton, type ActionButtonProps } from "./action-button";
import type * as React from "react";
export interface DialogRootProps extends ContentDialog.RootProps {
/**
* @default false
*/
closeOnInteractOutside?: ContentDialog.RootProps["closeOnInteractOutside"];
}
/**
* @see https://seed-design.io/react/components/dialog
*/
export const DialogRoot = (props: DialogRootProps) => {
return <ContentDialog.Root closeOnInteractOutside={false} {...props} />;
};
DialogRoot.displayName = "DialogRoot";
export interface DialogTriggerProps extends ContentDialog.TriggerProps {}
export const DialogTrigger = ContentDialog.Trigger;
export interface DialogContentProps extends Omit<ContentDialog.ContentProps, "title"> {
title?: React.ReactNode;
description?: React.ReactNode;
layerIndex?: number;
/**
* @default true
*/
showCloseButton?: boolean;
}
export const DialogContent = forwardRef<HTMLDivElement, DialogContentProps>(
({ children, title, description, layerIndex, showCloseButton = true, ...otherProps }, ref) => {
if (
!title &&
!otherProps["aria-labelledby"] &&
!otherProps["aria-label"] &&
process.env.NODE_ENV !== "production"
) {
console.warn(
"DialogContent: aria-labelledby or aria-label should be provided if title is not provided.",
);
}
const shouldRenderHeader = title || description || showCloseButton;
return (
<ContentDialog.Positioner style={{ "--layer-index": layerIndex } as React.CSSProperties}>
<ContentDialog.Backdrop />
<ContentDialog.Content ref={ref} {...otherProps}>
{shouldRenderHeader && (
<ContentDialog.Header>
{title && <ContentDialog.Title>{title}</ContentDialog.Title>}
{description && <ContentDialog.Description>{description}</ContentDialog.Description>}
{showCloseButton && (
<ContentDialog.CloseButton aria-label="닫기">
<Icon svg={<IconXmarkLine />} />
</ContentDialog.CloseButton>
)}
</ContentDialog.Header>
)}
{children}
</ContentDialog.Content>
</ContentDialog.Positioner>
);
},
);
DialogContent.displayName = "DialogContent";
export interface DialogBodyProps extends ContentDialog.BodyProps {}
export const DialogBody = ContentDialog.Body;
export interface DialogFooterProps extends ContentDialog.FooterProps {}
export const DialogFooter = ContentDialog.Footer;
export interface DialogActionProps
extends Omit<ContentDialog.ActionProps, "color">,
ActionButtonProps {}
export const DialogAction = forwardRef<HTMLButtonElement, DialogActionProps>((props, ref) => {
return (
<ContentDialog.Action asChild>
<ActionButton {...props} ref={ref} />
</ContentDialog.Action>
);
});
DialogAction.displayName = "DialogAction";
/**
* This file is a snippet from SEED Design, helping you get started quickly with @seed-design/* packages.
* You can extend this snippet however you want.
*/
Responsive
뷰포트에 따라 Bottom Sheet로 자동 전환되는 반응형 변형입니다.
npx @seed-design/cli@latest add ui:responsive-dialogpnpm dlx @seed-design/cli@latest add ui:responsive-dialogyarn dlx @seed-design/cli@latest add ui:responsive-dialogbun x @seed-design/cli@latest add ui:responsive-dialog의존성 설치
npm install @karrotmarket/react-monochrome-icon @seed-design/reactyarn add @karrotmarket/react-monochrome-icon @seed-design/reactpnpm add @karrotmarket/react-monochrome-icon @seed-design/reactbun add @karrotmarket/react-monochrome-icon @seed-design/react아래 코드를 복사 후 붙여넣고 사용하세요
/**
* @file ui:responsive-dialog
* @requires @seed-design/react@^2.1.0
* @requires @seed-design/css@^2.3.0
**/
"use client";
import IconXmarkLine from "@karrotmarket/react-monochrome-icon/IconXmarkLine";
import {
Icon,
ResponsiveDialog as SeedResponsiveDialog,
useResponsiveDialogContext,
VisuallyHidden,
} from "@seed-design/react";
import type * as React from "react";
import { forwardRef } from "react";
import { ActionButton, type ActionButtonProps } from "./action-button";
export interface ResponsiveDialogRootProps extends SeedResponsiveDialog.RootProps {}
/**
* @see https://seed-design.io/react/components/dialog
*/
export const ResponsiveDialogRoot = ({
dialogRootProps,
...otherProps
}: ResponsiveDialogRootProps) => {
return (
<SeedResponsiveDialog.Root
dialogRootProps={{ closeOnInteractOutside: false, ...dialogRootProps }}
{...otherProps}
/>
);
};
ResponsiveDialogRoot.displayName = "ResponsiveDialogRoot";
export interface ResponsiveDialogTriggerProps extends SeedResponsiveDialog.TriggerProps {}
export const ResponsiveDialogTrigger = SeedResponsiveDialog.Trigger;
export interface ResponsiveDialogContentProps
extends Omit<SeedResponsiveDialog.ContentProps, "title"> {
title?: React.ReactNode;
description?: React.ReactNode;
layerIndex?: number;
/**
* @default true
*/
showCloseButton?: boolean;
/**
* @default false
*/
showHandle?: boolean;
}
export const ResponsiveDialogContent = forwardRef<HTMLDivElement, ResponsiveDialogContentProps>(
(
{
children,
title,
description,
layerIndex,
showCloseButton = true,
showHandle = false,
...otherProps
},
ref,
) => {
const { shouldUseBottomSheet } = useResponsiveDialogContext();
if (
!title &&
!otherProps["aria-labelledby"] &&
!otherProps["aria-label"] &&
process.env.NODE_ENV !== "production"
) {
console.warn(
"ResponsiveDialogContent: aria-labelledby or aria-label should be provided if title is not provided.",
);
}
if (shouldUseBottomSheet) {
const shouldRenderHeader = title || description;
return (
<SeedResponsiveDialog.Positioner
style={{ "--layer-index": layerIndex } as React.CSSProperties}
>
<SeedResponsiveDialog.Backdrop />
<SeedResponsiveDialog.Content ref={ref} {...otherProps}>
{showHandle && <SeedResponsiveDialog.Handle />}
{shouldRenderHeader && (
<SeedResponsiveDialog.Header>
{title ? (
<SeedResponsiveDialog.Title>{title}</SeedResponsiveDialog.Title>
) : (
<VisuallyHidden asChild>
<SeedResponsiveDialog.Title>
{otherProps["aria-label"] || ""}
</SeedResponsiveDialog.Title>
</VisuallyHidden>
)}
{description && (
<SeedResponsiveDialog.Description>{description}</SeedResponsiveDialog.Description>
)}
</SeedResponsiveDialog.Header>
)}
{children}
{showCloseButton && (
<SeedResponsiveDialog.CloseButton aria-label="닫기">
<Icon svg={<IconXmarkLine />} />
</SeedResponsiveDialog.CloseButton>
)}
</SeedResponsiveDialog.Content>
</SeedResponsiveDialog.Positioner>
);
}
const shouldRenderHeader = title || description || showCloseButton;
return (
<SeedResponsiveDialog.Positioner
style={{ "--layer-index": layerIndex } as React.CSSProperties}
>
<SeedResponsiveDialog.Backdrop />
<SeedResponsiveDialog.Content ref={ref} {...otherProps}>
{shouldRenderHeader && (
<SeedResponsiveDialog.Header>
{title && <SeedResponsiveDialog.Title>{title}</SeedResponsiveDialog.Title>}
{description && (
<SeedResponsiveDialog.Description>{description}</SeedResponsiveDialog.Description>
)}
{showCloseButton && (
<SeedResponsiveDialog.CloseButton aria-label="닫기">
<Icon svg={<IconXmarkLine />} />
</SeedResponsiveDialog.CloseButton>
)}
</SeedResponsiveDialog.Header>
)}
{children}
</SeedResponsiveDialog.Content>
</SeedResponsiveDialog.Positioner>
);
},
);
ResponsiveDialogContent.displayName = "ResponsiveDialogContent";
export interface ResponsiveDialogBodyProps extends SeedResponsiveDialog.BodyProps {}
export const ResponsiveDialogBody = SeedResponsiveDialog.Body;
export interface ResponsiveDialogFooterProps extends SeedResponsiveDialog.FooterProps {}
export const ResponsiveDialogFooter = SeedResponsiveDialog.Footer;
export interface ResponsiveDialogActionProps
extends Omit<SeedResponsiveDialog.ActionProps, "color">,
ActionButtonProps {}
export const ResponsiveDialogAction = forwardRef<HTMLButtonElement, ResponsiveDialogActionProps>(
(props, ref) => {
return (
<SeedResponsiveDialog.Action asChild>
<ActionButton {...props} ref={ref} />
</SeedResponsiveDialog.Action>
);
},
);
ResponsiveDialogAction.displayName = "ResponsiveDialogAction";
/**
* This file is a snippet from SEED Design, helping you get started quickly with @seed-design/* packages.
* You can extend this snippet however you want.
*/
Props
DialogRoot
Prop
Type
childrenReact.ReactNodeopen?boolean | undefineddefaultOpen?boolean | undefinedonOpenChange?((open: boolean, details?: DialogChangeDetails) => void) | undefinedDialogTrigger
Prop
Type
DialogContent
Prop
Type
title?React.ReactNodedescription?React.ReactNodelayerIndex?number | undefinedwidth?ResponsiveValue<(string & {}) | Dimension | "spacingX.betweenChips" | "spacingX.globalGutter" | "spacingY.componentDefault" | "spacingY.navToTitle" | "spacingY.screenBottom" | "spacingY.betweenText" | "full"> | undefinedmaxWidth?ResponsiveValue<(string & {}) | Dimension | "spacingX.betweenChips" | "spacingX.globalGutter" | "spacingY.componentDefault" | "spacingY.navToTitle" | "spacingY.screenBottom" | "spacingY.betweenText" | "full"> | undefinedDialogBody
Prop
Type
paddingX?ResponsiveValue<0 | (string & {}) | Dimension | "spacingX.betweenChips" | "spacingX.globalGutter" | "spacingY.componentDefault" | "spacingY.navToTitle" | "spacingY.screenBottom" | "spacingY.betweenText"> | undefinedminHeight?ResponsiveValue<(string & {}) | Dimension | "spacingX.betweenChips" | "spacingX.globalGutter" | "spacingY.componentDefault" | "spacingY.navToTitle" | "spacingY.screenBottom" | "spacingY.betweenText" | "full"> | undefinedmaxHeight?ResponsiveValue<(string & {}) | Dimension | "spacingX.betweenChips" | "spacingX.globalGutter" | "spacingY.componentDefault" | "spacingY.navToTitle" | "spacingY.screenBottom" | "spacingY.betweenText" | "full"> | undefinedjustifyContent?"flex-start" | "flex-end" | "center" | "space-between" | "space-around" | undefinedalignItems?"flex-start" | "flex-end" | "center" | "stretch" | undefinedDialogFooter
Prop
Type
DialogAction
Prop
Type
ResponsiveDialogRoot
Prop
Type
children?React.ReactNodeopen?boolean | undefineddefaultOpen?boolean | undefinedonOpenChange?((open: boolean) => void) | undefinedExamples
Trigger
<DialogTrigger>는 aria-haspopup="dialog" 속성을 설정하고, Dialog의 open 상태에 따라 aria-expanded 속성을 자동으로 설정합니다. 이 속성은 스크린 리더와 같은 보조 기술에 유용합니다.
import { HStack, Text } from "@seed-design/react";
import {
DialogAction,
DialogBody,
DialogContent,
DialogFooter,
DialogRoot,
DialogTrigger,
} from "seed-design/ui/dialog";
import { ActionButton } from "seed-design/ui/action-button";
const DialogTriggerExample = () => {
return (
<DialogRoot>
<DialogTrigger asChild>
<ActionButton variant="neutralSolid">Open</ActionButton>
</DialogTrigger>
<DialogContent title="Trigger 패턴">
<DialogBody>
<Text textStyle="articleBody">Trigger를 클릭하면 현재 화면 위에 Dialog가 열립니다.</Text>
</DialogBody>
<DialogFooter>
<HStack gap="x2" justify="flex-end">
<DialogAction variant="neutralWeak">취소</DialogAction>
<DialogAction variant="neutralSolid">확인</DialogAction>
</HStack>
</DialogFooter>
</DialogContent>
</DialogRoot>
);
};
export default DialogTriggerExample;Controlled
Trigger 외의 방식으로 Dialog를 열고 닫을 수 있습니다. 이 경우 open prop을 사용하여 Dialog의 상태를 제어합니다.
import { HStack, Text } from "@seed-design/react";
import { useState } from "react";
import { ActionButton } from "seed-design/ui/action-button";
import {
DialogAction,
DialogBody,
DialogContent,
DialogFooter,
DialogRoot,
} from "seed-design/ui/dialog";
const DialogControlled = () => {
const [open, setOpen] = useState(false);
return (
<>
<ActionButton variant="neutralSolid" onClick={() => setOpen(true)}>
열기
</ActionButton>
<DialogRoot open={open} onOpenChange={setOpen}>
<DialogContent title="제목" description="설명을 작성할 수 있어요">
<DialogBody>
<Text textStyle="articleBody">
Labore do culpa dolore irure nisi dolor dolor laboris veniam ipsum excepteur
adipisicing laboris non quis. Velit ea ut minim. Magna dolore culpa velit incididunt
consequat sint. Fugiat ad culpa labore dolore esse dolore ex aliquip duis aute aliquip
ad velit et.
</Text>
</DialogBody>
<DialogFooter>
<HStack gap="x2" justify="flex-end">
<DialogAction variant="neutralWeak">취소</DialogAction>
<DialogAction variant="neutralSolid">확인</DialogAction>
</HStack>
</DialogFooter>
</DialogContent>
</DialogRoot>
</>
);
};
export default DialogControlled;Size
<DialogRoot>에 size prop을 설정하여 Dialog의 너비를 변경할 수 있습니다. "medium" (480px, 기본값)과 "large" (800px)를 지원합니다.
md 미만 화면에서는 뷰포트 너비의 90%를 차지하고, md 이상에서 각 size의 최대 너비가 적용됩니다. content의 높이는 뷰포트의 80%로 제한됩니다.
<DialogContent>에 width, maxWidth prop을 전달하여 너비를 직접 제어할 수도 있습니다.
프리셋 size 대신 뷰포트 기반의 유동적인 너비가 필요할 때 사용합니다.
단일 값은 모든 breakpoint에 적용되므로, md 미만의 기본 너비(뷰포트의 90%)를 유지하려면 width={{ md: "560px" }}처럼 breakpoint 객체로 전달하세요.
import { Flex, HStack, Text, VStack } from "@seed-design/react";
import { useState } from "react";
import { ActionButton } from "seed-design/ui/action-button";
import {
DialogAction,
DialogBody,
DialogContent,
DialogFooter,
DialogRoot,
DialogTrigger,
} from "seed-design/ui/dialog";
import { Slider } from "seed-design/ui/slider";
const DialogSize = () => {
const [width, setWidth] = useState(90);
const [maxWidth, setMaxWidth] = useState(640);
return (
<Flex gap="x3" wrap="wrap">
<DialogRoot size="medium">
<DialogTrigger asChild>
<ActionButton variant="neutralSolid">Medium (480px)</ActionButton>
</DialogTrigger>
<DialogContent title="Medium Dialog">
<DialogBody>
<Text textStyle="articleBody">
기본 너비로 상세 정보와 주요 액션을 함께 제공합니다.
</Text>
</DialogBody>
<DialogFooter>
<HStack gap="x2" justify="flex-end">
<DialogAction variant="neutralWeak">취소</DialogAction>
<DialogAction variant="neutralSolid">확인</DialogAction>
</HStack>
</DialogFooter>
</DialogContent>
</DialogRoot>
<DialogRoot size="large">
<DialogTrigger asChild>
<ActionButton variant="neutralSolid">Large (800px)</ActionButton>
</DialogTrigger>
<DialogContent title="Large Dialog">
<DialogBody>
<Text textStyle="articleBody">
넓은 다이얼로그에서 더 많은 폼 필드나 상세 콘텐츠를 다룹니다.
</Text>
</DialogBody>
<DialogFooter>
<HStack gap="x2" justify="flex-end">
<DialogAction variant="neutralWeak">취소</DialogAction>
<DialogAction variant="neutralSolid">확인</DialogAction>
</HStack>
</DialogFooter>
</DialogContent>
</DialogRoot>
<DialogRoot>
<DialogTrigger asChild>
<ActionButton variant="neutralSolid">Custom (조절 가능)</ActionButton>
</DialogTrigger>
<DialogContent title="Custom Size" width={`${width}vw`} maxWidth={`${maxWidth}px`}>
<DialogBody>
<VStack gap="x4">
<Text textStyle="articleBody">
뷰포트 너비에 따라 유동적으로 커지되 maxWidth로 최대 크기를 제한합니다.
</Text>
<Slider
label="width (vw)"
min={50}
max={100}
values={[width]}
onValuesChange={(values) => setWidth(values[0])}
/>
<Slider
label="maxWidth (px)"
min={320}
max={800}
values={[maxWidth]}
onValuesChange={(values) => setMaxWidth(values[0])}
/>
</VStack>
</DialogBody>
<DialogFooter>
<HStack gap="x2" justify="flex-end">
<DialogAction variant="neutralWeak">취소</DialogAction>
<DialogAction variant="neutralSolid">확인</DialogAction>
</HStack>
</DialogFooter>
</DialogContent>
</DialogRoot>
</Flex>
);
};
export default DialogSize;Body
<DialogBody>는 본문 영역을 스크롤 가능하게 만듭니다. 본문이 길어 스크롤되면 헤더 아래에 구분선이 나타나고, 하단은 서서히 사라지는 마스크가 적용됩니다.
Body가 뷰포트 높이를 넘겨 스크롤이 생길 때에만 하단 fade 마스크와 padding-bottom이 적용됩니다. 본문이 짧아 넘치지 않으면 마스크가 적용되지 않아 마지막 줄이 흐려지지 않습니다.
import { HStack, Text, VStack } from "@seed-design/react";
import { ActionButton } from "seed-design/ui/action-button";
import {
DialogAction,
DialogBody,
DialogContent,
DialogFooter,
DialogRoot,
DialogTrigger,
} from "seed-design/ui/dialog";
const DialogBodyExample = () => {
return (
<VStack gap="x3" align="stretch">
<DialogRoot size="medium">
<DialogTrigger asChild>
<ActionButton variant="neutralWeak">짧은 본문 (fade 없음)</ActionButton>
</DialogTrigger>
<DialogContent
title="짧은 본문"
description="Body가 넘치지 않으면 하단 fade와 padding-bottom이 적용되지 않습니다"
>
<DialogBody>
<Text textStyle="articleBody">
내용이 짧아 스크롤이 없으면 하단 마스크가 적용되지 않아, 마지막 줄이 흐려지지
않습니다.
</Text>
</DialogBody>
<DialogFooter>
<HStack gap="x2" justify="flex-end">
<DialogAction variant="neutralWeak">취소</DialogAction>
<DialogAction variant="neutralSolid">확인</DialogAction>
</HStack>
</DialogFooter>
</DialogContent>
</DialogRoot>
<DialogRoot size="medium">
<DialogTrigger asChild>
<ActionButton variant="neutralSolid">긴 본문 (fade 적용)</ActionButton>
</DialogTrigger>
<DialogContent
title="긴 본문"
description="Body가 넘쳐 스크롤되면 하단이 서서히 사라집니다"
>
<DialogBody>
<VStack gap="x4" align="stretch">
{Array.from({ length: 16 }, (_, index) => (
<Text key={index} textStyle="articleBody">
{index + 1}. Body가 넘치면 하단에 fade 마스크와 padding-bottom이 적용되고,
스크롤하면 헤더 아래에 구분선이 나타납니다.
</Text>
))}
</VStack>
</DialogBody>
<DialogFooter>
<HStack gap="x2" justify="flex-end">
<DialogAction variant="neutralWeak">취소</DialogAction>
<DialogAction variant="neutralSolid">확인</DialogAction>
</HStack>
</DialogFooter>
</DialogContent>
</DialogRoot>
</VStack>
);
};
export default DialogBodyExample;Custom Body
<DialogBody>에 paddingX, minHeight, maxHeight, justifyContent, alignItems prop을 전달하여 본문 영역을 직접 제어할 수 있습니다.
기본 캡(뷰포트의 80%)보다 낮게 스크롤 높이를 제한하거나, 짧은 내용에서도 높이를 고정하거나, 가로 패딩을 제거해 콘텐츠를 가장자리까지 배치할 때 사용합니다.
import { Box, Flex, HStack, Text, VStack } from "@seed-design/react";
import { ActionButton } from "seed-design/ui/action-button";
import {
DialogAction,
DialogBody,
DialogContent,
DialogFooter,
DialogRoot,
DialogTrigger,
} from "seed-design/ui/dialog";
const DialogCustomBody = () => {
return (
<Flex gap="x3" wrap="wrap">
<DialogRoot>
<DialogTrigger asChild>
<ActionButton variant="neutralSolid">maxHeight 200px</ActionButton>
</DialogTrigger>
<DialogContent
title="본문 최대 높이"
description="본문(Body)의 스크롤 높이를 200px로 제한합니다"
>
<DialogBody maxHeight="200px">
<VStack gap="x4" align="stretch">
{Array.from({ length: 12 }, (_, index) => (
<Text key={index} textStyle="articleBody">
{index + 1}. 본문이 200px을 넘으면 그 안에서 스크롤됩니다.
</Text>
))}
</VStack>
</DialogBody>
<DialogFooter>
<HStack gap="x2" justify="flex-end">
<DialogAction variant="neutralWeak">취소</DialogAction>
<DialogAction variant="neutralSolid">확인</DialogAction>
</HStack>
</DialogFooter>
</DialogContent>
</DialogRoot>
<DialogRoot>
<DialogTrigger asChild>
<ActionButton variant="neutralSolid">minHeight + 가운데 정렬</ActionButton>
</DialogTrigger>
<DialogContent title="빈 상태" description="짧은 내용에서도 높이를 고정합니다">
<DialogBody minHeight="240px" justifyContent="center" alignItems="center">
<Text textStyle="articleBody" color="fg.neutralMuted">
아직 항목이 없습니다
</Text>
</DialogBody>
<DialogFooter>
<HStack gap="x2" justify="flex-end">
<DialogAction variant="neutralWeak">취소</DialogAction>
<DialogAction variant="neutralSolid">추가</DialogAction>
</HStack>
</DialogFooter>
</DialogContent>
</DialogRoot>
<DialogRoot>
<DialogTrigger asChild>
<ActionButton variant="neutralSolid">paddingX 0</ActionButton>
</DialogTrigger>
<DialogContent
title="Full Bleed"
description="가로 패딩을 제거해 콘텐츠를 가장자리까지 배치합니다"
>
<DialogBody paddingX={0}>
<Box bg="palette.gray200" paddingY="x8">
<Text textStyle="articleBody">가장자리까지 닿는 영역입니다</Text>
</Box>
</DialogBody>
<DialogFooter>
<HStack gap="x2" justify="flex-end">
<DialogAction variant="neutralWeak">취소</DialogAction>
<DialogAction variant="neutralSolid">확인</DialogAction>
</HStack>
</DialogFooter>
</DialogContent>
</DialogRoot>
</Flex>
);
};
export default DialogCustomBody;Show Close Button
<DialogContent>에 showCloseButton prop을 전달하여 우측 상단 닫기 버튼을 표시할 수 있습니다.
기본 값은 true입니다.
import { Flex, HStack, Text } from "@seed-design/react";
import { ActionButton } from "seed-design/ui/action-button";
import {
DialogAction,
DialogBody,
DialogContent,
DialogFooter,
DialogRoot,
DialogTrigger,
} from "seed-design/ui/dialog";
const DialogShowCloseButton = () => {
return (
<Flex gap="x3">
<DialogRoot>
<DialogTrigger asChild>
<ActionButton variant="neutralSolid">닫기 버튼 있음</ActionButton>
</DialogTrigger>
<DialogContent title="닫기 버튼" showCloseButton>
<DialogBody>
<Text textStyle="articleBody">기본적으로 우측 상단에 닫기 버튼이 표시됩니다.</Text>
</DialogBody>
<DialogFooter>
<HStack gap="x2" justify="flex-end">
<DialogAction variant="neutralWeak">취소</DialogAction>
<DialogAction variant="neutralSolid">확인</DialogAction>
</HStack>
</DialogFooter>
</DialogContent>
</DialogRoot>
<DialogRoot>
<DialogTrigger asChild>
<ActionButton variant="neutralSolid">닫기 버튼 없음</ActionButton>
</DialogTrigger>
<DialogContent title="닫기 버튼 없음" showCloseButton={false}>
<DialogBody>
<Text textStyle="articleBody">
닫기 버튼을 숨길 때는 본문이나 푸터에 닫을 수 있는 액션을 제공하세요.
</Text>
</DialogBody>
<DialogFooter>
<HStack gap="x2" justify="flex-end">
<DialogAction variant="neutralWeak">취소</DialogAction>
<DialogAction variant="neutralSolid">확인</DialogAction>
</HStack>
</DialogFooter>
</DialogContent>
</DialogRoot>
</Flex>
);
};
export default DialogShowCloseButton;Footer Layout
DialogFooter는 flex 레이아웃만 제공하며, 버튼 배치는 VStack, HStack 등으로 직접 구성합니다.
import { HStack, Text, VStack } from "@seed-design/react";
import { ActionButton } from "seed-design/ui/action-button";
import {
DialogAction,
DialogBody,
DialogContent,
DialogFooter,
DialogRoot,
DialogTrigger,
} from "seed-design/ui/dialog";
const DialogFooterLayout = () => {
return (
<DialogRoot size="medium">
<DialogTrigger asChild>
<ActionButton variant="neutralSolid">Footer 레이아웃</ActionButton>
</DialogTrigger>
<DialogContent
title="Footer 레이아웃"
description="버튼 배치는 VStack, HStack 등으로 직접 구성합니다."
>
<DialogBody>
<VStack gap="x3" align="stretch">
<Text textStyle="articleBody">DialogFooter는 flex 레이아웃만 제공합니다.</Text>
<Text textStyle="articleBody">
넓은 다이얼로그에서는 주요 액션을 우측에 가로로 정렬할 수 있습니다.
</Text>
</VStack>
</DialogBody>
<DialogFooter>
<HStack gap="x2" justify="flex-end">
<DialogAction variant="neutralWeak">취소</DialogAction>
<DialogAction variant="neutralSolid">확인</DialogAction>
</HStack>
</DialogFooter>
</DialogContent>
</DialogRoot>
);
};
export default DialogFooterLayout;Prevent Close
DialogAction의 onClick에서 e.preventDefault()를 호출하면 다이얼로그가 닫히지 않습니다.
import { HStack } from "@seed-design/react";
import { useState } from "react";
import { ActionButton } from "seed-design/ui/action-button";
import {
DialogAction,
DialogBody,
DialogContent,
DialogFooter,
DialogRoot,
DialogTrigger,
} from "seed-design/ui/dialog";
import { Switch } from "seed-design/ui/switch";
const DialogPreventClose = () => {
const [preventClose, setPreventClose] = useState(true);
return (
<DialogRoot>
<DialogTrigger asChild>
<ActionButton variant="neutralSolid">열기</ActionButton>
</DialogTrigger>
<DialogContent
title="닫기 방지"
description="확인 버튼을 눌러도 Dialog가 닫히지 않도록 설정할 수 있습니다."
>
<DialogBody alignItems="flex-start">
<Switch
size="16"
tone="neutral"
label="preventDefault 사용"
checked={preventClose}
onCheckedChange={setPreventClose}
/>
</DialogBody>
<DialogFooter>
<HStack gap="x2" justify="flex-end">
<DialogAction variant="neutralWeak">취소</DialogAction>
<DialogAction
variant="neutralSolid"
onClick={(e) => {
if (preventClose) {
e.preventDefault();
}
}}
>
확인
</DialogAction>
</HStack>
</DialogFooter>
</DialogContent>
</DialogRoot>
);
};
export default DialogPreventClose;onOpenChange Details
onOpenChange 두 번째 인자로 details가 제공됩니다.
reason
열릴 때 (open: true)
"trigger":DialogTrigger로 열림
닫힐 때 (open: false)
"closeButton":DialogAction또는 우측 상단 닫기 버튼으로 닫힘"escapeKeyDown": ESC 키 사용"interactOutside": 외부 영역 클릭DialogRoot는 기본적으로closeOnInteractOutside={false}입니다.interactOutside는 이 옵션을true로 설정한 경우에만 발생할 수 있습니다.
"cascadeDismiss": 상위 레이어 닫힘으로 인한 연쇄 닫힘
import { HStack, Text, VStack } from "@seed-design/react";
import { useState } from "react";
import { ActionButton } from "seed-design/ui/action-button";
import {
DialogAction,
DialogBody,
DialogContent,
DialogFooter,
DialogRoot,
DialogTrigger,
} from "seed-design/ui/dialog";
import { Switch } from "seed-design/ui/switch";
const DialogOnOpenChangeReason = () => {
const [open, setOpen] = useState(false);
const [openReason, setOpenReason] = useState<string | null>(null);
const [closeReason, setCloseReason] = useState<string | null>(null);
const [closeOnInteractOutside, setCloseOnInteractOutside] = useState(false);
return (
<VStack gap="x4" align="center">
<DialogRoot
open={open}
closeOnInteractOutside={closeOnInteractOutside}
onOpenChange={(open, details) => {
setOpen(open);
(open ? setOpenReason : setCloseReason)(details?.reason ?? null);
}}
>
<DialogTrigger asChild>
<ActionButton variant="neutralSolid">열기</ActionButton>
</DialogTrigger>
<DialogContent title="onOpenChange 이유">
<DialogBody>
<VStack gap="x3" align="flex-start">
<Text textStyle="articleBody">
ESC 키를 누르거나 우측 상단 닫기 버튼, 하단 버튼을 눌러 닫아보세요. 아래 스위치를 켠
뒤 바깥 영역을 누르면 interactOutside 이유로 닫힙니다.
</Text>
<Switch
tone="neutral"
size="16"
label="closeOnInteractOutside"
checked={closeOnInteractOutside}
onCheckedChange={setCloseOnInteractOutside}
/>
</VStack>
</DialogBody>
<DialogFooter>
<HStack gap="x2" justify="flex-end">
<DialogAction variant="neutralWeak">취소</DialogAction>
<DialogAction variant="neutralSolid">확인</DialogAction>
</HStack>
</DialogFooter>
</DialogContent>
</DialogRoot>
<HStack gap="x4">
<Text fontSize="t3" color="fg.neutralMuted">
마지막 열림 이유: {openReason ?? "-"}
</Text>
<Text fontSize="t3" color="fg.neutralMuted">
마지막 닫힘 이유: {closeReason ?? "-"}
</Text>
</HStack>
</VStack>
);
};
export default DialogOnOpenChangeReason;Confirm Before Close
작성 중인 내용을 실수로 닫는 것을 막고 싶다면, open을 제어 상태로 두고 특정 reason일 때 open을 유지한 채 확인용 Alert Dialog를 띄워 사용자에게 되물을 수 있습니다.
import { HStack, ResponsivePair, Text, VStack } from "@seed-design/react";
import { useState } from "react";
import { ActionButton } from "seed-design/ui/action-button";
import {
AlertDialogAction,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogRoot,
AlertDialogTitle,
} from "seed-design/ui/alert-dialog";
import {
DialogAction,
DialogBody,
DialogContent,
DialogFooter,
DialogRoot,
DialogTrigger,
type DialogRootProps,
} from "seed-design/ui/dialog";
import { Switch } from "seed-design/ui/switch";
const closeReasons = [
"closeButton",
"escapeKeyDown",
"interactOutside",
] as const satisfies NonNullable<
Parameters<NonNullable<DialogRootProps["onOpenChange"]>>[1]
>["reason"][];
const confirmReasonLabels = {
closeButton: "닫기 버튼으로 닫을 때 확인 (closeButton)",
escapeKeyDown: "ESC 키로 닫을 때 확인 (escapeKeyDown)",
interactOutside: "바깥 클릭으로 닫을 때 확인 (interactOutside)",
} as const satisfies Record<(typeof closeReasons)[number], string>;
const DialogConfirmBeforeClose = () => {
const [open, setOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const [closeOnInteractOutside, setCloseOnInteractOutside] = useState(true);
const [confirmReasons, setConfirmReasons] = useState<
Record<(typeof closeReasons)[number], boolean>
>({ closeButton: true, escapeKeyDown: true, interactOutside: true });
return (
<>
<DialogRoot
open={open}
closeOnInteractOutside={closeOnInteractOutside}
onOpenChange={(nextOpen, details) => {
if (nextOpen) {
setOpen(true);
return;
}
const reason = closeReasons.find((reason) => reason === details?.reason);
if (reason && confirmReasons[reason]) {
setConfirmOpen(true);
return;
}
setOpen(false);
}}
>
<DialogTrigger asChild>
<ActionButton variant="neutralSolid">작성 폼 열기</ActionButton>
</DialogTrigger>
<DialogContent
title="글 작성"
description="닫기 동작을 바꿔가며 확인 다이얼로그를 띄워보세요"
>
<DialogBody>
<VStack align="flex-start" gap="x4">
<Switch
label="바깥 클릭으로 닫기 (closeOnInteractOutside)"
tone="brand"
size="16"
checked={closeOnInteractOutside}
onCheckedChange={setCloseOnInteractOutside}
/>
<VStack align="flex-start" gap="x2">
<Text fontSize="t3" color="fg.neutralMuted">
닫으려는 reason별로 확인 다이얼로그 띄우기
</Text>
{closeReasons.map((reason) => (
<Switch
key={reason}
label={confirmReasonLabels[reason]}
tone="neutral"
size="16"
checked={confirmReasons[reason]}
onCheckedChange={(checked) =>
setConfirmReasons((prev) => ({ ...prev, [reason]: checked }))
}
/>
))}
</VStack>
</VStack>
</DialogBody>
<DialogFooter>
<HStack gap="x2" justify="flex-end">
<DialogAction variant="neutralWeak">취소</DialogAction>
<DialogAction variant="neutralSolid">저장</DialogAction>
</HStack>
</DialogFooter>
</DialogContent>
</DialogRoot>
<AlertDialogRoot open={confirmOpen} onOpenChange={setConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>정말 닫을까요?</AlertDialogTitle>
<AlertDialogDescription>작성 중인 내용은 저장되지 않습니다.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<ResponsivePair gap="x2">
<AlertDialogAction variant="neutralWeak" onClick={() => setConfirmOpen(false)}>
계속 작성
</AlertDialogAction>
<AlertDialogAction
variant="criticalSolid"
onClick={() => {
setConfirmOpen(false);
setOpen(false);
}}
>
닫기
</AlertDialogAction>
</ResponsivePair>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogRoot>
</>
);
};
export default DialogConfirmBeforeClose;Portalled
Portal은 기본적으로 document.body에 렌더링됩니다.
import { HStack, Portal, Text } from "@seed-design/react";
import { ActionButton } from "seed-design/ui/action-button";
import {
DialogAction,
DialogBody,
DialogContent,
DialogFooter,
DialogRoot,
DialogTrigger,
} from "seed-design/ui/dialog";
const DialogPortalled = () => {
return (
// You can set z-index dialog with "--layer-index" custom property. useful for stackflow integration.
<DialogRoot>
<DialogTrigger asChild>
<ActionButton variant="neutralSolid">열기</ActionButton>
</DialogTrigger>
<Portal>
<DialogContent title="Portal" layerIndex={50}>
<DialogBody>
<Text textStyle="articleBody">Portal은 기본적으로 document.body에 렌더링됩니다.</Text>
</DialogBody>
<DialogFooter>
<HStack gap="x2" justify="flex-end">
<DialogAction variant="neutralWeak">취소</DialogAction>
<DialogAction variant="neutralSolid">확인</DialogAction>
</HStack>
</DialogFooter>
</DialogContent>
</Portal>
</DialogRoot>
);
};
export default DialogPortalled;Responsive
ResponsiveDialog를 사용하면 md 이상에서는 Dialog, sm 이하에서는 Bottom Sheet로 자동 전환됩니다. 뷰포트를 줄여서 전환 동작을 확인해보세요.
onOpenChange는 열림 상태만 전달하며, Dialog 또는 Bottom Sheet에만 적용되는 Root 옵션은 dialogRootProps, bottomSheetRootProps로 전달합니다.
import { HStack, Text, useResponsiveDialogContext } from "@seed-design/react";
import { forwardRef } from "react";
import { ActionButton } from "seed-design/ui/action-button";
import {
ResponsiveDialogAction,
ResponsiveDialogBody,
ResponsiveDialogContent,
ResponsiveDialogFooter,
ResponsiveDialogRoot,
ResponsiveDialogTrigger,
type ResponsiveDialogFooterProps,
} from "seed-design/ui/responsive-dialog";
const Footer = forwardRef<HTMLDivElement, ResponsiveDialogFooterProps>((props, ref) => {
const { shouldUseBottomSheet } = useResponsiveDialogContext();
return (
<ResponsiveDialogFooter ref={ref} {...props}>
<HStack gap="x2" justify="flex-end">
<ResponsiveDialogAction
variant="neutralWeak"
flexGrow={shouldUseBottomSheet ? 1 : undefined}
>
취소
</ResponsiveDialogAction>
<ResponsiveDialogAction
variant="neutralSolid"
flexGrow={shouldUseBottomSheet ? 1 : undefined}
>
확인
</ResponsiveDialogAction>
</HStack>
</ResponsiveDialogFooter>
);
});
const DialogResponsive = () => {
return (
<ResponsiveDialogRoot>
<ResponsiveDialogTrigger asChild>
<ActionButton variant="neutralSolid">Open</ActionButton>
</ResponsiveDialogTrigger>
<ResponsiveDialogContent
title="반응형 다이얼로그"
description="화면 크기에 따라 적합한 컴포넌트로 자동 전환됩니다."
>
<ResponsiveDialogBody>
<Text textStyle="articleBody">
md 이상에서는 화면 중앙의 Dialog로, sm 이하에서는 화면 하단에서 슬라이드되는 Bottom
Sheet로 표시됩니다.
</Text>
</ResponsiveDialogBody>
<Footer />
</ResponsiveDialogContent>
</ResponsiveDialogRoot>
);
};
export default DialogResponsive;Last updated on