Toast
토스트 알림 컴포넌트
목차
사용자에게 간단한 피드백 메시지를 일시적으로 표시하는 토스트 컴포넌트입니다.
기본 사용법 (Basic Usage)#
// 기본 토스트 카드
Toast(
message: '저장되었습니다.',
description: '변경사항이 성공적으로 저장되었습니다.',
onDismiss: () {},
)
// destructive 변형
Toast(
message: '오류가 발생했습니다.',
description: '네트워크 연결을 확인해 주세요.',
variant: CoreToastVariant.destructive,
onDismiss: () {},
)
// showToast와 함께 사용
showToast(
context: context,
builder: (context, overlay) => Toast(
message: '파일이 업로드되었습니다.',
onDismiss: overlay.close,
),
)
// 기본 토스트 (default 변형)
Toast(
message: '저장되었습니다.',
description: '변경사항이 성공적으로 저장되었습니다.',
onDismiss: handleDismiss,
)
// destructive 변형
Toast(
message: '오류가 발생했습니다.',
description: '네트워크 연결을 확인해 주세요.',
variant: CoreToastVariant.destructive,
onDismiss: handleDismiss,
)
Live Preview#
코드 보기
class ToastDefaultExample extends StatelessComponent {
const ToastDefaultExample({super.key});
@override
Component build(BuildContext context) {
return Toast(
message: 'Success',
description: 'Your changes have been saved.',
onDismiss: () {},
);
}
}
class ToastDefaultExample extends StatefulWidget {
const ToastDefaultExample({super.key});
@override
State<ToastDefaultExample> createState() => _ToastDefaultExampleState();
}
class _ToastDefaultExampleState extends State<ToastDefaultExample> {
@override
Widget build(BuildContext context) {
return Toast(
message: 'Success',
description: 'Your changes have been saved.',
onDismiss: () {},
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 작업 결과(성공/실패)를 간단히 알릴 때
- 되돌리기(undo) 액션을 제공할 때
- 사용자 흐름을 방해하지 않는 비침입적 알림이 필요할 때
대신 다른 컴포넌트를 사용하세요:
Dialog: 사용자 확인이 반드시 필요한 중요한 알림일 때Alert/Banner: 페이지에 고정된 영구적 알림이 필요할 때Tooltip: 특정 요소에 대한 설명이 필요할 때
Props / Parameters#
W는 액션 슬롯 타입이며 Flutter에서는 Widget, Web에서는 Component입니다. VoidCallback과
CoreVoidCallback은 모두 인자 없는 닫기 콜백입니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
message | String | 필수 | 토스트 메시지 |
description |
String? |
null |
보조 설명 텍스트 |
variant |
CoreToastVariant |
CoreToastVariant.defaultVariant (neutral) |
시각적 변형 (시맨틱 — 위젯 파라미터) |
showIcon |
bool |
CoreToastContract.defaultShowIcon (true) |
variant
가 고른 선행 아이콘을 그릴지.
false
면 아이콘과 그 뒤 간격이 함께 빠지고 메시지가 패널 안쪽 여백에서 시작합니다. 아이콘이 없는
neutral
에는 달라지는 것이 없습니다
|
onDismiss |
VoidCallback? / CoreVoidCallback? |
null |
닫기 버튼 콜백 (null이면 닫기 버튼 없음) |
action | W? | null | 액션 슬롯 콘텐츠 |
toastStyle |
CoreToastStyle? |
null |
panel chrome / nested 슬롯 묶음 |
빠른 오버라이드 (Chain)#
이미 만든 Toast 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class ToastChainExample extends StatelessWidget {
const ToastChainExample({super.key});
@override
Widget build(BuildContext context) {
return Toast(
message: 'Success',
description: 'Your changes have been saved.',
onDismiss: () {},
).withStyle(
const CoreToastStyle(
panelBorderRadius: CoreBorderRadius.all(CoreRadius.radius4),
panelBorderColor: CoreColor.token(CoreColors.primary),
panelBorderWidth: CoreStrokeWidth.stroke2,
panelPaddingWithDismiss: CoreEdgeInsets.directional(
start: CoreSpace.space16,
top: CoreSpace.space16,
end: CoreSpace.space32,
bottom: CoreSpace.space16,
),
messageDescriptionGapStyle: CoreGapStyle(size: CoreSpace.space8),
),
);
}
}
class ToastChainExample extends StatelessComponent {
const ToastChainExample({super.key});
@override
Component build(BuildContext context) {
return Toast(
message: 'Success',
description: 'Your changes have been saved.',
onDismiss: () {},
).withStyle(
const CoreToastStyle(
panelBorderRadius: CoreBorderRadius.all(CoreRadius.radius4),
panelBorderColor: CoreColor.token(CoreColors.primary),
panelBorderWidth: CoreStrokeWidth.stroke2,
panelPaddingWithDismiss: CoreEdgeInsets.directional(
start: CoreSpace.space16,
top: CoreSpace.space16,
end: CoreSpace.space32,
bottom: CoreSpace.space16,
),
messageDescriptionGapStyle: CoreGapStyle(size: CoreSpace.space8),
),
);
}
}
스타일 시스템 (Style System)#
toastStyle 슬롯#
Toast 의 panel chrome / 슬롯 미세 조정은 단일 toastStyle
(CoreToastStyle) 으로 흐릅니다. 시맨틱 enum (variant) 은 위젯 파라미터.
Toast(
message: '저장 완료',
description: '변경사항이 적용되었습니다',
variant: CoreToastVariant.destructive,
onDismiss: handleDismiss,
action: Button(
variant: .ghost,
onPressed: undo,
child: Text('되돌리기'),
),
toastStyle: CoreToastStyle(
panelBackgroundColor: CoreColor.token(CoreColors.surface),
panelBorderColor: CoreColor.token(CoreColors.outline),
panelBorderRadius: CoreBorderRadius.all(CoreRadius.radius12),
panelPadding: CoreEdgeInsets.all(CoreSpace.space16),
contentActionGapStyle: CoreGapStyle(size: CoreSpace.space12),
messageStyle: CoreTextStyle.token(
CoreTextStyles.labelLarge,
).copyWith(fontWeight: CoreFontWeight.semiBold),
descriptionStyle: CoreTextStyle.token(
CoreTextStyles.bodySmall,
color: CoreColor.token(CoreColors.onSurfaceVariant),
),
// actionButtonStyle 은 action 안의 Button 이 자동 적용 가능
// (위처럼 variant 직접 주입 — asChild)
),
)
CoreToastStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
panelBackgroundColor |
CoreColor? |
Panel background fill colour override. |
panelBorderColor |
CoreColor? |
Panel border stroke colour override. |
panelBorderRadius |
CoreBorderRadius? |
Panel border radius override. |
panelBorderWidth |
double? |
Panel border stroke width override (logical px). When null, defers to [defaultPanelBorderWidth] (1). |
panelPadding |
CoreEdgeInsets? |
Panel content padding override (no dismiss button). When null, defers to [defaultPanelPadding]. |
panelPaddingWithDismiss |
CoreEdgeInsets? |
Panel content padding override when a dismiss button is present — the resolver selects this (instead of [panelPadding]) so the trailing edge can leave room for the close button. When null, defers to [defaultPanelPaddingWithDismiss]. |
panelBoxShadow |
List<CoreShadowLayer>? |
Panel elevation shadow override. When null, defers to [defaultPanelBoxShadow]. |
minHeight |
double? |
Panel minimum height override (logical px). When null, defers to [defaultMinHeight]. |
contentActionGapStyle |
CoreGapStyle? |
Gap style between the message + description column and the action slot — nested [CoreGapStyle] slot forwarded to the
Gap
widget that separates the content column from the action.
null
defers to [defaultContentActionGapStyle].
|
iconStyle |
CoreIconStyle? |
Leading-glyph style override.
null
defers to [defaultIconStyle]. Size and any other icon chrome only — the colour comes from the variant row (the message colour), so the glyph reads on the bubble.
|
iconContentGapStyle |
CoreGapStyle? |
Gap between the leading glyph and the content column.
null
defers to [defaultIconContentGapStyle]. Reached only by the variants that draw a glyph; a toast with none starts at its content.
|
messageDescriptionGapStyle |
CoreGapStyle? |
Gap style between the message and description rows inside the content column — nested [CoreGapStyle] slot forwarded to the
Gap
widget that separates message from description.
null
defers to [defaultMessageDescriptionGapStyle].
|
closeInset |
CoreEdgeInsets? |
Close-button inset from the top-right corner — four-sided so the caller can shift the dismiss button on any axis. This is panel layout (it positions the dismiss button inside the panel padding region), not button chrome, so it stays flat; the button's own chrome lives in the nested [closeButtonStyle] slot. When null, defers to [defaultCloseInset]. |
transitionDuration |
Duration? |
Panel chrome transition duration override. When null, defers to [defaultTransitionDuration]. |
messageStyle |
CoreTextStyle? |
Message (primary text) style override. |
descriptionStyle |
CoreTextStyle? |
Description (secondary text) style override. |
actionButtonStyle |
CoreButtonStyle? |
Action button style override. To change the action's
variant
, inject the action widget directly via
Toast(action: Button(variant: outline,...))
rather than packing variant into this style — 원칙 8 (asChild pattern). Its default ([defaultActionButtonStyle]) names no colour, deliberately — a flat default would name one colour for three variants; what the resolver injects over it is instead the VARIANT's own foreground, the same way [defaultCloseButtonStyle] is tinted with
closeForeground
:
text defaultActionButtonStyle // focus band.merge(CoreButtonStyle(foregroundColor: variant.foregroundColor, hoverForegroundColor: variant.foregroundColor)).merge(actionButtonStyle) // this slot.merge(action.buttonStyle) // the caller's own Button, wins
The action is the caller's widget, but it sits on the toast's own slab, not on the page surface a
Button
's default foreground was chosen for — so the toast says what legible means there by laying the variant's foreground under it. The caller's explicit style still wins by merge order, and the caller's own
variant
is still theirs (asChild).
|
closeButtonStyle |
CoreButtonStyle? |
Close (dismiss) button style override. The single entry point for the dismiss-button chrome — padding / close-glyph (
leadingIconStyle
) / hover transition (
animationDuration
) / focus-ring width (
focusOutlineStyle.borderWidth
). Merged onto [defaultCloseButtonStyle] and raw-forwarded to
Button(variant:.plain, buttonStyle: …)
(the
plain
variant owns the resting-opacity / hover-fade / foreground semantics).
|
CoreToastStyle 변형별 기본값 (CoreToastVariantStyle)#
| 필드 | success |
destructive |
neutral |
|---|---|---|---|
backgroundColor |
inverseSurface | inverseSurface | inverseSurface |
closeForeground |
inverseOnSurface (opacity 70%) | inverseOnSurface (opacity 70%) | inverseOnSurface (opacity 70%) |
closeHoverForeground |
inverseOnSurface | inverseOnSurface | inverseOnSurface |
foregroundColor |
inverseOnSurface | inverseOnSurface | inverseOnSurface |
borderColor |
inverseSurface | inverseSurface | inverseSurface |
descriptionForeground |
inverseOnSurface (opacity 75%) | inverseOnSurface (opacity 75%) | inverseOnSurface (opacity 75%) |
iconColor | inverseOnSurface | inverseOnSurface | — |
asChild 패턴 — action 버튼 변경#
action 버튼의 시맨틱 (variant) 변경이 필요할 때는 action 슬롯에
직접 위젯을 주입합니다:
Toast(
message: '...',
action: Button(variant: .ghost, child: Text('되돌리기')), // ← variant 직접
)
toastStyle.actionButtonStyle 은 chrome 미세 조정용 (paddingH /
labelStyle 등).
Resolve chain#
design system default for variant
→ CoreToastTheme.style // 프로젝트 공통
→ CoreToastTheme.variantStyles[widget.variant] // variant 별
→ 부모 컴포넌트 슬롯 오버라이드
→ widget.toastStyle // 인스턴스별
테마 커스터마이징 (Theme Customization)#
CoreComponentTheme(
toast: CoreToastTheme(
style: CoreToastStyle(
panelBorderRadius: CoreBorderRadius.all(CoreRadius.radius12),
),
),
)
변형 (Variants)#
세 변형은 선행 글리프 하나로만 갈립니다. 배경 · 전경 · 보더는 셋 다 같은 inverseSurface / inverseOnSurface
쌍이라(위 변형별 기본값 표 참조), 변형을 고르는 것은 색을 고르는 것이 아니라 무엇을 주장하는지를 고르는 것입니다.
토스트는 페이지와 반대 명도의 말풍선입니다 — Light 에서는 어두운 회색, Dark 에서는 밝은 회색. 모서리는 surface 역할(16), 그림자는 shadow-sm, 안쪽 여백은 가로 18 · 세로 14(닫기 버튼이 있으면 끝쪽 32)입니다. 메시지는
labelLarge 를 600 으로, 설명은 메시지 아래 2 떨어져 inverseOnSurface 의 75 % 로, 닫기 ✕ 는 70 % 로 쉬다가 hover 에 100 % 가 됩니다. 스킴에
inverseOnSurfaceVariant 가 없어서 낮은 두 단은 같은 토큰에 불투명도를 곱해 진술합니다. 선행 글리프도 메시지와 같은 inverseOnSurface
로 그립니다 — 상태는 색이 아니라 글리프 모양(circle-check · circle-alert)과
문구가 전합니다.
neutral#
기본값입니다. 글리프가 없습니다 — 결과를 보고하지 않는 알림.
variant 를 적지 않으면 이것이 그려집니다. 아무 변형도 이름하지 않은 호출자는 어떤 결과도 주장하지 않은 것이고, 글리프 없는 토스트가 그 상태의 모습입니다.
Toast(
message: 'New version available',
description: 'Reload to get the latest.',
onDismiss: handleDismiss,
)
success#
성공 체크(circle-check)를 답니다. 무언가 성공했을 때만 씁니다.
Toast(
message: 'Saved',
description: 'Your changes have been saved.',
variant: CoreToastVariant.success,
onDismiss: handleDismiss,
)
destructive#
경고 원(circle-alert)을 답니다. 실패를 보고할 때 씁니다.
Toast(
message: 'Error',
description: 'Something went wrong.',
variant: CoreToastVariant.destructive,
onDismiss: handleDismiss,
)
아이콘 없이 (showIcon)#
세 변형은 선행 아이콘 하나로만 갈리므로, showIcon: false 는 아이콘과 그 뒤 간격을 함께 뺍니다. 메시지와 설명은 패널 안쪽 여백에서 시작하고, 설명 ·
action · 닫기 버튼은 그대로입니다. 변형이 고르는 것이 아이콘뿐이라 아이콘을 끈 토스트는 어느 변형이든 같은 모습이 됩니다 — 결과의 성격(성공 · 실패)을 아이콘 모양으로 말하지 않고 글로만 알리는 제품이 쓰는 모양입니다. 색이나 모서리 같은 면의 모습은 이 속성이 아니라 토큰과 앱 테마의 몫입니다. 아이콘을 끄면 종류를 눈으로 가를 수단이 글뿐이므로, 성공 · 실패 같은 성격은
message 에 말로 담아 주세요.
기본값(true)은 지금까지와 똑같이 그립니다. 아이콘이 없는 neutral 에서는 showIcon 이 켜져 있어도 꺼져 있어도 그려지는 것이 같습니다.
Toast(
message: 'Success',
description: 'Your changes have been saved.',
variant: CoreToastVariant.success,
showIcon: false,
onDismiss: () {},
)
Toast(
message: 'Success',
description: 'Your changes have been saved.',
variant: CoreToastVariant.success,
showIcon: false,
onDismiss: () {},
)
Flutter 인프라 (ToastLayer)#
Flutter에서는 ToastLayer와 showToast()로 토스트 표시/스태킹/애니메이션을 관리합니다.
// 앱 최상단에 ToastLayer 배치
ToastLayer(
maxStackedEntries: 3,
expandMode: ExpandMode.expandOnHover,
child: MyApp(),
)
// 토스트 표시
final overlay = showToast(
context: context,
builder: (context, overlay) => Toast(
message: '처리 완료',
onDismiss: overlay.close,
),
location: ToastLocation.bottomRight,
showDuration: Duration(seconds: 5),
);
// 프로그래밍 방식으로 닫기
overlay.close();
동작 스펙 (Behavior)#
Toast는 메시지 카드를 표시하며 variant가 선행 글리프를 선택하고 showIcon이 그 글리프를 그릴지 정합니다(끄면 글리프와 그 뒤 간격이 함께 빠집니다).
description이 없으면 설명과 그 간격을 생략하고 action이 있으면 콘텐츠 뒤에 합성합니다. onDismiss가 있어야 닫기 버튼이 나타나며 버튼은 콜백만 호출합니다. 카드 자체에는 자동 닫기 타이머·위치·스택 상태가 없습니다. Flutter의
showToast가 반환하는 overlay와 ToastLayer가 표시 lifecycle을 소유하며 showDuration은 Duration입니다.
사용 가이드라인 (Usage Guidelines)#
잠깐 나타나는 결과 알림에는 Toast를 사용합니다. 콘텐츠 옆에 계속 남는 실패·복구 안내는 Alert, 그릴 콘텐츠가 없는 영역은 EmptyState, 페이지 상단 안내 스트립은 Banner를 선택합니다. 응답을 기다려야 하는 확인은 Dialog가 맞습니다.
✅ Do#
action 의 시맨틱(variant)은 슬롯에 직접 주입 (asChild)
Toast(
message: '삭제되었습니다.',
action: Button(variant: .ghost, onPressed: undo, child: Text('되돌리기')),
onDismiss: handleDismiss,
)
이유: toastStyle.actionButtonStyle은 padding·라벨 스타일 같은 chrome 미세 조정 전용입니다. 버튼의 variant 자체를 바꾸려면 action 슬롯에 원하는 Button을 직접 넣어야 합니다.
❌ Don't#
Toast 를 ToastLayer/showToast() 없이 화면에 직접 배치하지 않기 (Flutter)
// ❌ Toast 카드만 트리 어딘가에 그대로 둠 — 스태킹/자동 닫힘/애니메이션 없음
Column(children: [Toast(message: '저장되었습니다.', onDismiss: () {})])
이유: Flutter에서 토스트의 표시·스태킹(maxStackedEntries)·등장/퇴장 애니메이션은 ToastLayer + showToast() 인프라가 관리합니다. Toast 카드만 트리에 두면 여러 토스트가 겹치거나, 위치(ToastLocation)와 지속 시간(showDuration)이 전혀 반영되지 않습니다.
접근성 (Accessibility)#
역할#
-
Flutter: 카드 루트에
Semantics(liveRegion: true)가 붙습니다. 역할(role)은 지정하지 않습니다. -
Web: 루트 요소에
role="status"+aria-live="polite"가 붙습니다. 두 속성은 caller attributes 뒤에 병합되어 호출부가 덮어쓸 수 없습니다. -
닫기 버튼(
onDismiss를 준 경우)은 양 플랫폼 모두buttonClose를 이름으로 갖습니다 — Web 은aria-label, Flutter 는Semantics(label:).
키보드#
| 키 | 동작 |
|---|---|
Tab | 토스트 안의 action 위젯 / 닫기 버튼으로 이동 |
Enter / Space | 포커스된 버튼 활성화 |
토스트를 닫는 전용 단축키(Escape 등)는 양 플랫폼 어디에도 없습니다.
스크린 리더#
- 접근 가능한 이름은 별도 라벨 없이
message/description슬롯의 텍스트 자체에서 옵니다. -
등장은 양 플랫폼 모두 통보됩니다 — Flutter 는
liveRegion: true, Web 은role="status"+aria-live="polite". -
variant: destructive여도 통보 방식은 그대로 polite 입니다 — Web 이role="alert"로 바뀌지 않으므로 오류 토스트가 읽던 문장을 끊고 끼어들지 않습니다. Flutter 에는 애초에 polite / assertive 축이 없어 "통보한다 / 안 한다" 두 갈래뿐입니다.
포커스 관리#
-
진입: 토스트가 뜰 때 포커스는 이동하지 않습니다(양 플랫폼). 액션 / 닫기 버튼에 닿으려면
Tab으로 직접 이동해야 합니다. -
이탈: 포커스를 옮긴 적이 없으므로 복귀 처리도 없습니다. Flutter
showToast가 돌려주는ToastOverlay.close()는 오버레이만 제거합니다. - 트랩: 없습니다. 토스트는 모달이 아니며 배경 조작을 막지 않습니다.
-
포커스 표시: 닫기 버튼과
Button액션의 링은 링과 버튼 사이 2px 간격을surface밴드로 칠합니다. 토스트는 어떤 페이지 위에든 떠서 링 하나로는 모든 인접 색과의 대비를 약속할 수 없어서, 링 + 밴드의 두 색 인디케이터로 그립니다(CoreToastStyle.defaultCloseButtonStyle·defaultActionButtonStyle). 호출자가 액션에 준buttonStyle이 그 위에 머지됩니다.
알려진 제약#
-
onDismiss를 주지 않으면 키보드로 닫을 방법이 없습니다 — FluttershowToast(dismissible: true)의 해제는 가로 드래그 제스처로만 동작하는 포인터 전용 경로입니다. 키보드로 닿을 수 있는 유일한 해제 수단이 닫기 버튼이므로, 자동 해제를 기다리지 않고 닫으려면onDismiss를 반드시 넘겨야 합니다. -
자동 해제 타이머가 포인터 hover 로만 멈춥니다(Flutter) —
showToast의showDuration기본값은 5초이고, 타이머를 취소하는 것은MouseRegion의onEnter뿐입니다. 키보드 포커스가 토스트 안으로 들어와도 타이머는 계속 흘러, 액션 버튼까지Tab으로 이동하는 도중 토스트가 사라질 수 있습니다(WCAG 2.2.1).showToast의showDuration파라미터는 non-nullable 이라 자동 해제를 끌 수 없습니다. — 대안:showDuration에 충분히 긴 값을 넘깁니다. -
Web 에는 토스트를 띄우는 계층이 없습니다 — Flutter 는
showToast/ToastLayer/ToastOverlay가 배치·스택·자동 해제·스와이프 해제를 관리하지만, Web 은Toast카드 컴포넌트만 있습니다. 따라서 위의 자동 해제·스와이프 해제 동작은 Flutter 에만 존재하고, Web 에서는 어디에 놓을지·언제 지울지·포커스를 어떻게 처리할지를 호출부가 직접 구현해야 합니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 토스트 카드 | Toast (통일) | Toast (통일) |
| 인프라 | ToastLayer + showToast() | 외부 관리 |
| 스태킹 | 내장 (hover 확장, 최대 개수) | 외부 관리 |
| 닫기 방식 | 스와이프 + 클릭 + 프로그래밍 | 닫기 버튼 클릭 |