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

Alert

정보, 성공, 경고, 오류 메시지를 표시하는 알림 영역 컴포넌트

목차

다양한 상태(정보, 성공, 경고, 오류)를 시각적으로 구분하여 메시지를 표시하는 알림 영역 컴포넌트입니다. Flutter와 Web이 동일한 named properties API(title, description, variant, layout, leading, onDismiss, action)를 사용합니다. layout 이 stacked(기본) 면 제목과 설명이 쌓인 패널이고, inline 이면 한 줄짜리 상태 띠입니다.

기본 사용법 (Basic Usage)#

// 기본 알림
Alert(
  title: 'Heads up!',
  description: 'You can add components to your app using the cli.',
)

// 정보 알림 (variant named constructor)
Alert.info(
  title: '안내',
  description: '새로운 업데이트가 있습니다.',
)

// 성공 알림 (닫기 버튼 포함)
Alert.success(
  title: '저장 완료',
  description: '변경 사항이 성공적으로 저장되었습니다.',
  onDismiss: handleDismiss,
)

// 오류 알림
Alert.destructive(
  title: '오류 발생',
  description: '요청을 처리하는 중 문제가 발생했습니다.',
)

// 한 줄 상태 띠 (inline) — 앞자리에 Badge, 뒤에 caller 의 아이콘
Alert(
  layout: CoreAlertLayout.inline,
  leading: Badge(
    tone: CoreBadgeTone.success,
    showDot: true,
    child: Text('Synced'),
  ),
  title: 'All changes saved to the cloud.',
  action: Icon(
    LucideIcons.chevronDown,
    iconStyle: CoreIconStyle(
      size: CoreIconSize.size16,
      color: CoreColor.token(CoreColors.onSurfaceSubtle),
    ),
  ),
)
// 기본 알림
Alert(
  title: 'Heads up!',
  description: 'You can add components to your app using the cli.',
)

// 정보 알림
Alert.info(
  title: '안내',
  description: '새로운 업데이트가 있습니다.',
)

// 성공 알림 (닫기 버튼 포함)
Alert.success(
  title: '저장 완료',
  description: '변경 사항이 성공적으로 저장되었습니다.',
  onDismiss: handleDismiss,
)

// 오류 알림
Alert.destructive(
  title: '오류 발생',
  description: '요청을 처리하는 중 문제가 발생했습니다.',
)

// 한 줄 상태 띠 (inline) — 앞자리에 Badge, 뒤에 caller 의 아이콘
Alert(
  layout: CoreAlertLayout.inline,
  leading: Badge(
    tone: CoreBadgeTone.success,
    showDot: true,
    child: Text('Synced'),
  ),
  title: 'All changes saved to the cloud.',
  action: Icon(
    LucideIcons.chevronDown,
    iconStyle: CoreIconStyle(
      size: CoreIconSize.size16,
      color: CoreColor.token(CoreColors.onSurfaceSubtle),
    ),
  ),
)

Live Preview#

사용 시기 (When to Use)#

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

  • 페이지 내 고정된 위치에서 중요한 상태 메시지를 안내할 때
  • 폼 제출 결과(성공/오류)를 인라인으로 표시할 때
  • 사용자가 반드시 인지해야 하는 경고나 안내를 표시할 때

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

  • 입력값 자체가 거부된 경우에는 FormField 의 오류·검증 표면을 씁니다. 검증 힌트는 ValidatorHint, 검증 표시 래퍼는 Validated 를 확인하세요. 요청 실패와 필드 값 문제는 다른 자리입니다
  • EmptyState: 실패한 영역에 보여줄 내용이 없어 그 자리를 대신 채워야 할 때. 내용이 남아 있고 한 동작만 실패했으면 Alert 를 그 옆에 둡니다
  • Toast: 본문에 남는 패널 없이 일시적인 오버레이 피드백이 필요할 때
  • Banner: 특정 영역 옆이 아니라 페이지 상단의 공지 스트립이 필요할 때
  • Dialog: 메시지 표시 외에 모달에서 사용자의 확인 응답을 받아야 할 때

Props / Parameters#

W 는 Flutter의 Widget, Web의 Jaspr Component 입니다. W? 는 선택 슬롯입니다.

속성타입기본값설명
title String 필수 알림 제목 — 이 컴포넌트가 반드시 갖는 한 줄
description String? null 제목 아래 보조 설명 (선택). layout: inline 에서는 그리지 않습니다
variant CoreAlertVariant neutral 알림 종류 ( neutral / info / success / warning / destructive ). 면 색과 아이콘 색을 정합니다
layout CoreAlertLayout stacked 내용을 놓는 방식. stacked 는 제목 + 설명이 쌓인 패널, inline 은 한 줄 상태 띠
leading W? null 톤 아이콘 대신 앞자리에 놓을 위젯. 두 layout 모두에서 쓸 수 있고, null 이면 variant 의 톤 아이콘을 그립니다
showIcon bool CoreAlertContract.defaultShowIcon (true) 앞자리(톤 아이콘 또는 leading )를 그릴지. false 면 앞자리와 그 뒤 간격이 함께 빠지고 글자가 패널 안쪽 여백에서 시작합니다
onDismiss VoidCallback? (Flutter) / CoreVoidCallback? (Web) null 닫기 버튼 클릭 핸들러. null이면 닫기 버튼 미표시
action W? null 알림 우측에 표시할 액션 위젯
alertStyle CoreAlertStyle? null 인스턴스 스타일 (Style 시스템 참조)

스타일 시스템 (Style System)#

Alert 의 모든 chrome / dimensional / nested-slot 오버라이드는 CoreAlertStyle 단일 슬롯으로 흐릅니다. 시맨틱 enum (variant) 과 behaviour (title / description / action / onDismiss) 는 위젯 파라미터로 직접 전달합니다.

시맨틱 vs 스타일#

  • 시맨틱 enum / behaviour: 위젯/컴포넌트 파라미터로 직접 (variant, layout, title, description, leading, action, onDismiss)
  • chrome / dimensional / 슬롯 스타일: CoreAlertStyle 한 곳으로 (backgroundColor / minHeight / borderRadius / padding / iconContentGapStyle / titleBottomMargin / titleStyle / descriptionStyle / iconStyle / actionButtonStyle / closeButtonStyle)

Resolve chain#

design system default for alert         // defaultsByLayout[layout] + defaultsByVariant[variant]
  → CoreAlertTheme.style                   // 프로젝트 공통
  → CoreAlertTheme.variantStyles[variant]
  → parent component slot override
  → widget.alertStyle                      // 인스턴스별

layout 은 Theme 에 따로 슬롯이 없습니다. CoreAlertTheme.style 과 alertStyle 로 덮은 패딩 · 간격 · 아이콘 크기 · 제목 스타일 · minHeight 는 두 layout 에 똑같이 적용됩니다 — 한 layout 만 바꾸려면 그 layout 의 Alert 에만 alertStyle 을 넘깁니다.

각 nested 슬롯 스타일 (titleStyle / descriptionStyle / iconStyle / actionButtonStyle / closeButtonStyle) 은 자기 컴포넌트의 자체 resolve chain 으로 다시 한 번 머지됩니다.

CoreAlertStyle 필드#

필드타입설명
backgroundColor CoreColor? Alert panel background fill colour.
foregroundColor CoreColor? Foreground colour applied to title / description / icon when the per-slot style does not override it.
borderColor CoreColor? Border stroke colour. Alert draws no border of its own since 0.131 — it fills opaquely, and [defaultBorderWidth] is zero. The slot stays so a caller can opt one back in by stating a colour AND a width; a colour alone strokes nothing.
titleColor CoreColor? Title colour. null → the variant's titleColor in [defaultsByVariant] (a tone's on-{tone}-container ), then the layout's row in [defaultsByLayout] ([defaultTitleColor] when stacked) for the variant that states none. Wins over [foregroundColor] for the title on both platforms.
dismissColor CoreColor? Dismiss-glyph colour. null → [defaultDismissColor]. Replaces the opacity multiplier this used to carry — a colour times an alpha resolves against whatever panel sits behind it, which no guard can check. A token can be.
borderWidth double? Border stroke width (logical px). null → [defaultBorderWidth], which is now zero.
borderRadiusCoreBorderRadius?Border radius.
padding CoreEdgeInsets? Padding. null → the layout's row in [defaultsByLayout] ([defaultPadding] when stacked).
minHeight double? Minimum box height (logical px). null → the layout's row in [defaultsByLayout] ([defaultMinHeight] when stacked, none inline).
iconContentGapStyle CoreGapStyle? Nested [CoreGapStyle] slot for the gap between the leading icon and the content column / trailing slots — forwarded straight to the Gap widget that separates the slots. Merged on top of the layout's row in [defaultsByLayout] ([defaultIconContentGapStyle] when stacked).
titleBottomMargin double? Bottom margin between the title and description (logical px). null defers to [defaultTitleBottomMargin].
titleStyle CoreTextStyle? Title text style override, merged on top of the layout's row in [defaultsByLayout] ([defaultTitleStyle] when stacked).
descriptionStyle CoreTextStyle? Description text style override.
iconStyle CoreIconStyle? Leading icon style override, merged on top of the layout's row in [defaultsByLayout] ([defaultIconStyle] when stacked). Its size is the ambient icon size a caller's leading widget receives.
actionButtonStyle CoreButtonStyle? Action button chrome. Deliberately has no default* — absence is the design. The action is a caller-supplied widget, and the alert only reaches into it when the caller asked for chrome: the Web widget rebuilds a Button action with resolved.actionButtonStyle!.merge(…) only in the when resolved.actionButtonStyle != null arm of its pattern match, and renders the action untouched otherwise. A constant would make that rebuild unconditional, so every alert would re-style a button it was handed — the composed Button 's own variant defaults are the baseline here, and overriding them from the parent is exactly what this slot is for when a caller opts in. Unlike [closeButtonStyle], which the alert draws itself and therefore does default ([defaultCloseButtonStyle]), this affordance is not the alert's to design. Asymmetry worth knowing: only Web consumes it. The Flutter resolver forwards it onto ResolvedAlert.actionButtonStyle , but the Flutter widget renders action! as-is and never reads that slot, so seat (4) of the 1:1:1:1:1:1 mapping is empty on that platform — a wiring gap, not a default gap.
closeButtonStyle CoreButtonStyle? Close (dismiss) button style override. The single entry point for the dismiss-button chrome — padding / corner radius / close-glyph ( leadingIconStyle ) / hover transition ( animationDuration ) / focus ring ( focusOutlineStyle ) / idle-vs-hover foreground ( foregroundColor / hoverForegroundColor ). Merged onto [defaultCloseButtonStyle] and raw-forwarded to Button(variant:.plain, buttonStyle: …) (the plain variant owns the transparent-fill / hover-fade semantics).

CoreAlertStyle 변형별 기본값 (CoreAlertVariantStyle)#

필드 neutral info success warning destructive
backgroundColor surfaceContainer infoContainer successContainer warningContainer errorContainer
descriptionColor onSurfaceVariant onSurfaceVariant onSurfaceVariant onSurfaceVariant onSurfaceVariant
iconColor onSurfaceVariant onInfoContainer onSuccessContainer onWarningContainer onErrorContainer
titleColor — onInfoContainer onSuccessContainer onWarningContainer onErrorContainer

제목은 톤의 색입니다 — info · success · warning · destructive 는 아이콘과 같은 on{Tone}Container 로 그려, 아이콘을 끈 알림도 제목만으로 톤이 읽힙니다. neutral 은 제목 색을 갖지 않고 layout 의 색을 씁니다(stacked 는 onSurface, inline 은 onSurfaceVariant). 본문은 모든 톤에서 onSurfaceVariant 입니다. 톤 제목은 불투명한 톤 면 위에서 약 8:1 로, 본문 기준(4.5:1)을 넉넉히 넘습니다. titleColor 와 foregroundColor 를 둘 다 넘기면 제목은 titleColor 를 따릅니다(양 플랫폼 동일).

채움이 불투명한 것이 중요합니다. 이전 모델은 톤을 10 % 로 깔고 그 위에 같은 톤을 올려서, 라이트 모드 warning 이 1.71 까지 떨어졌습니다. 지금은 라벨이 페이지가 아니라 자기 패널을 배경으로 삼습니다.

레이아웃 (Layout)#

layout 은 내용을 얼마나 보여 주고 얼마나 자리를 차지할지를 정합니다. 면 · 반경 · 아이콘 색은 두 layout 모두 variant 표 그대로입니다.

칸stacked (기본)inline
모양제목 아래 설명이 쌓인 패널한 줄 상태 띠
패딩위아래 12 · 앞 16 · 끝 12위아래 10 · 좌우 14
최소 높이64없음 — 내용 높이 (Badge md 를 앞에 두면 10 + 24 + 10 = 44)
앞자리 ↔ 내용 간격1210
톤 아이콘1816
제목 labelLarge · 600 · 톤 색 (neutral 은 onSurface) bodySmall · 톤 색 (neutral 은 onSurfaceVariant), 한 줄 · 넘치면 말줄임
설명onSurfaceVariant그리지 않음

stacked 는 최소 높이(minHeight, 64) 안에서 내용이 세로 중앙에 옵니다. 안쪽 행도 가운데 정렬이라 아이콘 · 본문 · 행동 · 닫기 표시가 같은 가운데 선을 공유합니다. 두 줄짜리 Alert 은 하나의 덩어리로 읽히고, 제목만 있는 Alert 은 최소 높이가 열어둔 박스의 가운데에 앉습니다. 패딩은 앞과 끝이 다른 값이라 읽는 방향(directional)으로 적혀 있어 RTL 로케일에서도 같은 쪽에 붙습니다.

inline 은 한 줄에 목소리가 하나라서 제목이 본문 크기(bodySmall)입니다 — 톤은 글자 색과 앞의 배지나 아이콘이 함께 나릅니다. description 을 넘겨도 그리지 않으며, 접근성 이름은 두 layout 모두 title 이라 말줄임으로 잘려도 전체 제목이 읽힙니다.

앞자리 슬롯 (leading)#

leading 은 톤 아이콘을 대신합니다 — 둘을 함께 그리지 않고, null 이면 variant 의 톤 아이콘을 그립니다. 두 layout 모두에서 쓸 수 있습니다. 슬롯에는 톤 아이콘의 색과 그 layout 의 아이콘 크기가 아이콘 테마로 내려가므로, 아이콘 하나를 그대로 넘겨도 원래 아이콘과 같은 모양이 됩니다. Badge 처럼 자기 색을 가진 위젯은 그 색을 그대로 씁니다.

showIcon: false 는 이 자리를 통째로 끕니다 — 톤 아이콘이든 넘긴 leading 이든 그리지 않고, 그 뒤 간격도 함께 빠집니다.

색과 크기가 닿는 곳은 아이콘 테마까지입니다. 아이콘이 아닌 내용(맨 Text 등)은 슬롯이 칠하지도 줄 높이를 바꾸지도 않아, 양 플랫폼 모두 옆의 본문과 같은 색과 줄 높이를 상속합니다.

// stacked 에서도 같은 슬롯 — 톤 아이콘 자리에 Badge 를 둔다.
Alert.warning(
  leading: Badge(tone: CoreBadgeTone.warning, child: Text('Beta')),
  title: '미리 보기 기능',
  description: '출시 전까지 동작이 바뀔 수 있습니다.',
)

사용 예 (Flutter)#

Alert.success(
  title: '저장 완료',
  description: '변경 사항이 성공적으로 저장되었습니다.',
  alertStyle: CoreAlertStyle(
    padding: CoreEdgeInsets.symmetric(
      horizontal: CoreSpace.space24,
      vertical: CoreSpace.space16,
    ),
    borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
    titleStyle: CoreTextStyle.token(CoreTextStyles.labelLarge),
    descriptionStyle: CoreTextStyle.token(CoreTextStyles.bodySmall),
    iconStyle: CoreIconStyle(size: CoreIconSize.size20),
  ),
  onDismiss: handleDismiss,
)

사용 예 (Web)#

Alert.success(
  title: '저장 완료',
  description: '변경 사항이 성공적으로 저장되었습니다.',
  alertStyle: CoreAlertStyle(
    padding: CoreEdgeInsets.symmetric(
      horizontal: CoreSpace.space24,
      vertical: CoreSpace.space16,
    ),
    borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
  ),
  onDismiss: handleDismiss,
)

빠른 오버라이드 (Chain)#

이미 만든 Alert 인스턴스에 alertStyle을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. .radius4처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius4 == CoreRadius.radius4) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.

class AlertChainExample extends StatefulWidget {
  const AlertChainExample({super.key});

  @override
  State<AlertChainExample> createState() => _AlertChainExampleState();
}

class _AlertChainExampleState extends State<AlertChainExample> {
  bool _showCombo = true;
  bool _showFull = true;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        if (_showCombo)
          Alert(
            title: 'Heads up!',
            description: 'You can add components to your app using the cli.',
            onDismiss: () => setState(() => _showCombo = false),
          ).radius16.primary
        else
          Button(
            variant: CoreButtonVariant.outline,
            onPressed: () => setState(() => _showCombo = true),
            child: const Text('Show alert'),
          ),
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
        if (_showFull)
          Alert(
            title: 'Heads up!',
            description: 'You can add components to your app using the cli.',
            onDismiss: () => setState(() => _showFull = false),
          ).withStyle(
            const CoreAlertStyle(
              backgroundColor: CoreColor.token(CoreColors.tertiaryContainer),
              foregroundColor: CoreColor.token(
                CoreColors.onTertiaryContainer,
              ),
              borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
              padding: CoreEdgeInsets.symmetric(
                horizontal: CoreSpace.space24,
                vertical: CoreSpace.space8,
              ),
            ),
          )
        else
          Button(
            variant: CoreButtonVariant.outline,
            onPressed: () => setState(() => _showFull = true),
            child: const Text('Show alert'),
          ),
      ],
    );
  }
}
class AlertChainExample extends StatefulComponent {
  const AlertChainExample({super.key});

  @override
  State<AlertChainExample> createState() => _AlertChainExampleState();
}

class _AlertChainExampleState extends State<AlertChainExample> {
  bool _showCombo = true;
  bool _showFull = true;

  @override
  Component build(BuildContext context) {
    return div(
      [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        if (_showCombo)
          Alert(
            title: 'Heads up!',
            description: 'You can add components to your app using the cli.',
            onDismiss: () => setState(() => _showCombo = false),
          ).radius16.primary
        else
          Button(
            variant: CoreButtonVariant.outline,
            onPressed: () => setState(() => _showCombo = true),
            child: const Text('Show alert'),
          ),
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
        if (_showFull)
          Alert(
            title: 'Heads up!',
            description: 'You can add components to your app using the cli.',
            onDismiss: () => setState(() => _showFull = false),
          ).withStyle(
            const CoreAlertStyle(
              backgroundColor: CoreColor.token(CoreColors.tertiaryContainer),
              foregroundColor: CoreColor.token(
                CoreColors.onTertiaryContainer,
              ),
              borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
              padding: CoreEdgeInsets.symmetric(
                horizontal: CoreSpace.space24,
                vertical: CoreSpace.space8,
              ),
            ),
          )
        else
          Button(
            variant: CoreButtonVariant.outline,
            onPressed: () => setState(() => _showFull = true),
            child: const Text('Show alert'),
          ),
      ],
      classes: 'flex flex-col items-start',
    );
  }
}

Named constructors#

  • Alert.info(...), Alert.success(...), Alert.warning(...), Alert.destructive(...) — 각 variant에 대한 편의 생성자.

변형 (Variants)#

Default#

Alert(
  title: 'Heads up!',
  description: '표준 surface 색상을 사용하는 기본 알림입니다.',
)

Info#

Alert.info(
  title: '정보',
  description: '시스템 점검이 예정되어 있습니다.',
)

Success#

Alert.success(
  title: '완료',
  description: '파일이 성공적으로 업로드되었습니다.',
  onDismiss: handleDismiss,
)

Warning#

Alert.warning(
  title: '주의',
  description: '이 작업은 되돌릴 수 없습니다.',
)

Destructive (Error)#

Alert.destructive(
  title: '오류',
  description: '네트워크 연결을 확인해 주세요.',
)

Inline (한 줄 상태 띠)#

Alert(
  layout: CoreAlertLayout.inline,
  leading: Badge(
    tone: CoreBadgeTone.success,
    showDot: true,
    child: Text('Synced'),
  ),
  title: 'All changes saved to the cloud.',
  action: Icon(
    LucideIcons.chevronDown,
    iconStyle: CoreIconStyle(
      size: CoreIconSize.size16,
      color: CoreColor.token(CoreColors.onSurfaceSubtle),
    ),
  ),
)

action 슬롯의 아이콘은 caller 가 그리는 내용이라 색을 직접 지정합니다(onSurfaceSubtle).

텍스트만 (아이콘 없음)#

아이콘 없이 글과 톤만으로 안내하는 제품은 showIcon: false 로 앞자리를 끕니다. 제목이 이미 톤의 색(on{Tone}Container)이라 따로 적을 것이 없습니다 — 면과 제목 색이 톤을 나릅니다.

const Alert.success(
  title: 'Transfer complete',
  description: 'The money arrives within ten minutes.',
  showIcon: false,
)
const Alert.success(
  title: 'Transfer complete',
  description: 'The money arrives within ten minutes.',
  showIcon: false,
)

앱 전체에 적용하려면 같은 CoreAlertTheme 를 앱 루트 테마의 coreComponentTheme 에 둡니다. ThemeData.copyWith 의 coreComponentTheme 인자는 Flutter 가 값을 돌려주는 함수, Web 이 값 자체를 받습니다.

동작 스펙 (Behavior)#

인터랙션#

  • 닫기: onDismiss가 제공되면 우측에 닫기 버튼 표시, 클릭 시 콜백 실행. 실제 숨김 처리는 부모 위젯이 담당.
  • 액션: action은 닫기 버튼 왼쪽에 표시. 버튼 등 임의 위젯을 전달할 수 있습니다.
  • 아이콘: variant별 아이콘(info/success/warning/destructive)이 자동 표시됩니다. leading 을 넘기면 그 위젯이 아이콘을 대신합니다.
  • inline: 제목은 한 줄이며 넘치면 말줄임으로 잘리고, description 은 그리지 않습니다.

토큰#

시각 기본값은 위 CoreAlertStyle 표와 프로젝트 테마에서 해석합니다. 기본 보더 굵기는 0 이며, 보더를 원하면 색과 굵기를 함께 지정합니다. Alert 자체에는 표시 타이머 · 자동 제거 · Escape 닫힘 · 비활성 상태가 없습니다.

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

✅ Do#

메시지의 심각도에 맞는 variant 사용

Alert.warning(
  title: '주의',
  description: '이 작업은 되돌릴 수 없습니다.',
)

색상과 아이콘이 심각도를 즉시 전달하여 사용자가 빠르게 인식합니다.


❌ Don't#

모든 안내 메시지에 destructive variant 사용 금지

// ❌ 일반 안내에 destructive 사용
Alert.destructive(
  title: '새 버전 안내',
  description: '새 버전이 출시되었습니다.',
)

잘못된 variant는 사용자에게 불필요한 긴장감을 주고 신뢰를 낮춥니다.

✅ Do#

일시적 피드백은 onDismiss로 처리

Alert.success(
  title: '저장 완료',
  description: '변경 사항이 저장되었습니다.',
  onDismiss: handleDismiss,
)

사용자가 확인 후 직접 닫을 수 있어 화면 공간을 효율적으로 사용합니다.


❌ Don't#

긴 설명 텍스트를 description에만 모두 넣지 않기

// ❌ 너무 긴 description
Alert.warning(
  title: '시스템 점검 안내',
  description: '시스템 점검으로 인해 2024년 3월 15일 오전 2시부터 4시까지 서비스가 일시 중단됩니다. 이용에 불편을 드려 죄송합니다.',
)

title로 핵심 요약, description으로 보충 설명을 분리해야 스캔하기 쉽습니다.

접근성 (Accessibility)#

역할#

  • Web — 루트 <div> 에 톤이 정한 role 이 붙고, 이름은 aria-label 로 실립니다. 닫기 버튼은 native <button type="button"> 에 로컬라이즈된 aria-label 을 답니다.
  • Flutter — CoUISemantics(role:, label:, container: true). alert 과 status 는 이 플랫폼에서 둘 다 Semantics(liveRegion: true) 한 가지로 접힙니다. 닫기 버튼은 Semantics(label:) 로 감싸집니다.
variantWeb roleFlutter
destructivealertlive region
info / success / warning status live region
neutral없음live region 아님

container: true 는 알림을 하나의 시맨틱 경계로 만듭니다. 다만 explicitChildNodes 를 켜지 않으므로 자체 경계를 갖지 않는 후손은 이 노드로 합쳐집니다 — 제목·본문 텍스트가 한 덩어리로 읽힐 수 있습니다. 버튼처럼 스스로 경계를 갖는 자식은 별도 노드로 남습니다.

키보드#

키FlutterWeb
Tabaction 위젯 → 닫기 버튼 순으로 각각 tab stop동일
Enter / Space포커스된 버튼 활성화포커스된 버튼 활성화
Escape 로 닫기없음없음

알림 상자 자체는 포커스를 받지 않습니다 — 양 플랫폼 모두 루트에 tabindex 나 Focus 를 두지 않습니다. 버튼 활성화는 Flutter 가 Clickable 의 Enter/Space 단축키를, Web 이 native <button> 의 브라우저 기본 동작을 탑니다.

스크린 리더#

이름은 필수 title 문자열입니다. 제목 생략용 기본 라벨 경로는 없습니다. description 은 그 안의 텍스트 노드로 읽힙니다. inline 은 설명을 그리지 않고, 제목이 말줄임으로 잘려도 이름은 전체 title 입니다.

통보 긴급도는 톤이 정합니다 — 실패만 읽던 문장을 끊고(alert), 나머지 톤은 차례를 기다리며(status), 중립 variant 는 아무것도 통보하지 않습니다. 다만 이 세 단계는 Web 에서만 구분됩니다.

닫기 버튼은 양 플랫폼 모두 로컬라이즈된 이름을 갖습니다.

포커스 관리#

  • 진입 — 없음. 알림은 오버레이가 아니라 인라인 상자라, 나타나도 포커스가 옮겨가지 않습니다.
  • 이탈 — 없음. onDismiss 로 알림을 트리에서 걷어내면 닫기 버튼에 있던 포커스가 사라지고, 어디로 돌려놓을지는 호출자 몫입니다.
  • 트랩 — 없음.

알려진 제약#

  • Flutter 는 통보의 긴급도를 구분하지 못합니다. alert 과 status 가 같은 live region 하나로 접혀, destructive 알림도 다른 톤과 똑같은 방식으로 통보됩니다. 미구현이 아니라 플랫폼에 통보 축이 하나뿐인 데서 오는 차이입니다.
  • 중립 variant 의 이름은 Web 에서 노출되지 않습니다. role 이 붙지 않아 루트가 generic 요소로 남고, generic 요소의 aria-label 은 보조기술이 읽지 않습니다. 같은 조건에서 Flutter 는 Semantics.label 이 그대로 붙습니다.
  • Web 루트의 aria-label 은 caller 가 덮을 수 없습니다. attributes 로 넘긴 값 위에 컴포넌트 값이 나중에 병합됩니다. 이름을 바꾸려면 title 을 씁니다 (role 은 configureAttributes 를 거치므로 caller 가 덮을 수 있습니다).
  • Dark 에서 neutral Alert 은 surfaceBright 면 위에서 경계가 사라집니다. Dark 의 surfaceContainer 와 surfaceBright 는 같은 색(neutral 900)이라, Dialog · Popup 같은 떠 있는 패널 위에 놓은 neutral Alert 은 (stacked · inline 모두) 면과 패널이 1.00:1 로 맞붙어 보입니다. 한 줄 상태 띠(inline)를 패널 안에 둘 때 가장 자주 만나는 자리입니다. 톤이 있는 variant(info / success / warning / destructive)는 자기 컨테이너 색을 가져 영향이 없고, Light 는 두 면이 다른 색이라 영향이 없습니다. 이 상태는 그대로 두고 기록만 합니다 — 패널 면이나 Alert 면을 바꾸는 것은 색 매핑의 결정입니다.

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

title / description / variant / layout / leading / onDismiss / action 은 양 플랫폼 동일 API 입니다. 아래는 플랫폼 고유 차이점만 나열합니다.

항목FlutterWeb
action · leading 타입Widget?Component?
콜백 타입VoidCallback?CoreVoidCallback?
통보 축 live region 유무 (polite/assertive 구분 없음) role="alert" / role="status" 로 급함까지 구분
닫기 버튼 영역제목 줄 높이로 제약제목 줄에 정렬하되 버튼은 줄 밖으로 넘쳐 원래 히트 영역 유지
leading 의 아이콘 테마 IconTheme IconTheme (같은 이름, 같은 색 · 크기). 아이콘이 아닌 내용은 두 플랫폼 모두 색을 받지 않습니다
inline 제목 말줄임 maxLines: 1 + TextOverflow.ellipsis truncate ( overflow: hidden · text-overflow: ellipsis · white-space: nowrap )
테마 덮기 (ThemeData.copyWith) coreComponentTheme: () => … — 값을 돌려주는 함수 coreComponentTheme: … — 값

actionButtonStyle 전달은 현재 Web의 Button 액션에만 적용되고 Flutter는 액션을 그대로 렌더합니다. 공통 스타일 슬롯의 배선 결손이며, 의도된 지원 차이로 취급하지 않습니다.

  • Banner: 페이지 최상단 전체 폭 공지에 사용
  • Toast: 자동으로 사라지는 일시적 알림에 사용
  • Dialog: 사용자 확인이 필요한 경고 메시지에 사용

조합 예제#

// 폼 제출 후 결과 표시
Column(
  children: [
    if (submitError != null)
      Alert.destructive(
        title: '오류',
        description: submitError!,
        onDismiss: handleErrorDismissed,
      ),
    // ... form fields
  ],
)