LogoSkills

coui-chip

칩, 태그, 필터 옵션, 선택 가능한 라벨, 제거/닫기 가능한 태그 요소를 CoUI Flutter(coui_flutter) 또는 CoUI Web(coui_web)에서 Chip 위젯과 CoreChipColor(custom, neutral, primary, secondary, tertiary), CoreComponentSize(xs-xl), CoreChi...

CoUI Chip#

Quick Reference#

  • Widget: Chip
  • Variant: CoreChipColor — custom, neutral, primary, secondary, tertiary (default neutral)
  • Size: CoreComponentSize — xs/sm/md/lg/xl (default md)
  • Style slot: chipStyle: CoreChipStyle(...)
  • Canonical snippet:
Chip(label: 'Flutter')

한마디로#

칩(Chip)은 이메일 받는 사람 칸에 뜨는 작고 동그란 이름표나, 쇼핑몰에서 "색상", "사이즈"처럼 눌러서 고르는 작은 버튼표를 떠올리면 됩니다. 이 스킬은 그런 작은 라벨/태그/필터 단추를 화면에 만들 때 일관된 모양과 동작으로 자동으로 만들어 주는 안내서입니다. 글자만 보여주는 정적인 태그부터, 눌러서 선택하는 필터, X 버튼으로 지울 수 있는 태그까지 같은 부품 하나로 처리합니다.

무엇을·언제#

  • 무엇을 해주나요: 화면에 들어갈 작은 라벨/태그/필터/선택 단추를 디자인 규칙에 맞게 만들어 줍니다. 색상, 크기, 선택 여부, 삭제(X) 버튼 같은 옵션을 손쉽게 붙일 수 있습니다.
  • 언제 작동하나요:
    • 태그 목록이나 분류 라벨을 보여줄 때 (예: "Flutter", "디자인" 같은 표시)
    • 눌러서 켜고 끄는 필터를 만들 때 (예: 검색 필터 막대)
    • 받는 사람, 연락처, 이메일처럼 X 버튼으로 지울 수 있는 항목을 보여줄 때
    • 검색 조건이나 입력한 단어를 지울 수 있는 칩으로 표시할 때
  • 어디서 쓰나요: 모바일/데스크톱 앱(Flutter)과 웹(Web) 양쪽에서 거의 같은 방식으로 동작합니다.

핵심 용어#

용어쉬운 설명
Chip(칩)화면에 뜨는 작은 라벨/태그/단추. 이름표나 작은 버튼표라고 생각하면 됩니다.
chipColor (색상 변형)칩의 색 테마(기본 회색/주요/보조/제3/직접 지정)를 고르는 설정.
size (크기)칩의 크기(아주 작음 xs ~ 아주 큼 xl)를 고르는 설정.
selected (선택됨)칩이 눌려서 켜진 상태인지 표시하는 값.
onTap (탭/누르기)칩을 눌렀을 때 일어날 동작.
onRemove (삭제)X 버튼을 눌러 칩을 지울 때 일어날 동작. 이 값이 있으면 X 버튼이 보입니다.
leading / trailing (앞/뒤 슬롯)글자 앞이나 뒤에 아이콘 같은 걸 넣는 자리.
seedColor (씨앗 색)직접 지정(custom) 색상 칩의 바탕이 되는 색.
chipStyle (스타일)모서리 둥글기, 여백 등 세부 외형을 따로 바꾸는 설정.
Legacy / Deprecated옛날 방식이라 새 코드에서는 쓰지 말아야 하는, 곧 사라질 항목.

Overview#

Chip is the unified cross-platform chip — a compact element for tags, labels, filters, and selectable items. The same widget covers static tags, selectable filter chips, and removable/dismissible chips. Visual variation is driven by the chipColor (CoreChipColor) and size (CoreComponentSize) enums; all chrome / dimensional / slot overrides flow through the single chipStyle slot (CoreChipStyle).

  • Color: CoreChipColorcustom, neutral, primary, secondary, tertiary (default CoreChipColor.defaultColor = neutral).
  • Size: CoreComponentSizexs, sm, md, lg, xl (widget default md on both platforms).
  • Style slot: chipStyle: CoreChipStyle(...) for per-instance chrome micro-tweaks. Semantic enums (chipColor / size / selected) are not part of the style — pass them as widget params; the theme cannot override them either.
  • Both platforms implement the shared CoreChipContract (<Widget> on Flutter, <Component> on Web), so parameter names and defaults are identical.

For multi-chip selection groups (single/multi select with min/max constraints), a separate ChipGroup<T> component exists — this skill covers the single Chip.

Flutter (coui_flutter)#

Import#

import 'package:coui_flutter/coui_flutter.dart';

The barrel is the only supported import — never import src/ paths. Note: if a file also imports package:flutter/material.dart directly, hide Material's own chip widget to avoid the name clash:

import 'package:flutter/material.dart' hide Chip;

Basic Usage#

Chip(label: 'Flutter')

Parameters#

ParameterTypeDefaultDescription
label String? null Text label displayed on the chip
child Widget? null Custom child widget (alternative to label; wins when both set)
chipColor CoreChipColor CoreChipColor.defaultColor (= neutral) Color variant
size CoreComponentSize CoreComponentSize.md Chip size
selected bool false Whether the chip is selected
enabled bool true Whether the chip is interactive
onTap VoidCallback? null Tap callback (only fires when enabled)
onRemove VoidCallback? null Remove callback — shows the close (X) button when non-null and enabled
leading Widget? null Widget displayed before the label
trailing Widget? null Widget displayed after the label
seedColor CoreColor? null Seed color (used when chipColor is custom)
maxLength int? null Max character length for the label (truncates with ...)
chipStyle CoreChipStyle? null Per-instance chrome / dimensional / slot style

Color Variants — CoreChipColor#

Chip(label: 'Default')                                  // neutral (default)
Chip(label: 'Primary', chipColor: CoreChipColor.primary)
Chip(label: 'Secondary', chipColor: CoreChipColor.secondary)
Chip(label: 'Tertiary', chipColor: CoreChipColor.tertiary)
Chip(
  label: 'Custom',
  chipColor: CoreChipColor.custom,
  seedColor: CoreColor.token(CoreColors.primary),
)

CoreChipColor values (full set): custom, neutral, primary, secondary, tertiary. The widget default is CoreChipColor.defaultColor = neutral. custom derives its colours from the supplied seedColor (raw values like CoreColor(0xFF6750A4) also work).

Enum-shorthand (inferred type) is idiomatic in real code:

Chip(label: '디자인', chipColor: .primary)

Sizes — CoreComponentSize#

Chip(label: 'XS', size: CoreComponentSize.xs)
Chip(label: 'SM', size: CoreComponentSize.sm)
Chip(label: 'MD', size: CoreComponentSize.md)   // default
Chip(label: 'LG', size: CoreComponentSize.lg)
Chip(label: 'XL', size: CoreComponentSize.xl)

CoreComponentSize values: xs, sm, md, lg, xl.

Selectable Chip#

Chip(
  label: '디자인',
  chipColor: CoreChipColor.primary,
  selected: isSelected,
  onTap: () => toggleSelection(),
)

Selection state is driven by selected; toggling happens in onTap. The resolver picks distinct selected / unselected / disabled colours per chipColor; the selected border mirrors the selected background. State colour changes animate via AnimatedContainer with transitionDuration (default 100ms).

Removable Chip#

onRemove renders an internal close (X) button — but only while enabled is true. The X is built from the unified Button(variant: .ghost, size: .sm) wrapping Icon(LucideIcons.x), so hover / focus / disabled states are handled by the design system.

Chip(
  label: 'user@example.com',
  onRemove: () => removeItem(),
)

Leading / Trailing Slots#

Chip(
  label: 'Verified',
  leading: Icon(LucideIcons.check),
  chipColor: CoreChipColor.primary,
)

leading / trailing are arbitrary widgets. Their icon size and colour are driven by the resolved per-size / per-state chip values (an IconTheme is applied around each slot), so you do not set them manually. The slot↔label spacers are Gap widgets driven by the size-aware iconLabelGapStyle slot.

Label Truncation#

Chip(label: 'A very long tag name', maxLength: 10)   // renders 'A very lon...'

The label is also rendered with maxLines: 1 + ellipsis overflow.

Disabled Chip#

Chip(label: 'Unavailable', enabled: false)

Disabled chips ignore onTap, hide the close button, show the forbidden cursor, and swap to the disabled colour set.

Per-instance Style — chipStyle: CoreChipStyle(...)#

Every chrome / dimensional / slot tweak flows through the single chipStyle slot. Semantic enums (chipColor / size / selected) stay as widget parameters — do not pack them into the style.

Chip(
  label: 'Custom radius',
  chipStyle: const CoreChipStyle(
    borderRadius: CoreBorderRadius.all(CoreRadius.radius8),
    borderWidth: CoreStrokeWidth.stroke1,
    padding: CoreEdgeInsets.symmetric(
      horizontal: CoreSpace.space12,
      vertical: CoreSpace.space4,
    ),
    labelStyle: CoreTextStyle(fontSize: CoreFontSize.s14),
    leadingIconStyle: CoreIconStyle(size: CoreSize.size16),
    iconLabelGapStyle: CoreGapStyle(size: CoreSpace.space4),
  ),
)

CoreChipStyle fields:

FieldTypeDescription
backgroundColor CoreColor? Panel background fill override (wins across every state)
foregroundColor CoreColor? Foreground (label) colour override
borderColorCoreColor?Border stroke colour
borderWidth double? Border stroke width (logical px; default CoreStrokeWidth.stroke1)
borderRadius CoreBorderRadius? Corner radius (default: pill radius9999)
paddingCoreEdgeInsets?Padding
iconLabelGapStyle CoreGapStyle? Gap between leading / label / trailing / close slots
closeIconStyle CoreIconStyle? Close (X) icon style
transitionDuration Duration? Hover / focus / state colour transition (default 100ms)
labelStyle CoreTextStyle? Label text style override (role default: bodySmall token)
leadingIconStyle CoreIconStyle? Leading icon style override
trailingIconStyle CoreIconStyle? Trailing icon style override (does not affect the close button)
closeButtonStyle CoreButtonStyle? Close-button chrome (padding / iconStyle) forwarded to the internal ghost Button

It also exposes merge, copyWith, and the shared design-system defaults as statics: defaultBorderRadius (pill, radius9999), defaultBorderWidth (stroke1), defaultLabelStyle (bodySmall token), defaultTransitionDuration (100ms), defaultDisabledOpacity, defaultHoverOpacity, defaultCustomDisabledBackgroundColor / defaultCustomDisabledForegroundColor / defaultCustomDisabledBorderColor, defaultCustomSelectedForegroundColor (white), defaultsBySize, and defaultsByVariant (a Map<CoreChipColor, CoreChipColorStyle>).

Chip Group Pattern#

Wrap(
  spacing: 8,
  runSpacing: 8,
  children: [
    for (final tag in tags)
      Chip(
        label: tag,
        onRemove: () => removeTag(tag),
      ),
  ],
)

For managed single/multi selection over a list of items, prefer the dedicated ChipGroup<T> component (items + chipBuilder + onChanged).

Web (coui_web / Jaspr)#

Import#

import 'package:coui_web/coui_web.dart';

The barrel re-exports Jaspr (Component, Styles, dom helpers like div / span / text) and coui_core, so no separate Jaspr import is needed.

Basic Usage#

The Web Chip has the same params and defaults; child / leading / trailing are Jaspr Component, callbacks are CoreVoidCallback?. It renders as a <div> pill with Tailwind classes plus inline styles synthesized by the resolver.

Chip(label: 'Flutter')

Removable Chip#

Chip(
  label: 'user@example.com',
  onRemove: () => removeItem(),
)

Same rule as Flutter: the close button renders only when onRemove != null && enabled, via the unified Button(variant: .ghost, size: .sm) + Icon(LucideIcons.x).

Selectable Chip#

Chip(
  label: '디자인',
  chipColor: CoreChipColor.primary,
  selected: isSelected,
  onTap: () => toggleSelection(),
)

onTap registers a DOM click event (only when enabled). Cursor classes follow state: cursor-not-allowed when disabled, cursor-pointer when interactive. State colours fade with an inline transition rule mirroring Flutter's AnimatedContainer.

Leading Slot (Component)#

Chip(
  label: 'Verified',
  leading: const Icon(LucideIcons.check),
  chipColor: CoreChipColor.primary,
)

Leading / trailing slots are wrapped in inline-flex <span>s; the slot↔label spacers render as Gap components (same iconLabelGapStyle slot as Flutter).

Custom Child#

When using child instead of label, supply a Component (child wins when both are set):

Chip(
  child: span([text('Custom')]),
  onRemove: () => removeItem(),
)

Disabled / Accessibility#

Chip(label: 'Unavailable', enabled: false)

A disabled Web chip gets aria-disabled="true", drops the click handler, and hides the close button.

Web-only extras#

The Web constructor additionally accepts id. The component also provides a copyWith(...) method covering every chip parameter.

Parameters (both platforms)#

ParameterType (Flutter)Type (Web)DefaultNotes
label String? String? null Plain-text label
child Widget? Component? null Custom content; wins over label
chipColor CoreChipColor CoreChipColor defaultColor (= neutral) custom / neutral / primary / secondary / tertiary
size CoreComponentSize CoreComponentSize md xs / sm / md / lg / xl
selected bool bool false Selected state
enabled bool bool true Gates onTap and the close button
onTap VoidCallback? CoreVoidCallback? null Tap / click
onRemove VoidCallback? CoreVoidCallback? null Shows close (X) button when non-null and enabled
leading Widget? Component? null Before the label
trailing Widget? Component? null After the label
seedColor CoreColor? CoreColor? null For chipColor: custom
maxLength int? int? null Label truncation with ...
chipStyle CoreChipStyle? CoreChipStyle? null Per-instance chrome/slot style

Web-only: id, plus copyWith(...).

Color / Size token tables#

Both platforms resolve from the same single source of truth in coui_core (CoreChipStyle.defaultsByVariant / CoreChipStyle.defaultsBySize).

Colors (CoreChipColor — full set; each entry is a CoreChipColorStyle):

chipColor Unselected (bg / fg / border) Selected (bg / fg / border) Disabled (bg / fg / border)
neutral transparent / onSurface / outline surfaceContainerHigh / onSurface / surfaceContainerHigh surfaceContainer / surfaceContainerHigh / surfaceContainer
primary transparent / primary / primary primary / onPrimary / primary surfaceContainer / surfaceContainerHigh / surfaceContainer
secondary transparent / secondary / secondary secondary / onSecondary / secondary surfaceContainer / surfaceContainerHigh / surfaceContainer
tertiary transparent / tertiary / tertiary tertiary / onTertiary / tertiary surfaceContainer / surfaceContainerHigh / surfaceContainer
custom transparent / seed / seed seed / white / seed surfaceContainer / surfaceContainerHigh / surfaceContainer

The selected border always mirrors the selected background. custom has no table entry — its colours derive from seedColor at use-site (white selected label via defaultCustomSelectedForegroundColor).

Sizes (CoreComponentSize — full set; token values, pre-scaling):

Size Padding (H × V) Label font size Leading/Trailing icon Close icon Icon↔label gap
xs space6 × 0 s10 size12 size12 space2
sm space8 × space2 s12 size14 size14 space4
md space12 × space4 s14 size16 size16 space4
lg space14 × space6 s16 size18 size18 space6
xl space16 × space8 s18 size20 size20 space8

Label typography role is bodySmall (CoreChipStyle.defaultLabelStyle) across every size — only the per-size font size varies. Shape default: pill (radius9999); border width default CoreStrokeWidth.stroke1.

Project-level theming — CoreChipTheme#

CoreChipTheme has exactly two slots — style and variantStyles (keyed by CoreChipColor). Semantic identifiers (chipColor / size) are widget-only; the theme cannot set them. Resolve chain:

design-system default (CoreChipStyle.defaultsBySize[size])CoreChipTheme.style                         // 프로젝트 공통CoreChipTheme.variantStyles[widget.chipColor]
  → parent component slot override
  → widget.chipStyle                            // 인스턴스별
const CoreChipTheme(
  style: CoreChipStyle(
    borderRadius: CoreBorderRadius.all(CoreRadius.radius8),
  ),
  variantStyles: {
    CoreChipColor.primary: CoreChipStyle(
      borderWidth: CoreStrokeWidth.stroke1,
    ),
  },
)

Register it on the app theme's coreComponentTheme.chip slot — both platform resolvers read theme.coreComponentTheme?.chip.

Flutter ↔ Web Differences#

These are the only behavioural differences confirmed in source — everything else (enums, parameter names, defaults, the chipStyle slot) is identical.

AspectFlutterWeb
Widget class Chip (StatelessWidget, CoreChipContract<Widget>) Chip (UiComponent, CoreChipContract<Component>)
child / leading / trailing type Widget? Component?
Callback typeVoidCallback?CoreVoidCallback?
Label render Text(label) with ellipsis, maxLines: 1 <span class="truncate"> inside the pill <div>
Tap wiring GestureDetector(onTap: ...) when enabled DOM click event when enabled
Disabled affordance MouseRegion forbidden cursor aria-disabled="true" + cursor-not-allowed class
State transition AnimatedContainer(duration: transitionDuration) inline CSS transition (background / color / border)
Extra ctor paramsid; also copyWith(...)

Both platforms render the close (X) button via the unified Button(variant: .ghost, size: .sm) wrapping Icon(LucideIcons.x), and both insert Gap(gapStyle: ...) spacers between adjacent slots.

When to Use#

  • Tag lists and category labels (static Chip(label: ...))
  • Filter selection bars (selected + onTap)
  • User / contact / email selection chips with removal (onRemove)
  • Removable search criteria / token input chips
  • Managed selection over a whole set → ChipGroup<T> instead

Pitfalls#

  • Bare Chip clashes with Material's own chip widget — if a file also imports package:flutter/material.dart directly, you must hide Chip on that import to avoid the name collision.
  • The close (X) button only renders when onRemove is non-null and enabled is true — setting onRemove alone is not enough on a disabled chip.
  • chipColor: custom has no entry in the shared colour token table — its unselected/selected/disabled colours derive from seedColor at the use-site instead (selected label falls back to defaultCustomSelectedForegroundColor, white).
  • Semantic enums (chipColor / size / selected) are not part of CoreChipStyle — they must stay as widget params, and CoreChipTheme cannot override them either.
  • trailingIconStyle in CoreChipStyle does not affect the close button — the close button's chrome is controlled separately via closeButtonStyle.

v0.127 Migration (Legacy)#

v0.127 is a de-prefix hard break: the widget class was renamed and no Co-prefixed alias remains. Core* contract names (CoreChipColor, CoreChipStyle, CoreChipColorStyle, CoreChipTheme, CoreChipContract, CoreComponentSize) are unchanged.

❌ Legacy (pre-0.127, removed)✅ v0.127
CoChip(label: …)Chip(label: …)
CoChip(chipColor: CoreChipColor.primary, selected: …) Chip(chipColor: CoreChipColor.primary, selected: …)
CoIcon(CoLucideIcons.check)Icon(LucideIcons.check)
CoLucideIcons.x (close icon internals)LucideIcons.x
CoButton(variant: ghost) (close-button internals) Button(variant: .ghost)
CoGap(…) (slot spacer internals)Gap(…)
CoChipGroup(…)ChipGroup(…)

Additional migration notes:

  • ❌ The old deprecated legacy surface (Chip.button(...) factory, standalone ChipStyle / ChipTheme / ChipCloseButton classes, CoreChipSelectedStyle / CoreChipUnselectedStyle enums) was removed entirely — v0.127 chip source carries no deprecated members. Use Chip + chipColor / size / chipStyle (CoreChipStyle) / CoreChipTheme only.
  • Import stays the single barrel (package:coui_flutter/coui_flutter.dart / package:coui_web/coui_web.dart); with the bare Chip name, hide Material's chip when importing material.dart directly (import 'package:flutter/material.dart' hide Chip;).