Dialog | CoUI

Dialog

다이얼로그/모달 컴포넌트

Dialog#

사용자에게 확인, 입력, 정보를 요청하는 모달 다이얼로그 컴포넌트입니다.

Live Preview#

Web
Flutter
Loading Flutter...
class DialogDefaultExample extends StatefulComponent {
  const DialogDefaultExample({super.key});

  @override
  State<DialogDefaultExample> createState() => _DialogDefaultExampleState();
}

class _DialogDefaultExampleState extends State<DialogDefaultExample> {
  bool _open = false;

  @override
  Component build(BuildContext context) {
    return div([
      Button(
        variant: CoreButtonVariant.primary,
        onPressed: () => setState(() => _open = true),
        child: Text('Open Dialog'),
      ),
      Dialog(
        open: _open,
        onClose: () => setState(() => _open = false),
        title: Text('Are you sure?'),
        content: Text('This action cannot be undone.'),
        actions: [
          Button(
            variant: CoreButtonVariant.outline,
            onPressed: () => setState(() => _open = false),
            child: Text('Cancel'),
          ),
          Button(
            variant: CoreButtonVariant.primary,
            onPressed: () => setState(() => _open = false),
            child: Text('Continue'),
          ),
        ],
      ),
    ]);
  }
}
class DialogDefaultExample extends StatefulWidget {
  const DialogDefaultExample({super.key});

  @override
  State<DialogDefaultExample> createState() => _DialogDefaultExampleState();
}

class _DialogDefaultExampleState extends State<DialogDefaultExample> {
  bool _open = false;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Button(
          variant: CoreButtonVariant.primary,
          onPressed: () => setState(() => _open = true),
          child: const Text('Open Dialog'),
        ),
        Dialog(
          open: _open,
          onClose: () => setState(() => _open = false),
          title: const Text('Are you sure?'),
          content: const Text('This action cannot be undone.'),
          actions: [
            Button(
              variant: CoreButtonVariant.outline,
              onPressed: () => setState(() => _open = false),
              child: const Text('Cancel'),
            ),
            Button(
              variant: CoreButtonVariant.primary,
              onPressed: () => setState(() => _open = false),
              child: const Text('Continue'),
            ),
          ],
        ),
      ],
    );
  }
}

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 사용자에게 중요한 확인을 요청할 때 (삭제, 저장 등 되돌리기 어려운 작업)
  • 추가 정보를 입력받아야 할 때 (폼 다이얼로그)
  • 중요한 알림이나 경고를 표시할 때

대신 다른 컴포넌트를 사용하세요:

  • Toast: 간단한 알림이나 성공/실패 피드백을 표시할 때
  • Drawer: 복잡한 내용이나 긴 폼을 사이드에서 보여줄 때
  • Popover: 특정 요소에 부착된 간단한 정보를 표시할 때
  • Tooltip: 짧은 도움말 텍스트만 필요할 때

기본 사용법 (Basic Usage)#

// 확인 다이얼로그
Dialog(
  open: isDeleteDialogOpen,
  title: Text('삭제 확인'),
  content: Text('이 항목을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.'),
  actions: [
    Button(
      variant: CoreButtonVariant.outline,
      onPressed: handleCancel,
      child: Text('취소'),
    ),
    Button(
      variant: CoreButtonVariant.destructive,
      onPressed: handleDelete,
      child: Text('삭제'),
    ),
  ],
  onClose: handleCancel,
)

// 커스텀 다이얼로그
Dialog(
  open: isEditDialogOpen,
  title: Text('프로필 편집'),
  content: Column(
    children: [
      TextField(label: '이름', onChanged: handleNameChange),
      TextField(label: '이메일', onChanged: handleEmailChange),
    ],
  ),
  actions: [
    Button(
      variant: CoreButtonVariant.ghost,
      onPressed: handleCancel,
      child: Text('취소'),
    ),
    Button(
      variant: CoreButtonVariant.primary,
      onPressed: handleSave,
      child: Text('저장'),
    ),
  ],
  onClose: handleCancel,
)
// 확인 다이얼로그 (open 속성으로 상태 제어)
Dialog(
  open: isDeleteDialogOpen,
  title: text('삭제 확인'),
  content: text('이 항목을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.'),
  actions: [
    Button(
      variant: CoreButtonVariant.outline,
      onPressed: handleCancel,
      child: text('취소'),
    ),
    Button(
      variant: CoreButtonVariant.destructive,
      onPressed: handleDelete,
      child: text('삭제'),
    ),
  ],
  onClose: handleCancel,
)

// 커스텀 콘텐츠 다이얼로그 (프로필 편집 폼)
Dialog(
  open: isEditDialogOpen,
  title: text('프로필 편집'),
  content: div([
    TextField(label: '이름', onChanged: handleNameChange),
    TextField(label: '이메일', onChanged: handleEmailChange),
  ]),
  actions: [
    Button(
      variant: CoreButtonVariant.ghost,
      onPressed: handleCancel,
      child: text('취소'),
    ),
    Button(
      variant: CoreButtonVariant.primary,
      onPressed: handleSave,
      child: text('저장'),
    ),
  ],
  onClose: handleCancel,
)

// 알림 다이얼로그 (단순 확인 버튼만)
Dialog(
  open: isAlertOpen,
  title: text('오류'),
  content: text('네트워크 연결에 실패했습니다.'),
  actions: [
    Button(
      variant: CoreButtonVariant.primary,
      onPressed: handleClose,
      child: text('확인'),
    ),
  ],
  onClose: handleClose,
)

스타일 시스템 — dialogStyle (Epic #1302)#

Dialog 의 panel chrome / 슬롯 미세 조정은 단일 dialogStyle (CoreDialogStyle) 으로 흐릅니다. 동작 / 콘텐츠 슬롯은 위젯 파라미터 그대로.

Dialog(
  open: isOpen,
  title: Text('확인'),
  content: Text('정말 삭제하시겠습니까?'),
  actions: [
    Button(variant: .outline, onPressed: cancel, child: Text('취소')),
    Button(variant: .destructive, onPressed: confirm, child: Text('삭제')),
  ],
  onClose: handleClose,
  dialogStyle: CoreDialogStyle(
    panelBackgroundColor: cs.surface,
    panelBorderRadius: CoreBorderRadius.all(16),
    panelPadding: CoreEdgeInsets.all(24),
    surfaceBlur: 0,
    surfaceOpacity: 1,
    barrierColor: cs.scrim.withOpacity(.4),
    titleStyle: CoreTextStyle(fontSize: 20, fontWeight: CoreFontWeight.semiBold),
    contentStyle: CoreTextStyle(fontSize: 14, color: cs.onSurfaceVariant),
    // actionButtonStyle 은 actions 안의 Button 이 자동 적용
    // (위 actions 처럼 variant 직접 주입 — Epic #1302 원칙 8)
  ),
)

CoreDialogStyle 필드#

필드타입설명
panelBackgroundColorCoreColor?panel 배경
panelBorderRadiusCoreBorderRadius?panel 보더 반경
panelPaddingCoreEdgeInsets?panel 내부 패딩
panelMaxWidthdouble?panel 최대 너비 (기본 512)
surfaceBlurdouble?배경 backdrop blur sigma
surfaceOpacitydouble?panel 표면 불투명도 (0–1)
barrierColor Color? / String? 배경 오버레이 색
barrierOpacity double? backdrop scrim 불투명도 (기본 0.8)
scaleAnimationStart double? panel open 애니메이션 시작 scale (기본 0.9)
scaleAnimationEnd double? panel open 애니메이션 종료 scale (기본 1.0)
titleStyleCoreTextStyle?제목 텍스트 스타일
contentStyleCoreTextStyle?본문 텍스트 스타일
actionButtonStyle CoreButtonStyle? action 버튼 chrome 미세 조정 (variant 변경은 위젯 슬롯 주입)

asChild 패턴 — action 버튼 변경#

action 버튼의 시맨틱 (variant) 변경이 필요할 때는 actions 슬롯에 직접 위젯을 주입합니다 (Epic #1302 원칙 8):

Dialog(
  actions: [
    Button(variant: .outline, child: Text('취소')),       // ← variant 직접
    Button(variant: .destructive, child: Text('삭제')),
  ],
)

dialogStyle.actionButtonStyle 은 chrome 미세 조정용 (paddingH / labelStyle 등).

Resolve chain#

design system default
  → CoreDialogTheme.style                       // 프로젝트 공통
  → 부모 컴포넌트 슬롯 오버라이드
  → widget.dialogStyle                          // 인스턴스별

Props / Parameters#

속성타입기본값설명
openbooltrue다이얼로그 표시 여부
title Widget? null 다이얼로그 제목 위젯
contentWidget?null내용 위젯
leading Widget? null 제목 앞 위젯 (아이콘 등)
trailing Widget? null 제목 뒤 위젯 (닫기 버튼 등)
actions List<Widget>? null 하단 액션 버튼
onClose VoidCallback? null 닫기 콜백
dialogStyle CoreDialogStyle? null panel chrome / nested 슬롯 묶음

변형 (Variants)#

알림 다이얼로그#

Dialog(
  open: isAlertOpen,
  title: Text('오류'),
  content: Text('네트워크 연결에 실패했습니다.'),
  actions: [
    Button(
      variant: CoreButtonVariant.primary,
      onPressed: handleClose,
      child: Text('확인'),
    ),
  ],
  onClose: handleClose,
)

아이콘 포함#

Dialog(
  open: isOpen,
  leading: Icon(Icons.warning),
  title: Text('경고'),
  content: Text('이 작업은 취소할 수 없습니다.'),
  onClose: handleClose,
)

동작 스펙 (Behavior)#

열기/닫기#

  • Flutter: showDialog() 함수로 열기. Navigator.pop(context) 또는 onConfirm/onCancel 콜백으로 닫기
  • Web: open boolean 속성으로 상태 제어. onOpenChange 콜백으로 닫기 이벤트 감지

스크림 (배경 오버레이)#

  • 다이얼로그가 열리면 뒤쪽에 반투명 배경이 표시되어 배경 콘텐츠와 분리
  • Flutter: ModalBackdrop으로 구현. dismissible: true이면 스크림 클릭으로 닫기 가능
  • Web: modal: true이면 스크림 표시

포커스 관리#

  • 다이얼로그 열리면 첫 번째 포커스 가능 요소에 자동 포커스
  • Flutter: FocusTrap으로 포커스가 다이얼로그 내부에 갇힘
  • 다이얼로그 닫히면 이전 포커스 요소로 복원

스크롤#

  • 내용이 다이얼로그 높이를 초과하면 내부 스크롤 활성화
  • 다이얼로그가 열린 동안 배경 스크롤 차단

애니메이션#

  • Flutter: DialogRoute로 Scale + Fade 애니메이션 적용
  • Web: CSS 트랜지션 기반 (DialogContainerStyle 의 open-state 클래스 swap)

Overlay 마운트#

  • Flutter: 루트 Overlay 에 마운트되어 ancestor ClipRect / Transform 영향 없이 항상 최상위에 표시
  • Web: OverlayHost.dialog 레이어로 portal — position: fixed 기반이라 ancestor overflow: hidden / transform 컨테이너 안에 Dialog 를 배치해도 잘리거나 좌표 어긋남이 없음. 같은 페이지에 popover / menu / tooltip 이 열려있어도 z-순서상 dialog 가 그 위에 위치

사용 가이드라인 (Usage Guidelines)#

✅ Do#

위험한 작업은 결과를 명확히 설명하세요.

Dialog(
  open: isOpen,
  title: Text('계정 삭제'),
  content: Text('계정을 삭제하면 모든 데이터가 영구적으로 제거됩니다. 이 작업은 취소할 수 없습니다.'),
  actions: [
    Button(variant: CoreButtonVariant.outline, onPressed: handleCancel, child: Text('취소')),
    Button(variant: CoreButtonVariant.destructive, onPressed: handleDeleteAccount, child: Text('계정 삭제')),
  ],
  onClose: handleCancel,
)

사용자가 결과를 충분히 이해한 뒤 결정할 수 있습니다.


❌ Don't#

모호한 메시지로 확인을 요청하지 마세요.

Dialog(
  open: isOpen,
  title: Text('확인'),
  content: Text('진행하시겠습니까?'),
  actions: [
    Button(variant: CoreButtonVariant.primary, onPressed: handleDeleteAccount, child: Text('확인')),
  ],
  onClose: handleCancel,
)

무엇이 진행되는지 알 수 없어 실수로 위험한 작업을 실행할 수 있습니다.

✅ Do#

다이얼로그는 간결하게 유지하세요.

Dialog(
  open: isOpen,
  title: Text('변경사항 저장'),
  content: Text('저장하지 않은 변경사항이 있습니다. 저장하시겠습니까?'),
  actions: [
    Button(variant: CoreButtonVariant.outline, onPressed: handleDiscard, child: Text('저장하지 않음')),
    Button(variant: CoreButtonVariant.primary, onPressed: handleSave, child: Text('저장')),
  ],
  onClose: handleDiscard,
)

하나의 결정에 집중하면 사용자가 빠르게 판단할 수 있습니다.


❌ Don't#

다이얼로그에 과도한 내용을 넣지 마세요.

Dialog(
  open: isOpen,
  title: Text('설정'),
  content: ComplexSettingsForm(), // 긴 폼, 여러 탭
  onClose: handleClose,
)

복잡한 내용은 별도 페이지나 Drawer를 사용하세요. 다이얼로그는 간단한 확인/입력에 적합합니다.

✅ Do#

닫을 수 있는 방법을 항상 제공하세요.

Dialog(
  open: isOpen,
  title: Text('알림'),
  content: Text('작업이 완료되었습니다.'),
  actions: [
    Button(variant: CoreButtonVariant.primary, onPressed: handleClose, child: Text('확인')),
  ],
  onClose: handleClose,
)

ESC 키, 스크림 클릭, 닫기 버튼 중 하나 이상의 닫기 방법이 있어야 합니다.


❌ Don't#

닫기 방법 없이 다이얼로그를 표시하지 마세요.

Dialog(
  open: isOpen,
  title: Text('알림'),
  content: Text('작업이 완료되었습니다.'),
  // actions 없음, onClose 없음 — 닫을 수 없음
)

사용자가 다이얼로그에 갇혀 앱을 사용할 수 없게 됩니다.

접근성 (Accessibility)#

키보드 인터랙션#

동작
Escape다이얼로그 닫기 (dismissible일 때)
Tab다이얼로그 내 다음 포커스 가능 요소로 이동
Shift+Tab다이얼로그 내 이전 포커스 가능 요소로 이동
Enter포커스된 버튼 활성화

스크린 리더#

  • Flutter: Semantics로 다이얼로그 역할, 제목, 내용 전달. 포커스 트랩이 읽기 순서 보장
  • Web: role="dialog", aria-modal="true", aria-labelledby, aria-describedby 자동 적용. AlertDialogrole="alertdialog" 사용

포커스 트랩#

  • 다이얼로그 내에서만 Tab 포커스 순환
  • 배경 요소에 포커스가 가지 않도록 차단

크로스 플랫폼 차이점 (Platform Differences)#

v3.0부터 기본 API (enabled, onChanged 등)가 통일되었습니다. 아래는 플랫폼 고유 차이점만 나열합니다.

항목FlutterWeb
클래스명DialogDialog
열기/닫기 open boolean + onClose 콜백 open boolean + onClose 콜백
제목title: Widgettitle: Component
내용content: Widgetcontent: Component
액션 actions: List<Widget> actions: List<Component>
스크림 ModalBackdrop 위젯 CSS 기반 (backdropStyle + 클릭 핸들러)
포커스 트랩FocusTrap 위젯네이티브 <dialog> 포커스 관리
애니메이션Scale + FadeCSS 트랜지션
오버레이 마운트 루트 Overlay OverlayHost.dialog 레이어 portal — popover / menu / tooltip 위에 위치
ARIA 속성 Semantics 사용 role="dialog", aria-modal="true"
  • Drawer: 화면 측면에서 슬라이드되는 패널. 복잡한 내용이나 긴 폼에 적합
  • Toast: 간단한 알림 메시지. 사용자 확인이 필요 없는 피드백에 사용
  • Popover: 특정 요소에 부착되는 작은 오버레이. 간단한 추가 정보 표시에 적합

조합 예제#

// 삭제 확인 패턴
Button(
  variant: CoreButtonVariant.destructive,
  onPressed: () => setState(() => _isDeleteDialogOpen = true),
  child: Text('삭제'),
)

// 다이얼로그 (위젯 트리에 배치)
Dialog(
  open: _isDeleteDialogOpen,
  title: Text('항목 삭제'),
  content: Text('선택한 ${items.length}개 항목을 삭제하시겠습니까?'),
  actions: [
    Button(
      variant: CoreButtonVariant.outline,
      onPressed: () => setState(() => _isDeleteDialogOpen = false),
      child: Text('취소'),
    ),
    Button(
      variant: CoreButtonVariant.destructive,
      onPressed: () {
        handleDelete();
        setState(() => _isDeleteDialogOpen = false);
      },
      child: Text('삭제'),
    ),
  ],
  onClose: () => setState(() => _isDeleteDialogOpen = false),
)