Badge#
상태, 카운트, 라벨을 표시하는 배지 컴포넌트입니다.
Live Preview#
class BadgePrimaryExample extends StatefulComponent {
const BadgePrimaryExample({super.key});
@override
State<BadgePrimaryExample> createState() => _BadgePrimaryExampleState();
}
class _BadgePrimaryExampleState extends State<BadgePrimaryExample> {
@override
Component build(BuildContext context) {
return Badge(
variant: CoreBadgeVariant.primary,
child: Text('Primary'),
);
}
}
class BadgePrimaryExample extends StatefulWidget {
const BadgePrimaryExample({super.key});
@override
State<BadgePrimaryExample> createState() => _BadgePrimaryExampleState();
}
class _BadgePrimaryExampleState extends State<BadgePrimaryExample> {
@override
Widget build(BuildContext context) {
return Badge(
variant: CoreBadgeVariant.primary,
child: const Text('Primary'),
);
}
}
class BadgeSecondaryExample extends StatefulComponent {
const BadgeSecondaryExample({super.key});
@override
State<BadgeSecondaryExample> createState() => _BadgeSecondaryExampleState();
}
class _BadgeSecondaryExampleState extends State<BadgeSecondaryExample> {
@override
Component build(BuildContext context) {
return Badge(
variant: CoreBadgeVariant.secondary,
child: Text('Secondary'),
);
}
}
class BadgeSecondaryExample extends StatefulWidget {
const BadgeSecondaryExample({super.key});
@override
State<BadgeSecondaryExample> createState() => _BadgeSecondaryExampleState();
}
class _BadgeSecondaryExampleState extends State<BadgeSecondaryExample> {
@override
Widget build(BuildContext context) {
return Badge(
variant: CoreBadgeVariant.secondary,
child: const Text('Secondary'),
);
}
}
class BadgeDestructiveExample extends StatefulComponent {
const BadgeDestructiveExample({super.key});
@override
State<BadgeDestructiveExample> createState() =>
_BadgeDestructiveExampleState();
}
class _BadgeDestructiveExampleState extends State<BadgeDestructiveExample> {
@override
Component build(BuildContext context) {
return Badge(
variant: CoreBadgeVariant.destructive,
child: Text('Destructive'),
);
}
}
class BadgeDestructiveExample extends StatefulWidget {
const BadgeDestructiveExample({super.key});
@override
State<BadgeDestructiveExample> createState() =>
_BadgeDestructiveExampleState();
}
class _BadgeDestructiveExampleState extends State<BadgeDestructiveExample> {
@override
Widget build(BuildContext context) {
return Badge(
variant: CoreBadgeVariant.destructive,
child: const Text('Destructive'),
);
}
}
class BadgeOutlineExample extends StatefulComponent {
const BadgeOutlineExample({super.key});
@override
State<BadgeOutlineExample> createState() => _BadgeOutlineExampleState();
}
class _BadgeOutlineExampleState extends State<BadgeOutlineExample> {
@override
Component build(BuildContext context) {
return Badge(
variant: CoreBadgeVariant.outline,
child: Text('Outline'),
);
}
}
class BadgeOutlineExample extends StatefulWidget {
const BadgeOutlineExample({super.key});
@override
State<BadgeOutlineExample> createState() => _BadgeOutlineExampleState();
}
class _BadgeOutlineExampleState extends State<BadgeOutlineExample> {
@override
Widget build(BuildContext context) {
return Badge(
variant: CoreBadgeVariant.outline,
child: const Text('Outline'),
);
}
}
class BadgeDotExample extends StatefulComponent {
const BadgeDotExample({super.key});
@override
State<BadgeDotExample> createState() => _BadgeDotExampleState();
}
class _BadgeDotExampleState extends State<BadgeDotExample> {
@override
Component build(BuildContext context) {
// The dot wears the same variant colours the pill does — the form
// says whether there is content, not which colour it is.
return div([
Badge(form: CoreBadgeForm.dot),
Badge(form: CoreBadgeForm.dot, variant: CoreBadgeVariant.secondary),
Badge(form: CoreBadgeForm.dot, variant: CoreBadgeVariant.outline),
Badge(form: CoreBadgeForm.dot, variant: CoreBadgeVariant.destructive),
Badge(form: CoreBadgeForm.dot, size: CoreComponentSize.xl),
], classes: 'flex items-center gap-${CoreSpace.scale.space12}');
}
}
class BadgeDotExample extends StatefulWidget {
const BadgeDotExample({super.key});
@override
State<BadgeDotExample> createState() => _BadgeDotExampleState();
}
class _BadgeDotExampleState extends State<BadgeDotExample> {
@override
Widget build(BuildContext context) {
// The dot wears the same variant colours the pill does — the form
// says whether there is content, not which colour it is.
return Row(
mainAxisSize: MainAxisSize.min,
spacing: CoreSpace.space12,
children: const [
Badge(form: CoreBadgeForm.dot),
Badge(form: CoreBadgeForm.dot, variant: CoreBadgeVariant.secondary),
Badge(form: CoreBadgeForm.dot, variant: CoreBadgeVariant.outline),
Badge(form: CoreBadgeForm.dot, variant: CoreBadgeVariant.destructive),
Badge(form: CoreBadgeForm.dot, size: CoreComponentSize.xl),
],
);
}
}
class BadgeChainExample extends StatelessComponent {
const BadgeChainExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
Badge(
variant: CoreBadgeVariant.primary,
child: Text('Primary'),
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
Badge(
variant: CoreBadgeVariant.primary,
child: Text('Full control'),
).withStyle(
const CoreBadgeStyle(
backgroundColor: CoreColor.token(CoreColors.tertiaryContainer),
foregroundColor: CoreColor.token(CoreColors.onTertiaryContainer),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space12,
vertical: CoreSpace.space4,
),
),
),
],
classes: 'flex flex-col items-start',
);
}
}
class BadgeChainExample extends StatelessWidget {
const BadgeChainExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
Badge(
variant: CoreBadgeVariant.primary,
child: const Text('Primary'),
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
Badge(
variant: CoreBadgeVariant.primary,
child: const Text('Full control'),
).withStyle(
const CoreBadgeStyle(
backgroundColor: CoreColor.token(CoreColors.tertiaryContainer),
foregroundColor: CoreColor.token(CoreColors.onTertiaryContainer),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space12,
vertical: CoreSpace.space4,
),
),
),
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 항목의 상태를 시각적으로 표시할 때 (신규, 완료, 오류 등)
- 알림 카운트를 아이콘에 부착할 때
- 카테고리나 태그를 간결하게 라벨링할 때
대신 다른 컴포넌트를 사용하세요:
Chip: 삭제 가능하거나 클릭 가능한 태그일 때Alert: 더 많은 내용을 포함하는 인라인 알림일 때Toast: 일시적 피드백 메시지일 때
기본 사용법 (Basic Usage)#
// 기본 배지
Badge(
variant: CoreBadgeVariant.primary,
child: Text('Primary'),
)
// Secondary 변형
Badge(
variant: CoreBadgeVariant.secondary,
child: Text('Secondary'),
)
// Outline 변형
Badge(
variant: CoreBadgeVariant.outline,
child: Text('Outline'),
)
// Destructive 변형
Badge(
variant: CoreBadgeVariant.destructive,
child: Text('Destructive'),
)
// 기본 배지
Badge(
variant: CoreBadgeVariant.primary,
child: Text('Primary'),
)
// Secondary 변형
Badge(
variant: CoreBadgeVariant.secondary,
child: Text('Secondary'),
)
// Outline 변형
Badge(
variant: CoreBadgeVariant.outline,
child: Text('Outline'),
)
// Destructive 변형
Badge(
variant: CoreBadgeVariant.destructive,
child: Text('Destructive'),
)
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
child |
Widget (Flutter) / Component (Web) |
필수 | 배지 콘텐츠 |
variant |
CoreBadgeVariant |
primary |
배지 변형 (primary, secondary, outline, destructive) |
size |
CoreComponentSize |
sm |
배지 크기 (xs, sm, md, lg, xl) |
leading |
Widget? (Flutter) / Component? (Web) |
null |
앞쪽 아이콘 |
trailing |
Widget? (Flutter) / Component? (Web) |
null |
뒤쪽 아이콘 |
onPressed |
VoidCallback? (Flutter) / CoreVoidCallback? (Web) |
null |
클릭 콜백 (설정 시 인터랙티브) |
badgeStyle |
CoreBadgeStyle? |
null |
인스턴스 스타일 (Style 시스템 참조) |
스타일 시스템 (Style System)#
Badge 의 모든 chrome / dimensional / nested-slot 오버라이드는 CoreBadgeStyle 단일 슬롯으로 흐릅니다. 시맨틱 enum (variant,
size) 과 behaviour (child / leading / trailing / onPressed) 는 위젯 파라미터로 직접 전달합니다.
시맨틱 vs 스타일#
-
시맨틱 enum / behaviour: 위젯/컴포넌트 파라미터로 직접 (
variant,size,child,leading,trailing,onPressed) -
chrome / dimensional / 슬롯 스타일:
CoreBadgeStyle한 곳으로 (backgroundColor/foregroundColor/borderColor/borderWidth/borderRadius/padding/iconLabelGapStyle/transitionDuration/labelStyle/iconStyle/focusOutlineStyle)
Resolve chain#
design system default for badge
→ CoreBadgeTheme.style // 프로젝트 공통
→ parent component slot override
→ widget.badgeStyle // 인스턴스별
각 nested 슬롯 스타일 (labelStyle / iconStyle / iconLabelGapStyle /
focusOutlineStyle) 은 자기 컴포넌트의 자체 resolve chain 으로 다시 한 번 머지됩니다.
CoreBadgeStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
backgroundColor |
CoreColor? |
Panel background fill colour override. |
foregroundColor |
CoreColor? |
Foreground (label / icon) colour override. |
borderColor | CoreColor? | Border stroke colour. |
borderWidth |
double? |
Border stroke width (logical px). null defers to [defaultBorderWidth]. |
borderRadius |
CoreBorderRadius? |
Border radius. null defers to [defaultBorderRadius] (pill). |
padding |
CoreEdgeInsets? |
Padding applied to the content row.
null
defers to
CoreBadgeStyle.defaultsBySize[size]!.padding!
.
|
iconLabelGapStyle |
CoreGapStyle? |
Nested [CoreGapStyle] slot for the leading / label / trailing spacers. Forwarded straight to the
Gap(gapStyle: …)
widget by the Flutter resolver; consumed by the Web resolver to emit the
gap-${tailwindSpace}
Tailwind utility.
null
defers to
CoreBadgeStyle.defaultsBySize[size]!.iconLabelGapStyle!
.
|
transitionDuration |
Duration? |
Chrome transition duration override. null defers to [defaultTransitionDuration]. |
labelStyle |
CoreTextStyle? |
Label text style override.
null
defers to
CoreBadgeStyle.defaultsBySize[size]!.labelStyle!
(size-aware typography token).
|
iconStyle |
CoreIconStyle? |
Leading / trailing icon style override (applied uniformly to both slots).
size
defers to
CoreBadgeStyle.defaultsBySize[size]!.iconStyle!.size!
.
|
dotSize |
double? |
Diameter of the dot in [CoreBadgeForm.dot] (logical px).
null
defers to
defaultsBySize[size]!.dotSize!
. Unused by [CoreBadgeForm.count], whose box is its content plus [padding] — the two forms size themselves by different means, which is why this is its own field rather than a reinterpretation of the size step.
|
focusOutlineStyle |
CoreFocusOutlineStyle? |
Focus-ring style override. Nested [CoreFocusOutlineStyle] slot —
null
defers to [defaultFocusOutlineStyle]. Only consumed when the badge is interactive (
onPressed != null
).
|
CoreBadgeStyle 변형별 기본값 (CoreBadgeVariantStyle)#
| 필드 | primary |
secondary |
outline |
destructive |
|---|---|---|---|---|
backgroundColor |
primary | secondary | surface (opacity 0%) | error |
foregroundColor |
onPrimary | onSecondary | onSurface | onError |
hoverBackgroundColor |
primary (opacity 80%) | secondary (opacity 80%) | — | error (opacity 80%) |
borderColor | — | — | outline | — |
사용 예 (Flutter)#
Badge(
variant: CoreBadgeVariant.primary,
badgeStyle: CoreBadgeStyle(
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space16,
vertical: CoreSpace.space4,
),
borderRadius: CoreBorderRadius.all(CoreRadius.radius8),
labelStyle: CoreTextStyle.token(CoreTextStyles.labelLarge),
iconStyle: CoreIconStyle(size: CoreIconSize.size16),
),
leading: Icon(LucideIcons.star),
child: Text('Featured'),
)
사용 예 (Web)#
Badge(
variant: CoreBadgeVariant.primary,
badgeStyle: CoreBadgeStyle(
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space16,
vertical: CoreSpace.space4,
),
borderRadius: CoreBorderRadius.all(CoreRadius.radius8),
),
leading: Icon(LucideIcons.star),
child: Text('Featured'),
)
빠른 오버라이드 (Chain)#
이미 만든 Badge 인스턴스에 badgeStyle을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 위의
badgeStyle: CoreBadgeStyle(...) 생성자 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다. .radius4처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 —
withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius4 == CoreRadius.radius4) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.
class BadgeChainExample extends StatelessWidget {
const BadgeChainExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
Badge(
variant: CoreBadgeVariant.primary,
child: const Text('Primary'),
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
Badge(
variant: CoreBadgeVariant.primary,
child: const Text('Full control'),
).withStyle(
const CoreBadgeStyle(
backgroundColor: CoreColor.token(CoreColors.tertiaryContainer),
foregroundColor: CoreColor.token(CoreColors.onTertiaryContainer),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space12,
vertical: CoreSpace.space4,
),
),
),
],
);
}
}
class BadgeChainExample extends StatelessComponent {
const BadgeChainExample({super.key});
@override
Component build(BuildContext context) {
return div(
[
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
Badge(
variant: CoreBadgeVariant.primary,
child: Text('Primary'),
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
Badge(
variant: CoreBadgeVariant.primary,
child: Text('Full control'),
).withStyle(
const CoreBadgeStyle(
backgroundColor: CoreColor.token(CoreColors.tertiaryContainer),
foregroundColor: CoreColor.token(CoreColors.onTertiaryContainer),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space12,
vertical: CoreSpace.space4,
),
),
),
],
classes: 'flex flex-col items-start',
);
}
}
변형 (Variants)#
스타일#
Badge(variant: CoreBadgeVariant.primary, child: Text('Primary'))
Badge(variant: CoreBadgeVariant.secondary, child: Text('Secondary'))
Badge(variant: CoreBadgeVariant.outline, child: Text('Outline'))
Badge(variant: CoreBadgeVariant.destructive, child: Text('Destructive'))
폼 — 알약과 점#
form 은 variant 와 직교합니다. 점도 여전히 primary 이거나 destructive 이고,
그래서 두 축은 하나로 접히지 않습니다 — 접으면 "빨간 점" 을 표현할 수 없습니다.
Badge(child: Text('12')) // count — 기본값
Badge(form: CoreBadgeForm.dot) // dot
Badge(form: CoreBadgeForm.dot, variant: CoreBadgeVariant.destructive)
Badge(form: CoreBadgeForm.dot, size: CoreComponentSize.xl)
두 폼은 서로 다른 방식으로 크기를 갖습니다. 알약은 콘텐츠와 padding 이
정하고, 점은 dotSize 가 정합니다(size 단계별 4·6·8·12·16). 그래서 지름이
size 축의 재해석이 아니라 자기 필드입니다.
점은 child · leading · trailing 을 그리지 않습니다. 콘텐츠 행이 없는
것이지 비어 있는 것이 아니며, 빈 행이라면 padding 을 참조해 박스가 단계가
아니라 padding 크기가 됩니다 — 그것이 이 폼이 존재하는 이유 그 자체입니다.
Leading / Trailing 아이콘#
Badge(
variant: CoreBadgeVariant.primary,
leading: Icon(LucideIcons.star),
child: Text('Featured'),
)
Badge(
variant: CoreBadgeVariant.secondary,
trailing: Icon(LucideIcons.arrowRight),
child: Text('Next'),
)
동작 스펙 (Behavior)#
표시#
- 배지는 정적 인디케이터로, 기본적으로 인터랙션 없음
onPressed가 있으면 클릭 가능한 배지 (버튼 동작)- Flutter: 통합
Clickable이 hover / focus / 커서 / 키보드 활성화를 담당 - Web:
role="button"+tabindex="0"속성 추가
토큰 기반 스타일링#
Badge는 coui_core의 CoreBadgeVariantStyle을 사용하여 크로스 플랫폼 일관된 스타일링을 제공합니다.
| 토큰 | 값 | 설명 |
|---|---|---|
CoreBadgeStyle.defaultBorderRadius |
CoreRadius.radius9999 (9999px) |
원형 모서리 |
CoreBadgeStyle.defaultBorderWidth |
CoreStrokeWidth.stroke1 (1px) |
Outline 보더 |
CoreBadgeStyle.defaultsBySize[size]!.padding |
size별 CoreEdgeInsets.symmetric(horizontal/vertical) |
size별 패딩 |
CoreBadgeStyle.defaultsBySize[size]!.iconLabelGapStyle |
size별 CoreGapStyle(size: CoreSpace.spaceN) |
leading / label / trailing 간격 |
CoreBadgeStyle.defaultsBySize[size]!.iconStyle |
size별 CoreIconStyle(size: CoreIconSize.sizeN) |
size별 아이콘 크기 |
CoreBadgeStyle.defaultsBySize[size]!.labelStyle |
size별 CoreTextStyle.token(CoreTextStyles.X) |
size별 라벨 typography |
CoreBadgeStyle.defaultTransitionDuration |
CoreDuration.fast (150ms) |
인터랙티브 hover 트랜지션 |
CoreBadgeStyle.defaultFocusOutlineStyle |
CoreFocusOutlineStyle |
인터랙티브 배지 포커스 링 |
사용 가이드라인 (Usage Guidelines)#
Do#
상태에 맞는 변형을 사용하세요.
Badge(variant: CoreBadgeVariant.primary, child: Text('활성'))
Badge(variant: CoreBadgeVariant.destructive, child: Text('오류'))
Badge(variant: CoreBadgeVariant.secondary, child: Text('대기'))
색상으로 상태를 즉시 구분할 수 있습니다.
Don't#
모든 배지에 같은 변형을 사용하지 마세요.
Badge(variant: CoreBadgeVariant.secondary, child: Text('활성'))
Badge(variant: CoreBadgeVariant.secondary, child: Text('오류'))
Badge(variant: CoreBadgeVariant.secondary, child: Text('대기'))
상태 구분이 불가능해집니다.
Do#
배지 텍스트는 짧게 유지하세요.
Badge(variant: CoreBadgeVariant.primary, child: Text('신규'))
Badge(variant: CoreBadgeVariant.secondary, child: Text('Beta'))
한두 단어로 간결하게 표현합니다.
Don't#
배지에 긴 텍스트를 넣지 마세요.
Badge(variant: CoreBadgeVariant.primary, child: Text('새로 추가된 기능입니다'))
배지는 간결한 라벨에 적합합니다.
접근성 (Accessibility)#
스크린 리더#
-
Flutter: 배지 텍스트가 그대로 읽힙니다.
onPressed설정 시 통합Clickable이 버튼 시맨틱과 포커스를 담당 -
Web: 배지 텍스트가 인라인으로 읽힘.
onPressed설정 시role="button"+tabindex="0"자동 부여
색상 대비#
- 모든 변형은 WCAG AA 기준의 텍스트 대비 충족
- 색상만으로 상태를 구분하지 않고 텍스트 라벨도 함께 제공
크로스 플랫폼 차이점 (Platform Differences)#
variant / size / child / leading / trailing
/ onPressed / badgeStyle 은 양 플랫폼 동일 API 입니다.
| 항목 | Flutter | Web |
|---|---|---|
| 슬롯 타입 | Widget / Widget? |
Component / Component? |
| 인터랙션 | 통합 Clickable |
click 이벤트 + role="button" |
| chrome emit | paint-ready BoxDecoration / TextStyle |
Tailwind class + inline CSS |
| 스케일링 | theme.scaling 을 resolver 에서 곱함 |
rem 이 브라우저 텍스트 배율에 반응 |