Toast | CoUI
본문으로 건너뛰기
LogoCoUI

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#

사용 시기 (When to Use)#

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

  • 작업 결과(성공/실패)를 간단히 알릴 때
  • 되돌리기(undo) 액션을 제공할 때
  • 사용자 흐름을 방해하지 않는 비침입적 알림이 필요할 때

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

  • Dialog: 사용자 확인이 반드시 필요한 중요한 알림일 때
  • Alert / Banner: 페이지에 고정된 영구적 알림이 필요할 때
  • Tooltip: 특정 요소에 대한 설명이 필요할 때

Props / Parameters#

W는 액션 슬롯 타입이며 Flutter에서는 Widget, Web에서는 Component입니다. VoidCallback과 CoreVoidCallback은 모두 인자 없는 닫기 콜백입니다.

속성타입기본값설명
messageString필수토스트 메시지
description String? null 보조 설명 텍스트
variant CoreToastVariant CoreToastVariant.defaultVariant (neutral) 시각적 변형 (시맨틱 — 위젯 파라미터)
showIcon bool CoreToastContract.defaultShowIcon (true) variant 가 고른 선행 아이콘을 그릴지. false 면 아이콘과 그 뒤 간격이 함께 빠지고 메시지가 패널 안쪽 여백에서 시작합니다. 아이콘이 없는 neutral 에는 달라지는 것이 없습니다
onDismiss VoidCallback? / CoreVoidCallback? null 닫기 버튼 콜백 (null이면 닫기 버튼 없음)
actionW?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%)
iconColorinverseOnSurfaceinverseOnSurface—

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 를 주지 않으면 키보드로 닫을 방법이 없습니다 — Flutter showToast(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)#

항목FlutterWeb
토스트 카드Toast (통일)Toast (통일)
인프라ToastLayer + showToast()외부 관리
스태킹내장 (hover 확장, 최대 개수)외부 관리
닫기 방식스와이프 + 클릭 + 프로그래밍닫기 버튼 클릭
  • Dialog: 사용자 확인이 필요한 모달 알림
  • Alert: 페이지에 고정된 알림
  • Banner: 페이지 상단의 전체 너비 알림