Alert | CoUI
LogoCoUI

Alert

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

Alert#

다양한 상태(정보, 성공, 경고, 오류)를 시각적으로 구분하여 메시지를 표시하는 알림 영역 컴포넌트입니다. Flutter와 Web이 동일한 named properties API(title, description, variant, onDismiss, action)를 사용합니다.

Live Preview#

사용 시기 (When to Use)#

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

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

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

  • Toast: 일시적으로 나타났다 사라지는 짧은 알림에는 Toast 사용
  • Banner: 페이지 최상단 전체 폭으로 중요 공지를 표시할 때 사용
  • Dialog: 사용자의 확인 액션이 필요한 경고성 메시지에는 Dialog 사용

기본 사용법 (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: '요청을 처리하는 중 문제가 발생했습니다.',
)
// 기본 알림
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: '요청을 처리하는 중 문제가 발생했습니다.',
)

Props / Parameters#

속성타입기본값설명
title String 필수 알림 제목 — 이 컴포넌트가 반드시 갖는 한 줄
description String? null 제목 아래 보조 설명 (선택)
variant CoreAlertVariant defaultVariant 알림 종류 ( defaultVariant / info / success / warning / destructive )
onDismiss VoidCallback? (Flutter) / CoreVoidCallback? (Web) null 닫기 버튼 클릭 핸들러. null이면 닫기 버튼 미표시
action Widget? (Flutter) / Component? (Web) null 알림 우측에 표시할 액션 위젯
alertStyle CoreAlertStyle? null 인스턴스 스타일 (Style 시스템 참조)

스타일 시스템 (Style System)#

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

시맨틱 vs 스타일#

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

Resolve chain#

design system default for alert
  → CoreAlertTheme.style                   // 프로젝트 공통
  → parent component slot override
  → widget.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 → [defaultTitleColor]. Neutral on every variant by default — the icon carries the tone, so the title does not have to, and that is where its contrast headroom comes from. Stated as a field anyway: a caller who wants a toned title should not have to reach past the Style slot to get one.
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.
paddingCoreEdgeInsets?Padding.
minHeight double? Minimum box height (logical px). null → [defaultMinHeight].
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. null defers to [defaultIconContentGapStyle].
titleBottomMargin double? Bottom margin between the title and description (logical px). null defers to [defaultTitleBottomMargin].
titleStyle CoreTextStyle? Title text style override.
descriptionStyle CoreTextStyle? Description text style override.
iconStyle CoreIconStyle? Leading icon style override.
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).
dismissIdleOpacity double? Deprecated since 0.131 — the dismiss glyph no longer dims. Was an idle-state opacity multiplier over the variant-tinted icon colour. An opacity times a tint resolves against whatever panel sits behind it, so no guard could check the result; a token can. Override closeButtonStyle.foregroundColor / hoverForegroundColor instead. Stated in the doc comment rather than left to the annotation alone: the docs-table generator skips annotations entirely, so a deprecation that lives only in @Deprecated(...) never reaches the published table.

CoreAlertStyle 변형별 기본값 (CoreAlertVariantStyle)#

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

제목은 variant 와 무관하게 항상 onSurface 입니다 — 톤은 아이콘과 본문이 나르고, 제목은 나르지 않습니다. 제목이 톤을 나르지 않는 덕분에 대비 여유가 생깁니다.

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

레이아웃 — 아이콘은 타이틀 줄에 앉습니다#

Alert 은 최소 높이(minHeight, 72)를 갖고, 그 안에서 내용이 세로 중앙에 옵니다. 그 안쪽 행은 다시 위쪽 정렬이라, 아이콘과 닫기 표시가 타이틀의 line box 에 맞춰집니다.

두 박스인 이유가 있습니다. 하나로 합치면 둘 중 하나를 골라야 합니다 — 행을 중앙 정렬하면 아이콘이 제목과 본문 사이에 놓이고, 위쪽 정렬하면 제목만 있는 Alert 이 최소 높이가 열어둔 박스의 맨 위에 붙습니다. 두 경우가 동시에 맞는 배치는 이 구조뿐입니다.

아이콘 슬롯 높이는 토큰이 아니라 resolver 가 타이틀에서 파생합니다. 타이포가 바뀌면 따라옵니다.

사용 예 (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: '네트워크 연결을 확인해 주세요.',
)

동작 스펙 (Behavior)#

인터랙션#

  • 닫기: onDismiss가 제공되면 우측에 닫기 버튼 표시, 클릭 시 콜백 실행. 실제 숨김 처리는 부모 위젯이 담당.
  • 액션: action은 닫기 버튼 왼쪽에 표시. 버튼 등 임의 위젯을 전달할 수 있습니다.
  • 아이콘: variant별 아이콘(info/success/warning/destructive)이 자동 표시됩니다.

토큰#

  • Border radius: CoreRadius.radius16 (16px)
  • Border width: CoreStrokeWidth.stroke1 (1px)
  • Padding: CoreSpace.space12 세로 × CoreSpace.space16 가로
  • Icon size: CoreIconSize.size16 (16px)
  • Icon gap: CoreSpace.space12 (12px)
  • 제목↔설명 여백: CoreSpace.space4 (4px)

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

✅ Do#

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

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

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


❌ Don't#

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

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

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

✅ Do#

일시적 피드백은 onDismiss로 처리

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

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


❌ Don't#

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

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

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

접근성 (Accessibility)#

스크린 리더#

통보 방식은 variant 의 톤이 정합니다 — 모든 알림이 읽던 문장을 끊지는 않습니다.

variantWebFlutter
destructiverole="alert" (assertive)live region
info / success / warning role="status" (polite) live region
defaultVariant역할 없음live region 아님

라벨은 title (없으면 로컬라이즈된 기본 알림 라벨) 이 채우고, Flutter 는 container: true 로 한 덩어리로 읽힙니다.

키보드 인터랙션#

동작
Tab닫기 버튼 / action 위젯으로 포커스 이동
Enter / Space포커스된 버튼 활성화

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

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

항목FlutterWeb
action 타입Widget?Component?
콜백 타입VoidCallback?CoreVoidCallback?
통보 축 live region 유무 (polite/assertive 구분 없음) role="alert" / role="status" 로 급함까지 구분
  • Banner: 페이지 최상단 전체 폭 공지에 사용
  • Toast: 자동으로 사라지는 일시적 알림에 사용
  • Dialog: 사용자 확인이 필요한 경고 메시지에 사용

조합 예제#

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