Badge | CoUI
LogoCoUI

Badge

배지 컴포넌트

Badge#

상태, 카운트, 라벨을 표시하는 배지 컴포넌트입니다.

Live Preview#

사용 시기 (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.
borderColorCoreColor?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%)
borderColoroutline

사용 예 (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'))

폼 — 알약과 점#

formvariant 와 직교합니다. 점도 여전히 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_coreCoreBadgeVariantStyle을 사용하여 크로스 플랫폼 일관된 스타일링을 제공합니다.

토큰설명
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 입니다.

항목FlutterWeb
슬롯 타입 Widget / Widget? Component / Component?
인터랙션 통합 Clickable click 이벤트 + role="button"
chrome emit paint-ready BoxDecoration / TextStyle Tailwind class + inline CSS
스케일링 theme.scaling 을 resolver 에서 곱함 rem 이 브라우저 텍스트 배율에 반응
  • Chip: 인터랙티브 태그. Badge보다 크고 삭제/클릭 가능
  • Avatar: 사용자 프로필 이미지. Badge와 조합하여 상태 표시
  • Icon: 아이콘. Badge의 leading/trailing으로 사용