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

Banner

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

목차

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

기본 사용법 (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,
)

Live Preview#

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

사용 시기 (When to Use)#

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

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

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

  • 필드 값의 검증 결과이면 상단 배너 대신 FormField / ValidatorHint 의 필드 옆 안내를 사용합니다
  • Toast: 페이지 흐름에 남는 스트립 없이 임시 오버레이 피드백이 필요할 때
  • Alert: 페이지 상단 공지 대신 남아 있는 콘텐츠 옆에 메시지·복구 액션을 붙일 때
  • Dialog: 알림 표시를 넘어 모달에서 사용자의 응답을 받아야 할 때

Props / Parameters#

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

속성타입기본값설명
messageW필수배너에 표시할 메시지
description W? null 메시지 아래에 표시되는 선택적 보조 설명
variant CoreBannerVariant neutral 무엇을 말하는가 — neutral , info , success , warning , destructive
emphasis CoreBannerEmphasis weak 얼마나 세게 말하는가 — weak(톤 틴트 + 하단 라인), solid(톤 통짜 채움)
onDismiss VoidCallback? (Flutter) / CoreVoidCallback? (Web) null 닫기 버튼 클릭 핸들러. null이면 닫기 버튼 미표시
action W? 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',
    );
  }
}

테마 오버라이드#

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

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

두 축 — variant 와 emphasis#

Banner 는 축이 둘입니다. variant 는 무엇을 말하는지(정보 / 성공 / 경고 / 파괴적)를, emphasis 는 얼마나 세게 말하는지를 정합니다. 둘은 곱해집니다 — destructive 는 weak 로도 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 위젯에 정의된 콜백 실행
  • 호버: 합성된 Button(variant: plain) 의 전경색·포커스 동작을 사용

상태 전환#

  • 배너 자체는 타이머 · 등장/퇴장 애니메이션 · 화면 상단 고정 배치를 소유하지 않습니다. 필요한 표시 상태 · 애니메이션 · sticky 배치는 호출부가 제공합니다
  • 배너 닫기: onDismiss 호출 후 부모에서 상태를 변경하여 제거

사용 가이드라인 (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)#

역할#

  • Flutter: 루트에 Semantics(container: true, label: CoUILocalizations.bannerLabel) 가 붙습니다.
  • Web: 루트 <div> 에 role="banner" + 같은 로컬라이제이션 멤버(CouiLocalizations.bannerLabel)에서 온 aria-label 이 붙습니다. 두 속성은 caller attributes 뒤에 병합되어 호출부가 덮어쓸 수 없습니다.
  • variant 아이콘은 장식으로 취급됩니다 — Web 은 아이콘을 감싼 <span> 에 aria-hidden="true" 를 주고, Flutter 의 Icon 은 시맨틱 노드를 만들지 않습니다.
  • 닫기 버튼은 양 플랫폼 모두 이름을 받습니다 — Flutter Semantics(label: CoUILocalizations.dismissLabel), Web aria-label(CouiLocalizations.dismissLabel).

키보드#

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

Escape 로 배너를 닫는 처리는 양 플랫폼 어디에도 없습니다.

스크린 리더#

  • 컨테이너의 접근 가능한 이름은 로컬라이즈된 고정 문자열(bannerLabel — 영어 "Banner", 한국어 "배너")이며 message 내용을 반영하지 않습니다.
  • 본문은 message / description 슬롯에 넘긴 위젯의 텍스트에서 읽힙니다.
  • 배너는 live region 이 아닙니다 — Flutter 에 liveRegion 설정이 없고 Web 의 role="banner" 는 랜드마크일 뿐이어서, 화면에 새로 나타나도 자동으로 통보되지 않습니다. 사용자가 그 지점까지 이동해야 읽힙니다.

포커스 관리#

  • 진입: 오버레이가 아니므로 포커스 이동이 없습니다. 마운트 시 자동 포커스도 없습니다.
  • 이탈: onDismiss 는 호출부 콜백일 뿐이라 컴포넌트가 포커스를 되돌리지 않습니다. 닫기 버튼에 포커스가 있는 상태에서 배너를 트리에서 제거하면 포커스를 잃은 상태가 되므로, 배너를 지우는 쪽에서 포커스를 옮겨야 합니다.
  • 트랩: 없습니다.
  • 포커스 표시: solid 강조의 배너는 채움이 primary·상태 색이라 닫기·액션 버튼의 링이 그 색과 맞닿으므로, 링과 버튼 사이 2px 간격을 surface 밴드로 칠합니다. 기본값은 CoreBannerStyle.defaultsByVariant 의 solid 행 focusOutlineStyle 에 있고, weak 강조 행은 밴드를 켜지 않아 간격이 투명합니다. 호출자가 버튼에 준 스타일이 그 위에 머지됩니다.

알려진 제약#

  • variant 가 접근성 트리에 나타나지 않습니다 — 정보 / 성공 / 경고 / 오류를 구분하는 것은 색과 아이콘인데, 아이콘은 Web 에서 aria-hidden="true" 이고 Flutter 에서는 시맨틱 노드를 만들지 않습니다. 컨테이너 라벨도 variant 와 무관한 고정 "Banner" 입니다. — 대안: 심각도를 message 슬롯 텍스트에 직접 담습니다(예: Text('오류: 세션이 만료되었습니다')).
  • 컨테이너 라벨을 바꿀 수 없습니다 — Web 의 role / aria-label 도 Flutter 의 Semantics(label:) 도 컴포넌트 내부에서 고정되고, Web 은 caller attributes 뒤에 병합되어 override 되지 않습니다. 한 화면에 배너를 여러 개 두면 스크린 리더의 랜드마크 목록에 같은 이름으로 여러 개가 나열됩니다. — 대안 없음.

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

메시지·액션·닫기 콜백의 의미는 공통이며, 자동 sticky 배치는 어느 쪽에도 없습니다.

항목FlutterWeb
message / description / action 타입 Widget Component
onDismiss 콜백 타입 VoidCallback? CoreVoidCallback?
렌더링 SurfaceBlur + Container + Row 루트 <div> flex
의미 경계 Semantics(container: true, label:) role="banner" 랜드마크 + aria-label
표시·고정 배치호출부가 트리 위치·표시 상태를 관리호출부가 DOM 위치·sticky 배치를 관리
  • Toast: 자동으로 사라지는 일시적 알림에 사용
  • Alert: 특정 섹션 내 인라인 메시지에 사용
  • Status: 시스템이나 사용자 상태 표시에 사용