Dialog#
사용자에게 확인, 입력, 정보를 요청하는 모달 다이얼로그 컴포넌트입니다.
Live Preview#
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'),
),
],
),
],
);
}
}
class DialogChainExample extends StatefulComponent {
const DialogChainExample({super.key});
@override
State<DialogChainExample> createState() => _DialogChainExampleState();
}
class _DialogChainExampleState extends State<DialogChainExample> {
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'),
),
],
).withStyle(
const CoreDialogStyle(
panelBackgroundColor: CoreColor.token(
CoreColors.surfaceContainerHigh,
),
panelBorderRadius: CoreBorderRadius.all(CoreRadius.radius24),
panelPadding: CoreEdgeInsets.all(CoreSpace.space24),
sectionGapStyle: CoreGapStyle(size: CoreSpace.space12),
actionSpacing: CoreSpace.space12,
),
),
]);
}
}
class DialogChainExample extends StatefulWidget {
const DialogChainExample({super.key});
@override
State<DialogChainExample> createState() => _DialogChainExampleState();
}
class _DialogChainExampleState extends State<DialogChainExample> {
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'),
),
],
).withStyle(
const CoreDialogStyle(
panelBackgroundColor: CoreColor.token(
CoreColors.surfaceContainerHigh,
),
panelBorderRadius: CoreBorderRadius.all(CoreRadius.radius24),
panelPadding: CoreEdgeInsets.all(CoreSpace.space24),
sectionGapStyle: CoreGapStyle(size: CoreSpace.space12),
actionSpacing: CoreSpace.space12,
),
),
],
);
}
}
사용 시기 (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,
)
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
open | bool | true | 다이얼로그 표시 여부 |
title |
Widget? |
null |
다이얼로그 제목 위젯 |
content | Widget? | null | 내용 위젯 |
leading |
Widget? |
null |
제목 앞 위젯 (아이콘 등) |
trailing |
Widget? |
null |
제목 뒤 위젯 (닫기 버튼 등) |
actions |
List<Widget>? |
null |
하단 액션 버튼 |
onClose |
VoidCallback? |
null |
닫기 콜백 |
dialogStyle |
CoreDialogStyle? |
null |
panel chrome / nested 슬롯 묶음 |
빠른 오버라이드 (Chain)#
이미 만든 Dialog 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class DialogChainExample extends StatefulWidget {
const DialogChainExample({super.key});
@override
State<DialogChainExample> createState() => _DialogChainExampleState();
}
class _DialogChainExampleState extends State<DialogChainExample> {
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'),
),
],
).withStyle(
const CoreDialogStyle(
panelBackgroundColor: CoreColor.token(
CoreColors.surfaceContainerHigh,
),
panelBorderRadius: CoreBorderRadius.all(CoreRadius.radius24),
panelPadding: CoreEdgeInsets.all(CoreSpace.space24),
sectionGapStyle: CoreGapStyle(size: CoreSpace.space12),
actionSpacing: CoreSpace.space12,
),
),
],
);
}
}
class DialogChainExample extends StatefulComponent {
const DialogChainExample({super.key});
@override
State<DialogChainExample> createState() => _DialogChainExampleState();
}
class _DialogChainExampleState extends State<DialogChainExample> {
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'),
),
],
).withStyle(
const CoreDialogStyle(
panelBackgroundColor: CoreColor.token(
CoreColors.surfaceContainerHigh,
),
panelBorderRadius: CoreBorderRadius.all(CoreRadius.radius24),
panelPadding: CoreEdgeInsets.all(CoreSpace.space24),
sectionGapStyle: CoreGapStyle(size: CoreSpace.space12),
actionSpacing: CoreSpace.space12,
),
),
]);
}
}
스타일 시스템 — dialogStyle#
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: CoreColor.token(CoreColors.surface),
panelBorderRadius: CoreBorderRadius.all(CoreRadius.radius16),
panelPadding: CoreEdgeInsets.all(CoreSpace.space24),
surfaceBlur: 0,
surfaceOpacity: 1,
barrierColor: CoreColor.token(CoreColors.scrim),
barrierOpacity: CoreOpacity.opacity40,
titleStyle: CoreTextStyle.token(CoreTextStyles.titleLarge),
contentStyle: CoreTextStyle.token(
CoreTextStyles.bodyMedium,
color: CoreColor.token(CoreColors.onSurfaceVariant),
),
// actionButtonStyle 은 actions 안의 Button 이 자동 적용
// (위 actions 처럼 variant 직접 주입)
),
)
CoreDialogStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
panelBackgroundColor |
CoreColor? |
Panel background fill colour override. When null, defers to [defaultsByVariant]
background
(
surface
token).
|
panelBorderColor |
CoreColor? |
Panel border stroke colour override. When null, defers to [defaultPanelBorderColor] (
outline
token).
|
panelBorderWidth |
double? |
Panel border width override (logical pixels). When null, defers to [defaultPanelBorderWidth] (1 logical px). |
panelBorderRadius |
CoreBorderRadius? |
Panel border radius override. When null, defers to [defaultPanelBorderRadius] (16 logical px uniform). |
panelPadding |
CoreEdgeInsets? |
Panel content padding override. When null, defers to [defaultPanelPadding] (24 logical px on all sides). |
panelMaxWidth |
double? |
Panel maximum width override (logical pixels). When null, defers to [defaultPanelMaxWidth] (512). |
surfaceBlur |
double? |
Backdrop blur sigma applied behind the panel. |
surfaceOpacity |
double? |
Panel surface opacity multiplier (0.0 – 1.0). |
panelBoxShadow |
List<CoreShadowLayer>? |
Elevation shadow of the dialog panel. When null, defers to [defaultPanelBoxShadow] (
CoreShadow.xl
, the Modal/Dialog rung).
|
shadowBaseColor |
CoreColor? |
Base colour the [panelBoxShadow] layers tint against. When null, defers to [defaultShadowBaseColor] (
shadow
token).
|
barrierColor |
CoreColor? |
Backdrop barrier overlay colour override. When null, defers to [defaultsByVariant]
overlayColor
(
onSurface
token).
|
barrierOpacity |
double? |
Backdrop (scrim) opacity multiplier override (0.0 – 1.0). When null, defers to [defaultBarrierOpacity] (0.8). |
sectionGapStyle |
CoreGapStyle? |
Gap slot between dialog stack sections — header / content / actions — and inside the header row between leading / title / trailing. Forwarded straight to
Gap(gapStyle: …)
(Flutter) and to the inter-slot inline
gap
CSS (Web).
null
defers to [defaultSectionGapStyle] (16 logical px).
|
actionSpacing |
double? |
Gap between adjacent action buttons inside the actions row (logical pixels). Rendered as
Row(spacing: …)
(Flutter native) / inline
gap
rem CSS on the footer row (Web).
null
defers to [defaultActionSpacing] (8 logical px).
|
transitionDuration |
Duration? |
Open/close transition duration override. When null, defers to [defaultTransitionDuration] (200 ms). |
scaleAnimationStart |
double? |
Panel open-animation start scale override. When null, defers to [defaultScaleAnimationStart] (0.9). |
scaleAnimationEnd |
double? |
Panel open-animation end scale override. When null, defers to [defaultScaleAnimationEnd] (1.0). |
titleStyle |
CoreTextStyle? |
Title text style override. |
contentStyle |
CoreTextStyle? |
Content (body) text style override. |
actionButtonStyle |
CoreButtonStyle? |
Action button style override. To change an action button's
variant
, inject the button widget directly via
Dialog(actions: [Button(...)])
rather than packing variant into this style — 원칙 8 (asChild pattern).
No default, and must not have one.
Null means the actions the caller handed in are rendered as they were handed in: Web branches on
when resolved.actionButtonStyle != null
and otherwise emits
action
untouched, taking no copy. A constant would rebuild every action
Button
in every dialog with dialog-level chrome merged underneath its own. And the value it would have to carry does not exist as a constant. A
Button
's baseline chrome is per-variant (
CoreButtonStyle.defaultsByVariant
), and anything named here outranks that table inside
Button
's resolver — so one constant would repaint a
.ghost
Cancel and a filled Save the same, which is the exact opposite of what the two buttons are for.
|
asChild 패턴 — action 버튼 변경#
action 버튼의 시맨틱 (variant) 변경이 필요할 때는 actions 슬롯에
직접 위젯을 주입합니다:
Dialog(
actions: [
Button(variant: .outline, child: Text('취소')), // ← variant 직접
Button(variant: .destructive, child: Text('삭제')),
],
)
dialogStyle.actionButtonStyle 은 chrome 미세 조정용 (paddingH /
labelStyle 등).
Resolve chain#
CoreDialogStyle.defaultX / defaultsVariant (static const, 단일 출처)
→ CoreDialogTheme.style // 프로젝트 공통
→ widget.dialogStyle // 인스턴스별
변형 (Variants)#
알림 다이얼로그#
Dialog(
open: isAlertOpen,
title: Text('오류'),
content: Text('네트워크 연결에 실패했습니다.'),
actions: [
Button(
variant: CoreButtonVariant.primary,
onPressed: handleClose,
child: Text('확인'),
),
],
onClose: handleClose,
)
아이콘 포함#
Dialog(
open: isOpen,
leading: Icon(LucideIcons.triangleAlert),
title: Text('경고'),
content: Text('이 작업은 취소할 수 없습니다.'),
onClose: handleClose,
)
동작 스펙 (Behavior)#
열기/닫기#
- 양 플랫폼 모두
openboolean 으로 표시 여부를 제어하고, 닫힘은onClose콜백으로 통보합니다 - Flutter:
open이 true 가 되면 내부적으로 dialog route 를 push, false 가 되면 pop - Web:
open이 true 일 때만OverlayHost.dialog레이어에 패널을 마운트
스크림 (배경 오버레이)#
- 다이얼로그가 열리면 뒤쪽에 반투명 배경이 표시되어 배경 콘텐츠와 분리
- 스크림 색·불투명도는
dialogStyle.barrierColor/barrierOpacity로 조정 -
스크림 클릭 / Escape 로 닫기는
onClose가 있을 때만 활성화됩니다 —onClose를 주지 않으면 dismiss 경로가 없는 다이얼로그가 됩니다
포커스 관리#
- Flutter: dialog route 가
FocusScope로 감싸져 배경 위젯이 traversal 에서 빠집니다 -
Web: 패널은
role="dialog"+aria-modal="true"를 emit 하지만 JS 포커스 트랩은 없습니다 — 배경 요소로 Tab 이 넘어갈 수 있고, 닫힌 뒤 이전 포커스로의 복원도 없습니다
애니메이션#
-
양 플랫폼 모두
dialogStyle.transitionDuration과scaleAnimationStart/scaleAnimationEnd를 씁니다 - Flutter:
ScaleTransition+FadeTransition - Web: CSS 트랜지션
Overlay 마운트#
-
Flutter: 루트
Overlay에 마운트되어 ancestorClipRect/Transform영향 없이 항상 최상위에 표시 -
Web:
OverlayHost.dialog레이어로 portal —position: fixed기반이라 ancestoroverflow: 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 | 다이얼로그 닫기 (onClose 가 있을 때만) |
Tab | 다음 포커스 가능 요소로 이동 |
Shift+Tab | 이전 포커스 가능 요소로 이동 |
Enter | 포커스된 버튼 활성화 |
스크린 리더#
-
Flutter: dialog route 가 배리어와 함께 modal route 로 올라가고,
FocusScope가 배경 위젯을 traversal 에서 제외합니다 - Web: 패널에
role="dialog"+aria-modal="true"를 emit 합니다
알려진 제약#
-
접근 가능한 이름을 잇는 파라미터가 없습니다 —
aria-labelledby/aria-describedby를 emit 하지 않으므로title/content가 다이얼로그의 이름·설명으로 프로그래밍적으로 연결되지 않습니다. - Web 에는 포커스 트랩이 없습니다 (위 포커스 관리 참고).
-
onClose를 주지 않으면 Escape·스크림 클릭 둘 다 동작하지 않습니다 — 닫기 버튼을trailing/actions에 반드시 두세요.
전역으로 적용되는 축(동작 줄이기·고대비·색 강제 모드·최소 터치 타겟)은 전역 접근성 축에 있습니다.
크로스 플랫폼 차이점 (Platform Differences)#
파라미터 이름·순서·기본값은 양 플랫폼 동일합니다. 아래는 렌더 엔진 차이만 나열합니다.
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | Dialog | Dialog |
| 제목 | title: Widget? | title: Component? |
| 내용 | content: Widget? | content: Component? |
| 액션 | actions: List<Widget>? |
actions: List<Component>? |
| 스크림 | modal route 의 배리어 | 배경 <div> + 클릭 핸들러 |
| 포커스 | FocusScope | 트랩 없음 (배경으로 Tab 이동 가능) |
| 애니메이션 | ScaleTransition + FadeTransition | CSS 트랜지션 |
| 오버레이 마운트 | dialog route (루트 Navigator) |
OverlayHost.dialog 레이어 portal — popover / menu / tooltip 위에 위치 |
| ARIA | FocusScope + modal route |
role="dialog", aria-modal="true" |
관련 컴포넌트 (Related Components)#
- 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),
)