CoUI Chip#
Quick Reference#
- Widget:
Chip -
Variant:
CoreChipColor— custom, neutral, primary, secondary, tertiary (defaultneutral) - Size:
CoreComponentSize— xs/sm/md/lg/xl (defaultmd) - 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:
CoreChipColor—custom,neutral,primary,secondary,tertiary(defaultCoreChipColor.defaultColor=neutral). -
Size:
CoreComponentSize—xs,sm,md,lg,xl(widget defaultmdon 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#
| Parameter | Type | Default | Description |
|---|---|---|---|
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:
| Field | Type | Description |
|---|---|---|
backgroundColor |
CoreColor? |
Panel background fill override (wins across every state) |
foregroundColor |
CoreColor? |
Foreground (label) colour override |
borderColor | CoreColor? | Border stroke colour |
borderWidth |
double? |
Border stroke width (logical px; default CoreStrokeWidth.stroke1) |
borderRadius |
CoreBorderRadius? |
Corner radius (default: pill radius9999) |
padding | CoreEdgeInsets? | 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)#
| Parameter | Type (Flutter) | Type (Web) | Default | Notes |
|---|---|---|---|---|
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.
| Aspect | Flutter | Web |
|---|---|---|
| Widget class | Chip (StatelessWidget, CoreChipContract<Widget>) |
Chip (UiComponent, CoreChipContract<Component>) |
child / leading / trailing type |
Widget? |
Component? |
| Callback type | VoidCallback? | 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 params | — | id; 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
Chipclashes with Material's own chip widget — if a file also importspackage:flutter/material.dartdirectly, you musthide Chipon that import to avoid the name collision. -
The close (X) button only renders when
onRemoveis non-null andenabledis true — settingonRemovealone is not enough on a disabled chip. -
chipColor: customhas no entry in the shared colour token table — its unselected/selected/disabled colours derive fromseedColorat the use-site instead (selected label falls back todefaultCustomSelectedForegroundColor, white). -
Semantic enums (
chipColor/size/selected) are not part ofCoreChipStyle— they must stay as widget params, andCoreChipThemecannot override them either. -
trailingIconStyleinCoreChipStyledoes not affect the close button — the close button's chrome is controlled separately viacloseButtonStyle.
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, standaloneChipStyle/ChipTheme/ChipCloseButtonclasses,CoreChipSelectedStyle/CoreChipUnselectedStyleenums) was removed entirely — v0.127 chip source carries no deprecated members. UseChip+chipColor/size/chipStyle(CoreChipStyle) /CoreChipThemeonly. -
Import stays the single barrel (
package:coui_flutter/coui_flutter.dart/package:coui_web/coui_web.dart); with the bareChipname, hide Material's chip when importingmaterial.dartdirectly (import 'package:flutter/material.dart' hide Chip;).