Component
Dialog
현재 흐름을 멈추고 한 작업에 집중하게 하는 모달입니다.
Playground
옵션을 조절하며 화면과 사용 코드를 함께 확인하세요.
모바일에서 바깥 클릭과 드래그로 닫기를 허용합니다.
모바일 Drawer 자동 전환
모바일 Drawer를 전체 높이로 표시합니다. 끄면 내용 높이를 사용합니다.
제목과 푸터를 고정하고 본문을 스크롤합니다. 끄면 PC에서 모달 전체가 스크롤됩니다.
끄면 제목을 화면에서 숨기고 스크린 리더에는 제공합니다.
PC 모달 크기입니다. 모바일 높이는 fullScreenOnMobile으로 정합니다.
필수 접근 가능한 제목
선택적 설명
닫기 버튼 표시
닫기 버튼의 접근 가능한 이름
본문 안쪽 여백: none 없음, sm 8px, md 16px.
PC·모바일 화면보다 긴 안내·목록·입력 폼을 표시합니다.
import { Dialog, DialogTrigger, Button, DialogContent, DialogBody, Input, DialogFooter, DialogClose } from '@amineslab/ui'
export default function ExamplePreview() {
return ((<Dialog dismissible={true} drawerOnMobile={true}>
<DialogTrigger asChild>
<Button>초대 보내기</Button>
</DialogTrigger>
<DialogContent fullScreenOnMobile={true} scrollable={true} showTitle={true} size={"md"} closeLabel={"닫기"} description={"초대받은 사람은 프로젝트를 볼 수 있습니다."} showClose={true} title={"팀원 초대"}>
<DialogBody padding={"md"}>
<div className="grid gap-4">
<Input aria-label="초대할 이메일" placeholder="name@example.com"/>
{false}
</div>
</DialogBody>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">취소</Button>
</DialogClose>
<Button>초대 보내기</Button>
</DialogFooter>
</DialogContent>
</Dialog>))
}Props
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogClose,
DialogPortal,
// ...
} from '@amineslab/ui'Dialog
| Name | Type | Default | Description |
|---|---|---|---|
Dialog.drawerOnMobile | boolean | true | 768px 미만에서 Drawer로 자동 전환합니다. false이면 PC 형태를 유지합니다. |
Dialog.dismissible | boolean | true | 모바일 Drawer의 바깥 클릭·드래그 닫기를 허용합니다. |
DialogHeader.padding | 'none' | 'sm' | 'md' | 'md' | 헤더 여백. none=없음, sm=8px, md=좌우 20px·상하 16px. 검색창 등 조밀한 헤더에는 sm을 사용합니다. |
DialogHeader.showDivider | boolean | true | 헤더 아래 구분선 표시 여부입니다. |
DialogBody.padding | 'none' | 'sm' | 'md' | 'md' | 본문 여백. none=없음, sm=8px(자체 여백이 있는 목록), md=16px(텍스트·폼). 이중 여백을 피합니다. |
DialogBody.paddingBlock | 'none' | 'sm' | 'md' | padding과 동일 | 좌우 패딩은 유지하고 상하 패딩만 별도로 지정합니다. |
DialogBody.scrollable | boolean | true | false이면 본문 자체 스크롤을 끄고 자식 영역에서 스크롤을 관리합니다. |
DialogBody.viewportRef | Ref<HTMLDivElement> | - | 실제 스크롤 요소. PC에서는 ScrollArea viewport, 모바일에서는 native 본문을 가리킵니다. |
DialogContent.fullScreenOnMobile | boolean | true | 모바일 Drawer를 전체 높이로 표시합니다. false이면 내용 높이를 사용합니다. 좁은 화면에서 내용을 전체 화면으로 표시합니다. |
DialogContent.scrollable | boolean | true | header/footer를 고정하고 PC ScrollArea, 모바일 native scroll로 본문을 처리합니다. |
DialogContent.size | 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'md' | DialogContent의 크기를 정합니다. full은 가로·세로 작업 공간을 확장합니다. |
DialogContent.title | ReactNode | - | 접근 가능한 제목을 만드는 기존 단축 prop입니다. |
DialogContent.description | ReactNode | - | 접근 가능한 설명을 만드는 기존 단축 prop입니다. |
DialogContent.showTitle | boolean | true | false면 생성된 title을 화면에서만 숨기고 스크린 리더에는 제공합니다. |
DialogContent.showClose | boolean | true | 닫기 버튼 표시 여부입니다. |
DialogContent.closeIcon | ReactNode | - | 닫기 버튼에 표시할 선택적 아이콘입니다. |
DialogContent.closeLabel | string | '닫기' | 닫기 버튼의 접근 가능한 이름입니다. |
Examples
자주 쓰는 조합을 살펴보고, 필요한 예시의 코드를 펼쳐 확인하세요.
제목과 닫기 버튼 정렬
닫기 아이콘은 제목 첫 줄의 중앙에 맞춥니다. 긴 제목은 줄바꿈하고 설명은 다음 행을 사용합니다. 모바일 닫기 버튼의 터치 영역은 44px입니다.
import { Dialog, DialogTrigger, Button, DialogContent, DialogBody, Popover, PopoverTrigger, PopoverContent } from '@amineslab/ui'
function HeaderExample() {
return (<div className="flex flex-wrap gap-2">
{[
{ label: '짧은 제목', title: '프로젝트 설정', description: undefined },
{
label: '긴 제목과 설명',
title: '모든 팀원이 함께 사용하는 프로젝트의 알림 설정',
description: '설명은 제목 아래에 배치되며 닫기 버튼의 정렬에 영향을 주지 않습니다.',
},
].map(({ label, title, description }) => (<Dialog key={label}>
<DialogTrigger asChild>
<Button variant="outline">{label}</Button>
</DialogTrigger>
<DialogContent title={title} description={description}>
<DialogBody>닫기 아이콘은 제목 첫 줄의 중앙에 정렬됩니다.</DialogBody>
</DialogContent>
</Dialog>))}
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">긴 Drawer 제목</Button>
</PopoverTrigger>
<PopoverContent title="모든 팀원이 함께 사용하는 프로젝트의 정렬 기준">
<p className="m-0 text-body-2">모바일에서도 긴 제목이 닫기 버튼을 밀어내지 않습니다.</p>
</PopoverContent>
</Popover>
</div>);
}
export default function ExamplePreview() {
return (<HeaderExample />)
}본문 여백과 스크롤 소유권
텍스트는 md(16px), 자체 여백이 있는 목록은 sm(8px), 직접 배치하는 본문은 none입니다. scrollable={false}이면 자식 하나가 스크롤을 소유하게 구성합니다.
import { Popover, PopoverTrigger, Button, PopoverContent } from '@amineslab/ui'
function PaddingExample() {
return (<div className="flex flex-wrap gap-2">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">텍스트 · md</Button>
</PopoverTrigger>
<PopoverContent title="안내">
<p className="m-0 text-body-2">일반 텍스트는 16px 여백을 사용합니다.</p>
</PopoverContent>
</Popover>
<Popover fullScreenOnMobile>
<PopoverTrigger asChild>
{<Button variant="outline">자체 스크롤 · 자식 소유</Button>}
</PopoverTrigger>
<PopoverContent title="직접 배치" className="flex max-h-[70dvh] flex-col overflow-hidden p-0" scrollable={false}>
<div className="min-h-0 flex-1 overflow-y-auto p-4">
{Array.from({ length: 20 }, (_, i) => (<p key={`own-scroll-${i}`} className="text-body-2">
직접 관리하는 스크롤 영역 {i + 1}
</p>))}
</div>
</PopoverContent>
</Popover>
</div>);
}
export default function ExamplePreview() {
return (<PaddingExample />)
}PC ScrollArea · 모바일 전체 화면
768px 미만에서는 아래로 끌어 닫는 Drawer입니다. 본문 중간에서는 스크롤하고, 최상단에서 아래로 당기면 패널이 내려갑니다. 정렬·공개 범위를 누르면 중첩 패널을 확인할 수 있습니다.
import { Dialog, DialogTrigger, Button, DialogContent, DialogHeader, DialogTitle, DialogBody, Input, Popover, PopoverTrigger, PopoverContent, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, DialogFooter, DialogClose } from '@amineslab/ui'
import { useState } from 'react'
function ResponsiveMenuExample() {
const [open, setOpen] = useState(false);
const [selection, setSelection] = useState('최근 수정순');
return (<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="outline">{selection}</Button>
</PopoverTrigger>
<PopoverContent title="정렬 기준" padding="sm">
<div className="grid gap-1">
{['최근 수정순', '이름순', '생성일순'].map((label) => (<Button key={label} variant="ghost" className="justify-start" onClick={() => {
setSelection(label);
setOpen(false);
}}>
{label}
</Button>))}
</div>
</PopoverContent>
</Popover>);
}
function MobileDialogExample({ longActions = false }: {
longActions?: boolean;
}) {
return (<Dialog>
<DialogTrigger asChild>
<Button>{longActions ? '긴 버튼 모달 열기' : '스크롤 모달 열기'}</Button>
</DialogTrigger>
<DialogContent size="lg">
<DialogHeader>
<DialogTitle>프로젝트 설정</DialogTitle>
</DialogHeader>
<DialogBody>
<div className="grid gap-4">
<Input aria-label="프로젝트 이름" placeholder="프로젝트 이름"/>
<ResponsiveMenuExample />
<Select defaultValue="team">
<SelectTrigger aria-label="공개 범위">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="team">팀만 보기</SelectItem>
<SelectItem value="all">전체 공개</SelectItem>
</SelectContent>
</Select>
{Array.from({ length: 24 }, (_, i) => (<p key={`setting-${i}`} className="m-0 text-body-2 text-text-secondary">
설정 안내 {i + 1}. 본문을 스크롤해도 제목과 하단 작업 버튼은 같은 자리에 유지됩니다.
</p>))}
</div>
</DialogBody>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">
{longActions ? '변경사항을 저장하지 않고 나가기' : '취소'}
</Button>
</DialogClose>
<DialogClose asChild>
<Button>{longActions ? '변경사항을 저장하고 프로젝트로 돌아가기' : '저장'}</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>);
}
export default function ExamplePreview() {
return (<MobileDialogExample />)
}긴 버튼은 전체 너비 두 행
모바일에서는 기본 두 열입니다. 둘 중 하나라도 한 열에 들어가지 않으면 두 버튼 모두 전체 너비로 전환합니다.
import { Dialog, DialogTrigger, Button, DialogContent, DialogHeader, DialogTitle, DialogBody, Input, Popover, PopoverTrigger, PopoverContent, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, DialogFooter, DialogClose } from '@amineslab/ui'
import { useState } from 'react'
function ResponsiveMenuExample() {
const [open, setOpen] = useState(false);
const [selection, setSelection] = useState('최근 수정순');
return (<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="outline">{selection}</Button>
</PopoverTrigger>
<PopoverContent title="정렬 기준" padding="sm">
<div className="grid gap-1">
{['최근 수정순', '이름순', '생성일순'].map((label) => (<Button key={label} variant="ghost" className="justify-start" onClick={() => {
setSelection(label);
setOpen(false);
}}>
{label}
</Button>))}
</div>
</PopoverContent>
</Popover>);
}
function MobileDialogExample({ longActions = false }: {
longActions?: boolean;
}) {
return (<Dialog>
<DialogTrigger asChild>
<Button>{longActions ? '긴 버튼 모달 열기' : '스크롤 모달 열기'}</Button>
</DialogTrigger>
<DialogContent size="lg">
<DialogHeader>
<DialogTitle>프로젝트 설정</DialogTitle>
</DialogHeader>
<DialogBody>
<div className="grid gap-4">
<Input aria-label="프로젝트 이름" placeholder="프로젝트 이름"/>
<ResponsiveMenuExample />
<Select defaultValue="team">
<SelectTrigger aria-label="공개 범위">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="team">팀만 보기</SelectItem>
<SelectItem value="all">전체 공개</SelectItem>
</SelectContent>
</Select>
{Array.from({ length: 24 }, (_, i) => (<p key={`setting-${i}`} className="m-0 text-body-2 text-text-secondary">
설정 안내 {i + 1}. 본문을 스크롤해도 제목과 하단 작업 버튼은 같은 자리에 유지됩니다.
</p>))}
</div>
</DialogBody>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">
{longActions ? '변경사항을 저장하지 않고 나가기' : '취소'}
</Button>
</DialogClose>
<DialogClose asChild>
<Button>{longActions ? '변경사항을 저장하고 프로젝트로 돌아가기' : '저장'}</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>);
}
export default function ExamplePreview() {
return (<MobileDialogExample longActions/>)
}사용 기준
- PC에서는 크기별 모달과 ScrollArea, 모바일에서는 전체 화면 Drawer를 사용합니다.
- Header·Body·Footer를 분리하고, 본문 여백은 텍스트 md·자체 여백이 있는 목록 sm·직접 배치 none으로 선택합니다.
- 짧은 확인은 Confirm, 선택 메뉴는 Popover를 사용합니다.
- 겉모양만으로 사용을 결정하지 마세요.
- 의미와 키보드 동작, 모바일에서의 흐름을 함께 확인하세요.
접근성
화면에 맞는 이름을 제공하고, 키보드만으로 작업을 마칠 수 있는지 확인하세요.
Tab: dialog 안에서 포커스 이동Escape: 닫고 trigger로 포커스 복귀