Dialog
먼저 보고, 그다음 계약을 읽습니다
포커스를 가두는 모달 다이얼로그 (Radix Dialog 기반).
Component intent의도와 경계 읽기
Radix Dialog 위에 .axe-dialog__* 스타일을 입힌 wrapper 다. Dialog(Root)·DialogTrigger·DialogContent·DialogTitle·DialogDescription·DialogClose 를 조합해 쓰고, DialogHeader/DialogFooter 는 title·설명·액션 라인을 잡아주는 선택적 레이아웃 헬퍼다. 포커스 트랩·Esc 닫기·바깥 클릭 닫기·ARIA role/labelledby/describedby·바깥 트리 aria-hidden 격리는 전부 Radix 가 처리하고, @axe/ui 는 size 변형과 표면 스타일만 담당한다. 닫기 X 버튼은 자동으로 그려지지 않으므로 DialogClose 를 직접 배치한다(헤더는 우상단 X 자리를 위해 padding-right 를 비워둔다).
예제
SSOT에 등록된 실제 API 기준 snippet입니다. standalone 배지만 독립 실행 단위이며, fragment는 주변 state·handler 문맥을 생략합니다.
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose, Button,} from "@axe/ui";<Dialog> <DialogTrigger asChild> <Button>초대 보내기</Button> </DialogTrigger> <DialogContent size="sm"> <DialogHeader> <DialogTitle>멤버 초대</DialogTitle> <DialogDescription> 입력한 이메일로 워크스페이스 초대 링크가 발송됩니다. </DialogDescription> </DialogHeader> <DialogFooter> <DialogClose asChild> <Button variant="ghost">취소</Button> </DialogClose> <Button variant="primary" onClick={sendInvite}>보내기</Button> </DialogFooter> </DialogContent></Dialog>import { Dialog, DialogContent, DialogTitle } from "@axe/ui";<Dialog open={open} onOpenChange={setOpen}> <DialogContent size="full"> <DialogTitle>문서 뷰어</DialogTitle> {/* … 전체화면 콘텐츠 … */} </DialogContent></Dialog><div class="axe-dialog__overlay"></div><div class="axe-dialog__content axe-dialog__content--sm" role="dialog" aria-modal="true" aria-labelledby="dlg-t"> <div class="axe-dialog__header"> <h2 id="dlg-t" class="axe-dialog__title">멤버 초대</h2> <p class="axe-dialog__description">초대 링크가 발송됩니다.</p> </div> <div class="axe-dialog__footer"> <button class="axe-btn axe-btn--ghost">취소</button> <button class="axe-btn axe-btn--primary">보내기</button> </div></div>Props
TSX 소스가 진실입니다. 주요 export 컴포넌트의 public API만 노출합니다.
| 이름 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
open | boolean | — | — | 열림 상태 (controlled). Radix Dialog.Root prop. |
onOpenChange | (open: boolean) => void | — | — | 열림/닫힘 전환 콜백 (Root). |
defaultOpen | boolean | — | false | uncontrolled 초기 열림 상태 (Root). |
modal | boolean | — | true | true 면 바깥과의 상호작용을 차단하고 포커스를 가둔다 (Root). |
size | "sm" | "md" | "lg" | "xl" | "full" | — | "md" | DialogContent 의 max-width. sm=440 / md=560 / lg=720 / xl=960 / full=100vw(전체화면·radius 0). |
withOverlay | boolean | — | true | DialogContent 가 흐린 backdrop 오버레이를 함께 렌더할지. false 면 콘텐츠만 뜬다. |
...rest (DialogContent) | React.ComponentPropsWithoutRef<typeof Dialog.Content> | — | — | onEscapeKeyDown·onPointerDownOutside·forceMount 등 Radix Content props 를 그대로 전파. |
...rest (Header/Footer) | React.HTMLAttributes<HTMLDivElement> | — | — | DialogHeader·DialogFooter 는 className 병합 후 나머지 속성을 루트 <div> 로 전파. |
.axe-* 클래스 계약
React 밖에서도 같은 표면을 그리는 공개 계약입니다. stable은 minor 버전 안에서 이름을 바꾸지 않습니다.
| 클래스 | 안정성 | 용도 |
|---|---|---|
.axe-dialog__overlay | stable | 화면 전체를 덮는 흐린 backdrop(fixed inset:0). |
.axe-dialog__content | stable | 중앙 정렬 카드 표면(fixed·translate 중앙). |
.axe-dialog__content--sm | stable | max-width 440px 변형. |
.axe-dialog__content--md | stable | max-width 560px 변형(기본). |
.axe-dialog__content--lg | stable | max-width 720px 변형. |
.axe-dialog__content--xl | stable | max-width 960px 변형. |
.axe-dialog__content--full | stable | 전체화면 변형(radius 0·100dvh). |
.axe-dialog__header | stable | title+설명 스택. 우상단 close X 자리로 padding-right 확보. |
.axe-dialog__title | stable | 제목(display 폰트·semibold). |
.axe-dialog__description | stable | 보조 설명 문단(secondary 톤). |
.axe-dialog__footer | stable | 우정렬 액션 버튼 라인(상단 구분선). |
접근성
키보드, ARIA, 구현 노트를 함께 검토합니다.
- Esc 로 닫기(트리거로 포커스 복원)
- Tab/Shift+Tab 은 콘텐츠 안에서만 순환(포커스 트랩)
- 열릴 때 첫 포커스 대상으로 자동 이동, 닫힐 때 트리거로 복원
Radix 가 콘텐츠에 role=dialog 를 부여하고, DialogTitle 을 aria-labelledby, DialogDescription 을 aria-describedby 로 자동 연결한다. 모달 격리는 aria-modal 속성이 아니라 바깥 트리 전체에 aria-hidden 을 거는 방식(aria-hidden 패키지의 hideOthers)으로 구현된다 — 설치된 @radix-ui/react-dialog 는 콘텐츠에 aria-modal 을 붙이지 않는다. 트리거에는 aria-haspopup=dialog·aria-expanded·aria-controls 가 붙는다.
DialogTitle 은 접근성상 필수 — 생략하면 Radix 가 개발 콘솔에 경고한다. 시각적으로 숨기려면 sr-only 로. modal=true 기본이라 바깥 스크롤이 잠기고 포인터가 차단된다. data-state=open|closed 로 진입/퇴장 애니메이션이 갈리며 prefers-reduced-motion 시 애니메이션이 제거된다.