Banner#
페이지 상단에 중요한 정보, 경고, 오류 등을 표시하는 배너 컴포넌트입니다. Banner는 Flutter와 Web에서 동일한 named properties API를 제공하며,
default/info/success/warning/destructive
5개 variant를 지원합니다.
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'),
);
}
}
class BannerInfoExample extends StatefulComponent {
const BannerInfoExample({super.key});
@override
State<BannerInfoExample> createState() => _BannerInfoExampleState();
}
class _BannerInfoExampleState extends State<BannerInfoExample> {
bool _show = true;
@override
Component build(BuildContext context) {
if (_show) {
return Banner.info(
message: const Text('새로운 업데이트가 준비되었습니다.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
class BannerInfoExample extends StatefulWidget {
const BannerInfoExample({super.key});
@override
State<BannerInfoExample> createState() => _BannerInfoExampleState();
}
class _BannerInfoExampleState extends State<BannerInfoExample> {
bool _show = true;
@override
Widget build(BuildContext context) {
if (_show) {
return Banner.info(
message: const Text('새로운 업데이트가 준비되었습니다.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
class BannerWarningExample extends StatefulComponent {
const BannerWarningExample({super.key});
@override
State<BannerWarningExample> createState() => _BannerWarningExampleState();
}
class _BannerWarningExampleState extends State<BannerWarningExample> {
bool _show = true;
@override
Component build(BuildContext context) {
if (_show) {
return Banner.warning(
message: const Text('세션이 5분 후에 만료됩니다.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
class BannerWarningExample extends StatefulWidget {
const BannerWarningExample({super.key});
@override
State<BannerWarningExample> createState() => _BannerWarningExampleState();
}
class _BannerWarningExampleState extends State<BannerWarningExample> {
bool _show = true;
@override
Widget build(BuildContext context) {
if (_show) {
return Banner.warning(
message: const Text('세션이 5분 후에 만료됩니다.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
class BannerDestructiveExample extends StatefulComponent {
const BannerDestructiveExample({super.key});
@override
State<BannerDestructiveExample> createState() =>
_BannerDestructiveExampleState();
}
class _BannerDestructiveExampleState extends State<BannerDestructiveExample> {
bool _show = true;
@override
Component build(BuildContext context) {
if (_show) {
return Banner.destructive(
message: const Text('요청을 처리하는 중 문제가 발생했습니다.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
class BannerDestructiveExample extends StatefulWidget {
const BannerDestructiveExample({super.key});
@override
State<BannerDestructiveExample> createState() =>
_BannerDestructiveExampleState();
}
class _BannerDestructiveExampleState extends State<BannerDestructiveExample> {
bool _show = true;
@override
Widget build(BuildContext context) {
if (_show) {
return Banner.destructive(
message: const Text('요청을 처리하는 중 문제가 발생했습니다.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
class BannerSuccessExample extends StatefulComponent {
const BannerSuccessExample({super.key});
@override
State<BannerSuccessExample> createState() => _BannerSuccessExampleState();
}
class _BannerSuccessExampleState extends State<BannerSuccessExample> {
bool _show = true;
@override
Component build(BuildContext context) {
if (_show) {
return Banner.success(
message: const Text('변경 사항이 성공적으로 저장되었습니다.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
class BannerSuccessExample extends StatefulWidget {
const BannerSuccessExample({super.key});
@override
State<BannerSuccessExample> createState() => _BannerSuccessExampleState();
}
class _BannerSuccessExampleState extends State<BannerSuccessExample> {
bool _show = true;
@override
Widget build(BuildContext context) {
if (_show) {
return Banner.success(
message: const Text('변경 사항이 성공적으로 저장되었습니다.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
class BannerSolidExample extends StatefulComponent {
const BannerSolidExample({super.key});
@override
State<BannerSolidExample> createState() => _BannerSolidExampleState();
}
class _BannerSolidExampleState extends State<BannerSolidExample> {
bool _show = true;
@override
Component build(BuildContext context) {
if (_show) {
return Banner(
variant: CoreBannerVariant.destructive,
emphasis: CoreBannerEmphasis.solid,
message: const Text('Payments are currently unavailable.'),
description: const Text('We are working on it. No action needed.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = true),
child: const Text('Show banner'),
);
}
}
class BannerSolidExample extends StatefulWidget {
const BannerSolidExample({super.key});
@override
State<BannerSolidExample> createState() => _BannerSolidExampleState();
}
class _BannerSolidExampleState extends State<BannerSolidExample> {
bool _show = true;
@override
Widget build(BuildContext context) {
if (_show) {
return Banner(
variant: CoreBannerVariant.destructive,
emphasis: CoreBannerEmphasis.solid,
message: const Text('Payments are currently unavailable.'),
description: const Text('We are working on it. No action needed.'),
onDismiss: () => setState(() => _show = false),
);
}
return Button(
variant: CoreButtonVariant.outline,
onPressed: () => setState(() => _show = 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',
);
}
}
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'),
),
],
);
}
}
위 갤러리의
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 전환은 전부 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',
);
}
}
두 축 — 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위젯에 정의된 콜백 실행 - 호버: 닫기 버튼 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를 사용합니다. 플랫폼별로 타입이 다른 부분만 차이가 있습니다.
| 항목 | Flutter | Web |
|---|---|---|
message / description / action 타입 |
Widget |
Component |
onDismiss 콜백 타입 |
VoidCallback? |
CoreVoidCallback? |
| 배치 방식 | Column 최상단 또는 Scaffold 내 삽입 |
페이지 상단 고정 (position: sticky) |
| 애니메이션 | AnimatedSwitcher 등 Flutter 애니메이션 | CSS transition |