Banner | CoUI
LogoCoUI

Banner

페이지 상단에 표시되는 알림 배너 컴포넌트

Banner#

페이지 상단에 중요한 정보, 경고, 오류 등을 표시하는 배너 컴포넌트입니다. Banner는 Flutter와 Web에서 동일한 named properties API를 제공하며, default/info/success/warning/destructive 5개 variant를 지원합니다.

Live Preview#

위 갤러리의 solid 는 variant 가 아니라 emphasis 입니다 — 프리뷰 갤러리가 한 축만 받아서 같이 늘어놓은 것뿐이고, 실제로는 variant × emphasis 두 축이 곱해집니다.

사용 시기 (When to Use)#

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

  • 페이지 전체에 영향을 미치는 시스템 상태 메시지를 표시할 때 (점검 공지, 서비스 알림)
  • 사용자가 취해야 할 중요한 액션을 안내할 때 (세션 만료 경고, 구독 만료 알림)
  • 사용자가 방금 완료한 작업의 결과를 페이지 상단에 지속적으로 표시할 때

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

  • Toast: 일시적으로 나타났다가 자동으로 사라지는 알림이 필요할 때
  • Alert: 특정 콘텐츠 영역 내에 인라인으로 표시하는 경고 메시지가 필요할 때
  • Dialog: 사용자의 즉각적인 응답이 필요한 중요한 알림에는 모달 다이얼로그 사용

기본 사용법 (Basic Usage)#

CoUI는 Flutter와 Web에서 동일한 API를 제공합니다. 아래 예제 코드는 양쪽 플랫폼에서 그대로 사용할 수 있습니다.

// 정보 배너
Banner.info(
  message: Text('시스템 점검이 예정되어 있습니다.'),
)

// 성공 배너 (해제 버튼 포함)
Banner.success(
  message: Text('저장이 완료되었습니다.'),
  onDismiss: handleDismiss,
)

// 경고 배너 (액션 포함)
Banner.warning(
  message: Text('세션이 곧 만료됩니다.'),
  description: Text('지금 연장하면 작업을 이어서 할 수 있습니다.'),
  action: Button(
    variant: CoreButtonVariant.outline,
    onPressed: handleExtendSession,
    child: Text('연장하기'),
  ),
  onDismiss: handleDismiss,
)

// 오류 배너
Banner.destructive(
  message: Text('요청을 처리하는 중 오류가 발생했습니다.'),
  onDismiss: handleDismiss,
)

Props / Parameters#

속성타입기본값설명
message Widget (Flutter) / Component (Web) 필수 배너에 표시할 메시지
description Widget? (Flutter) / Component? (Web) null 메시지 아래에 표시되는 선택적 보조 설명
variant CoreBannerVariant neutral 무엇을 말하는가neutral , info , success , warning , destructive
emphasis CoreBannerEmphasis weak 얼마나 세게 말하는가weak(톤 틴트 + 하단 라인), solid(톤 통짜 채움)
onDismiss VoidCallback? (Flutter) / CoreVoidCallback? (Web) null 닫기 버튼 클릭 핸들러. null이면 닫기 버튼 미표시
action Widget? (Flutter) / Component? (Web) null 배너 우측에 표시할 액션 위젯
showIcon bool true variant 별 기본 아이콘 표시 여부
bannerStyle CoreBannerStyle? null 인스턴스 스타일 (Style 시스템 참조)

스타일 시스템 (Style System)#

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

시맨틱 vs 스타일#

  • 시맨틱 enum / behaviour: 위젯/컴포넌트 파라미터로 직접 (variant, message, description, action, onDismiss, showIcon)
  • chrome / dimensional / 슬롯 스타일: CoreBannerStyle 한 곳으로 (backgroundColor / foregroundColor / borderColor / borderWidth / borderRadius / padding / iconContentGapStyle / contentDismissGapStyle / titleDescriptionGapStyle / titleStyle / descriptionStyle / iconStyle / actionButtonStyle / closeButtonStyle — 닫기 버튼의 패딩·모서리·아이콘·hover 전환은 전부 nested closeButtonStyle 슬롯)

Resolve chain#

design system default for banner
  → CoreBannerTheme.style                  // 프로젝트 공통
  → parent component slot override
  → widget.bannerStyle                     // 인스턴스별

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

CoreBannerStyle 필드#

필드타입설명
backgroundColor CoreColor? Banner panel background fill colour.
foregroundColor CoreColor? Foreground colour applied to the title / description / icon when the per-slot style does not override it.
borderColor CoreColor? Bottom-rule colour. null suppresses the rule entirely.
borderWidth double? Bottom-rule thickness (logical px). null defers to [defaultBorderWidth]. See that constant for why the rule is bottom-only and why it must not add to the band's height.
borderRadius CoreBorderRadius? Border radius. null defers to [defaultBorderRadius].
minHeight double? Minimum band height (logical px). null defers to [defaultMinHeight].
padding CoreEdgeInsets? Padding. null defers to [defaultPadding].
iconContentGapStyle CoreGapStyle? Nested gap style between leading icon / text column / trailing slots — forwarded to Gap(gapStyle: …) by the resolver. null defers to [defaultIconContentGapStyle].
contentDismissGapStyle CoreGapStyle? Nested gap style before the dismiss button — forwarded to Gap(gapStyle: …) by the resolver. null defers to [defaultContentDismissGapStyle].
titleDescriptionGapStyle CoreGapStyle? Nested vertical gap style between the title and description — forwarded to Gap(gapStyle: …) by the resolver. null defers to [defaultTitleDescriptionGapStyle].
titleStyle CoreTextStyle? Title text style override.
descriptionStyle CoreTextStyle? Description text style override.
iconStyle CoreIconStyle? Leading icon style override.
actionButtonStyle CoreButtonStyle? Action ( action: ) button chrome — merged onto the variant's own foreground (which both resolvers inject) and raw-forwarded to the action when it is a Button , so a caller's fields still win.
closeButtonStyle CoreButtonStyle? Close ( onDismiss ) button chrome — recursively merged onto [defaultCloseButtonStyle] (plus the resolver's variant-foreground injection) and raw-forwarded to Button(variant:.plain, buttonStyle: …) on both platforms.

사용 예 (Flutter)#

Banner.warning(
  message: Text('세션이 곧 만료됩니다.'),
  description: Text('지금 연장하면 작업을 이어서 할 수 있습니다.'),
  bannerStyle: CoreBannerStyle(
    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)#

Banner.warning(
  message: Text('세션이 곧 만료됩니다.'),
  description: Text('지금 연장하면 작업을 이어서 할 수 있습니다.'),
  bannerStyle: CoreBannerStyle(
    padding: CoreEdgeInsets.symmetric(
      horizontal: CoreSpace.space24,
      vertical: CoreSpace.space16,
    ),
    borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
  ),
  onDismiss: handleDismiss,
)

빠른 오버라이드 (Chain)#

이미 만든 Banner 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다. .radius4처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius4 == CoreRadius.radius4) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.

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

  @override
  State<BannerChainExample> createState() => _BannerChainExampleState();
}

class _BannerChainExampleState extends State<BannerChainExample> {
  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)
          Banner.info(
            message: const Text('This is an informational message.'),
            onDismiss: () => setState(() => _showCombo = false),
          ).radius16.primary
        else
          Button(
            variant: CoreButtonVariant.outline,
            onPressed: () => setState(() => _showCombo = true),
            child: const Text('Show banner'),
          ),
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
        if (_showFull)
          Banner.info(
            message: const Text('This is an informational message.'),
            onDismiss: () => setState(() => _showFull = false),
          ).withStyle(
            const CoreBannerStyle(
              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 banner'),
          ),
      ],
    );
  }
}
class BannerChainExample extends StatefulComponent {
  const BannerChainExample({super.key});

  @override
  State<BannerChainExample> createState() => _BannerChainExampleState();
}

class _BannerChainExampleState extends State<BannerChainExample> {
  bool _showCombo = true;
  bool _showFull = true;

  @override
  Component build(BuildContext context) {
    return div(
      [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        if (_showCombo)
          Banner.info(
            message: const Text('This is an informational message.'),
            onDismiss: () => setState(() => _showCombo = false),
          ).radius16.primary
        else
          Button(
            variant: CoreButtonVariant.outline,
            onPressed: () => setState(() => _showCombo = true),
            child: const Text('Show banner'),
          ),
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
        if (_showFull)
          Banner.info(
            message: const Text('This is an informational message.'),
            onDismiss: () => setState(() => _showFull = false),
          ).withStyle(
            const CoreBannerStyle(
              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 banner'),
          ),
      ],
      classes: 'flex flex-col items-start',
    );
  }
}

두 축 — variant 와 emphasis#

Banner 는 축이 둘입니다. variant 는 무엇을 말하는지(정보 / 성공 / 경고 / 파괴적)를, emphasis 는 얼마나 세게 말하는지를 정합니다. 둘은 곱해집니다 — destructiveweak 로도 solid 로도 존재합니다.

emphasis채움하단 라인언제
weak (기본)톤의 옅은 컨테이너있음대부분의 경우
solid 톤 자체로 통짜 없음 사용자가 놓치면 안 되는 것 — 장애, 강제 점검

solid 는 아껴 쓰십시오. 일상적인 안내에 쓰면 신호가 닳아서, 정작 급한 일이 생겼을 때 쓸 수 있는 강도가 남지 않습니다.

Banner(
  variant: CoreBannerVariant.destructive,
  emphasis: CoreBannerEmphasis.solid,
  message: Text('결제가 일시적으로 불가합니다.'),
  description: Text('복구 중입니다. 별도 조치는 필요 없습니다.'),
)

변형 (Variants)#

Info#

일반적인 정보 전달에 사용합니다.

Banner.info(
  message: Text('새로운 업데이트가 있습니다.'),
)

Success#

성공적인 작업 완료를 알릴 때 사용합니다.

Banner.success(
  message: Text('변경 사항이 저장되었습니다.'),
)

Warning#

사용자의 주의가 필요한 상황에 사용합니다.

Banner.warning(
  message: Text('구독이 7일 후 만료됩니다.'),
)

Destructive (Error)#

오류 또는 실패 상태를 알릴 때 사용합니다.

Banner.destructive(
  message: Text('네트워크 연결에 실패했습니다.'),
)

동작 스펙 (Behavior)#

인터랙션#

  • 닫기 버튼 클릭: onDismiss 콜백 실행. 배너 숨김 처리는 부모 위젯에서 상태로 관리
  • 액션 버튼 클릭: action 위젯에 정의된 콜백 실행
  • 호버: 닫기 버튼 opacity 상승

상태 전환#

  • 배너 표시: 슬라이드 다운 또는 페이드인 (부모에서 AnimatedSwitcher 사용 권장)
  • 배너 닫기: onDismiss 호출 후 부모에서 상태를 변경하여 제거

테마 오버라이드#

프로젝트 레벨에서 CoreComponentTheme.banner로 기본값을 재정의할 수 있습니다.

CoreComponentTheme(
  banner: CoreBannerTheme(
    style: CoreBannerStyle(
      backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
      borderColor: CoreColor.token(CoreColors.primary),
      borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
    ),
  ),
)

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

✅ Do#

중요도에 맞는 variant 사용

// 시스템 점검 공지: info
Banner.info(
  message: Text('2026-03-15 02:00~04:00 시스템 점검이 예정되어 있습니다.'),
)

// 보안 경고: warning
Banner.warning(
  message: Text('비밀번호를 90일 이상 변경하지 않았습니다.'),
  action: Button(
    variant: CoreButtonVariant.outline,
    onPressed: handleChangePassword,
    child: Text('변경하기'),
  ),
)

올바른 variant를 사용하면 사용자가 메시지의 중요도를 즉시 파악할 수 있습니다.


❌ Don't#

여러 배너를 동시에 표시 금지

// ❌ 여러 배너 동시 표시
Column(
  children: [
    Banner.info(message: Text('점검 예정')),
    Banner.success(message: Text('저장 완료')),
    Banner.warning(message: Text('세션 만료')),
  ],
)

여러 배너가 동시에 표시되면 사용자에게 압도적인 느낌을 주고 콘텐츠 영역을 지나치게 차지합니다. 가장 중요한 하나만 표시하세요.

✅ Do#

해제 가능한 배너에는 항상 onDismiss 제공

Banner.info(
  message: Text('새로운 기능이 추가되었습니다. 지금 확인해보세요!'),
  onDismiss: handleDismissBanner,
  action: Button(
    variant: CoreButtonVariant.outline,
    onPressed: handleLearnMore,
    child: Text('자세히 보기'),
  ),
)

사용자가 불필요한 배너를 닫을 수 있어야 콘텐츠에 집중할 수 있습니다.


❌ Don't#

destructive variant를 일반 정보 표시에 남용 금지

// ❌ 단순 안내에 destructive variant 사용
Banner.destructive(
  message: Text('이 기능은 프리미엄 회원 전용입니다.'),
)

destructive variant는 실제 오류 상황에만 사용해야 합니다. 남용하면 사용자가 진짜 오류를 놓칠 수 있습니다.

✅ Do#

액션이 필요한 배너에는 action 버튼을 추가하세요.

Banner.warning(
  message: Text('구독이 3일 후 만료됩니다.'),
  action: Button(
    variant: CoreButtonVariant.outline,
    onPressed: handleRenewSubscription,
    child: Text('갱신하기'),
  ),
  onDismiss: handleDismiss,
)

사용자가 즉각적인 조치를 취할 수 있도록 명확한 액션 버튼을 제공하면 전환율이 높아집니다.

접근성 (Accessibility)#

키보드 인터랙션#

동작
Tab배너 내 인터랙티브 요소(액션 버튼, 닫기 버튼)로 이동
Enter / Space포커스된 버튼 활성화

스크린 리더#

  • Flutter: Semantics(container: true) + 로컬라이즈된 배너 라벨(CoUILocalizations.bannerLabel)이 적용됩니다
  • Web: role="banner" + 같은 로컬라이제이션 멤버(CouiLocalizations.bannerLabel)에서 온 aria-label 이 루트에 적용됩니다

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

Banner는 Flutter와 Web에서 동일한 named properties API를 사용합니다. 플랫폼별로 타입이 다른 부분만 차이가 있습니다.

항목FlutterWeb
message / description / action 타입 Widget Component
onDismiss 콜백 타입 VoidCallback? CoreVoidCallback?
배치 방식 Column 최상단 또는 Scaffold 내 삽입 페이지 상단 고정 (position: sticky)
애니메이션AnimatedSwitcher 등 Flutter 애니메이션CSS transition
  • Toast: 자동으로 사라지는 일시적 알림에 사용
  • Alert: 특정 섹션 내 인라인 메시지에 사용
  • Status: 시스템이나 사용자 상태 표시에 사용