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#
코드 보기
class BannerDefaultExample extends StatefulComponent {
const BannerDefaultExample({super.key});
@override
State<BannerDefaultExample> createState() => _BannerDefaultExampleState();
}
class _BannerDefaultExampleState extends State<BannerDefaultExample> {
bool _show = true;
@override
Component build(BuildContext context) {
if (_show) {
return Banner.info(
message: const Text('This is an informational message.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
class BannerDefaultExample extends StatefulWidget {
const BannerDefaultExample({super.key});
@override
State<BannerDefaultExample> createState() => _BannerDefaultExampleState();
}
class _BannerDefaultExampleState extends State<BannerDefaultExample> {
bool _show = true;
@override
Widget build(BuildContext context) {
if (_show) {
return Banner.info(
message: const Text('This is an informational message.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
위 갤러리의
solid는 variant 가 아니라 emphasis 입니다 — 프리뷰 갤러리가 한 축만 받아서 같이 늘어놓은 것뿐이고, 실제로는variant × emphasis두 축이 곱해집니다.
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 페이지 전체에 영향을 미치는 시스템 상태 메시지를 표시할 때 (점검 공지, 서비스 알림)
- 사용자가 취해야 할 중요한 액션을 안내할 때 (세션 만료 경고, 구독 만료 알림)
- 사용자가 방금 완료한 작업의 결과를 페이지 상단에 지속적으로 표시할 때
대신 다른 컴포넌트를 사용하세요:
- 필드 값의 검증 결과이면 상단 배너 대신 FormField / ValidatorHint 의 필드 옆 안내를 사용합니다
- Toast: 페이지 흐름에 남는 스트립 없이 임시 오버레이 피드백이 필요할 때
- Alert: 페이지 상단 공지 대신 남아 있는 콘텐츠 옆에 메시지·복구 액션을 붙일 때
- Dialog: 알림 표시를 넘어 모달에서 사용자의 응답을 받아야 할 때
Props / Parameters#
W 는 Flutter의 Widget, Web의 Jaspr Component 입니다. W?
는 선택 슬롯입니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
message | W | 필수 | 배너에 표시할 메시지 |
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 전환은 전부 nestedcloseButtonStyle슬롯)
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), Webaria-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 배치는 어느 쪽에도 없습니다.
| 항목 | Flutter | Web |
|---|---|---|
message / description / action 타입 |
Widget |
Component |
onDismiss 콜백 타입 |
VoidCallback? |
CoreVoidCallback? |
| 렌더링 | SurfaceBlur + Container + Row |
루트 <div> flex |
| 의미 경계 | Semantics(container: true, label:) |
role="banner" 랜드마크 + aria-label |
| 표시·고정 배치 | 호출부가 트리 위치·표시 상태를 관리 | 호출부가 DOM 위치·sticky 배치를 관리 |