coui-chip
CoUI로 태그나 선택·제거 가능한 작은 라벨을 만들 때 사용합니다. Chip의 색·형태·강조와 닫기 동작을 구성합니다.
Quick Reference#
- Widget:
Chip -
Colour when selected:
CoreChipColor— custom, neutral, primary (defaultneutral) -
Unselected look:
CoreChipVariant— outlined, filled (defaultoutlined) - Emphasis:
CoreChipEmphasis— low, high (defaulthigh) -
Size:
CoreChipSize— sm / md / lg = 28 / 32 / 40 (defaultmd) -
Shape:
CoreChipShape— rounded, pill (defaultrounded) — new in coui_core 0.163.0 · coui_flutter 0.164.0 · coui_web 0.163.0 - Style slot:
chipStyle: CoreChipStyle(...) - Canonical snippet:
Chip(label: 'Flutter')
한마디로#
Chip의 색·형태·강조와 닫기 동작을 구성합니다.
무엇을·언제#
- 무엇을 해주나요: 화면에 들어갈 작은 라벨/태그/필터/선택 단추를 디자인 규칙에 맞게 만들어 줍니다. 색상, 꺼진 모양, 강조, 크기, 윤곽(둥근 사각형/알약), 선택 여부, 삭제(X) 버튼 같은 옵션을 손쉽게 붙일 수 있습니다.
-
언제 작동하나요:
- 태그 목록이나 분류 라벨을 보여줄 때 (예: "Flutter", "디자인" 같은 표시)
- 눌러서 켜고 끄는 필터를 만들 때 (예: 검색 필터 막대, 통화 · 정렬 칩 줄)
- "무엇을 거르는가"와 "지금 값"을 한 칩에 보여줄 때 (예: "상태 · 진행 중")
- 받는 사람, 연락처, 이메일처럼 X 버튼으로 지울 수 있는 항목을 보여줄 때
- 검색 조건이나 입력한 단어를 지울 수 있는 칩으로 표시할 때
- 어디서 쓰나요: 모바일/데스크톱 앱(Flutter)과 웹(Web) 양쪽에서 같은 이름의 옵션으로 동작합니다.
핵심 용어#
| 용어 | 쉬운 설명 |
|---|---|
| Chip(칩) | 화면에 뜨는 작은 라벨/태그/단추. 이름표나 작은 버튼표라고 생각하면 됩니다. |
| chipColor (색) | 칩이 켜졌을 때 입는 색(기본 중립 neutral / 브랜드 primary / 직접 지정 custom). 꺼진 칩은 색이 없습니다. |
| variant (꺼진 모양) | 꺼진 칩을 테두리만(outlined) 그릴지, 회색으로 채울지(filled) 고르는 설정. |
| emphasis (강조) | 켜진 칩을 진하게 채울지(high) 연하게 물들일지(low) 고르는 설정. |
| size (크기) | 칩의 크기(작음 sm 28 / 보통 md 32 / 큼 lg 40)를 고르는 설정. |
| shape (윤곽) | 칩의 윤곽 — 둥근 사각형(rounded, 기본) 또는 양 끝이 반원인 알약(pill). |
| prefixLabel (키 글자) | 필터 칩에서 값 앞에 붙는 "무엇을 거르는가" 글자 (예: "상태"). |
| selected (선택됨) | 칩이 눌려서 켜진 상태인지 표시하는 값. |
| onTap (탭/누르기) | 칩을 눌렀을 때 일어날 동작. |
| onRemove (삭제) | X 버튼을 눌러 칩을 지울 때 일어날 동작. 이 값이 있고 칩이 활성일 때 X 버튼이 보입니다. |
| leading / trailing (앞/뒤 슬롯) | 글자 앞이나 뒤에 아이콘 같은 걸 넣는 자리. |
| seedColor (씨앗 색) | 직접 지정(custom) 색 칩이 진하게(high) 켜졌을 때 칠하는 색. 연하게(low) 켜진 칩은 씨앗 색 대신 연한 회색으로 칠합니다. |
| 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, key + value filter chips, and removable chips. Four semantic axes each decide one thing:
| Parameter | Decides | Values |
|---|---|---|
chipColor |
the colour a selected chip takes |
neutral
(default) ·
primary
·
custom
(from
seedColor
at
high
emphasis)
|
variant |
how an unselected chip is drawn | outlined (default) · filled |
emphasis |
how loudly a selected chip states its colour | high (default, solid fill) · low (tint) |
shape |
the outline | rounded (default, the size row's corner) · pill |
size (CoreChipSize: sm · md · lg) picks the height ladder. An unselected chip is colourless whatever
chipColor says — the colour first appears when the chip is selected. variant
never touches a selected or a disabled chip.
-
Style slot:
chipStyle: CoreChipStyle(...)for per-instance chrome micro-tweaks. Semantic enums (chipColor/variant/emphasis/size/shape/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. The defaults are the enums' own statics:CoreChipColor.defaultColor,CoreChipVariant.defaultVariant,CoreChipEmphasis.defaultEmphasis,CoreChipSize.defaultSize,CoreChipShape.defaultShape. -
shapeis new in coui_core 0.163.0 · coui_flutter 0.164.0 · coui_web 0.163.0. The basis these skills are written against (coui_flutter 0.163.0, with coui_core / coui_web 0.162.0) does not have it — see Pill beforeshape.
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 |
|---|---|---|---|
prefixLabel |
String? |
null |
Key text drawn before the label — what a filter chip filters by ("Status" before "Open"). One line, never shrinks |
label |
String? |
null |
Text label displayed on the chip |
labelMaxLines |
int? |
null |
Line clamp for label; null keeps one line |
labelOverflow |
CoreTextOverflow? |
null |
clip
/
fade
/
ellipsis
/
visible
once the clamp is reached;
null
keeps
ellipsis
|
child |
Widget? |
null |
Custom child widget (alternative to label; wins when both set) |
chipColor |
CoreChipColor |
CoreChipColor.defaultColor (= neutral) |
Colour a selected chip takes |
variant |
CoreChipVariant |
CoreChipVariant.defaultVariant (= outlined) |
How an unselected chip is drawn |
emphasis |
CoreChipEmphasis |
CoreChipEmphasis.defaultEmphasis (= high) |
How loudly a selected chip states its colour |
selected |
bool |
false |
Whether the chip is selected |
enabled |
bool |
true |
Whether the chip is interactive |
size |
CoreChipSize |
CoreChipSize.defaultSize (= md) |
Chip size — sm / md / lg |
shape |
CoreChipShape |
CoreChipShape.defaultShape (= rounded) |
Outline — rounded / pill (coui_flutter 0.164.0+) |
leading |
Widget? |
null |
Widget displayed before the label |
trailing |
Widget? |
null |
Widget displayed after the label |
onTap |
VoidCallback? |
null |
Tap callback — composes a
Clickable
around the chip; fires only when
enabled
|
onRemove |
VoidCallback? |
null |
Remove callback — shows the close (X) button when non-null and enabled |
seedColor |
CoreColor? |
null |
Seed colour (used when chipColor is custom) |
maxLength |
int? |
null |
Max character length for the label (cuts and appends ...) |
chipStyle |
CoreChipStyle? |
null |
Per-instance chrome / dimensional / slot style |
Chip also takes key and has a copyWith(...) covering every parameter above.
Color — CoreChipColor#
Chip(label: 'Neutral', selected: true) // neutral (default)
Chip(label: 'Primary', chipColor: CoreChipColor.primary, selected: true)
Chip(
label: 'Custom',
chipColor: CoreChipColor.custom,
seedColor: CoreColor.token(CoreColors.tertiary),
selected: true,
)
CoreChipColor values (full set): custom, neutral, primary. It names the colour a chip takes
when selected: neutral selects to the inverse pair (inverseSurface
fill, inverseOnSurface label), primary to the brand colour. Status colours belong to
Badge, not to a control the user toggles.
At emphasis: high (the default), custom fills a selected chip with seedColor
(raw values like CoreColor(0xFF6750A4) also work). Its label is the seed's own on-colour: a token the scheme pairs (tertiary
→ onTertiary, success → onSuccess) takes that on-token; a raw colour takes white or black, whichever contrasts more with it; an unpaired token keeps white.
chipStyle: CoreChipStyle(customSelectedForegroundColor: …) overrides it.
At emphasis: low a selected custom chip does not paint the seed — a raw seed has no container tint to derive. It fills with
surfaceContainer and labels with onSurface (CoreChipStyle.defaultCustomLowSelectedBackgroundColor
/ defaultCustomLowSelectedForegroundColor); state your own tint through chipStyle: CoreChipStyle(customLowSelectedBackgroundColor: …, customLowSelectedForegroundColor: …).
customSelectedForegroundColor and the on-colour rule apply to the high row only.
The seed is never painted on an unselected chip — an unselected custom chip, or a custom
chip without a seedColor, draws the neutral row.
Enum-shorthand (inferred type) is idiomatic in real code:
Chip(label: '디자인', chipColor: .primary)
Unselected Look — CoreChipVariant#
Chip(
label: 'Recommended',
variant: CoreChipVariant.filled,
selected: isSelected,
onTap: handleSelect,
)
variant | Unselected chip | Fits |
|---|---|---|
outlined (default) |
transparent fill, quiet outlineVariant hairline |
selection / filter groups, on any surface |
filled |
surfaceContainerHigh fill; the border mirrors the fill, so no outline shows |
recommend / run-something chips, a light filter row on a white page |
filled belongs with emphasis: CoreChipEmphasis.high. A filled resting chip and a low-emphasis selected chip differ by a few percent of lightness, so the two states are told apart by hue alone (WCAG 1.4.1); debug builds report the pair
filled + low (it still renders). On a grey surface (surfaceContainer
and up) a filled chip sinks into the ground — use outlined there.
Emphasis — CoreChipEmphasis#
Chip(
label: 'Design',
chipColor: CoreChipColor.primary,
emphasis: CoreChipEmphasis.low, // primaryContainer tint when selected
selected: isSelected,
onTap: handleSelect,
)
emphasis and selected together pick one of four rows (outlined
shown):
| selected | emphasis | Fill | Border | Label |
|---|---|---|---|---|
| no | low |
none | outlineVariant |
onSurfaceVariant |
| no | high |
none | outlineVariant |
onSurface |
| yes | low |
{color}Container (neutral / custom: surfaceContainer) |
mirrors the fill | on{color}Container (neutral / custom: onSurface) |
| yes | high |
{color} (neutral: inverseSurface; custom: the seed) |
mirrors the fill | on{color} (neutral: inverseOnSurface; custom: the seed's on-colour) |
On an unselected chip emphasis only changes how dark the label reads.
Sizes — CoreChipSize#
Chip(label: 'SM', size: CoreChipSize.sm) // 28
Chip(label: 'MD', size: CoreChipSize.md) // 32 — default
Chip(label: 'LG', size: CoreChipSize.lg) // 40
CoreChipSize values: sm, md, lg — three tiers, not the shared component sizes. The 24 tier below belongs to
Badge and the 48 / 56 tiers above to Button. Each height is a minHeight
floor (CoreChipStyle.defaultsBySize[size]!.minHeight), so a chip matches the control of the same tier beside it.
Shape — CoreChipShape#
New in coui_core 0.163.0 · coui_flutter 0.164.0 · coui_web 0.163.0.
// skill-example: no-compile — `shape` is new in coui_flutter 0.164.0 / coui_web 0.163.0, after the 0.163.0 basis
Chip(label: 'USD', shape: CoreChipShape.pill)
shape | Corner | Fits |
|---|---|---|
rounded (default) |
the size row's corner —
sm
6 ·
md
8 ·
lg
10 (the chip's height × 0.25 on a
CoreRadius
rung)
|
tags and selection groups; the default corner since coui_core 0.162.0 · coui_flutter 0.163.0 · coui_web 0.162.0 |
pill |
both ends a half circle on every size — 14 · 16 · 20 on the 28 · 32 · 40 floors | filter, sort and currency chips laid out in one row |
pill is a shape, not a new radius value: its corner is the full rung CoreRadius.radius9999
(CoreChipStyle.defaultsByShape[CoreChipShape.pill]), and each platform's renderer clamps it to half the drawn height. No resolver computes
height / 2, so a chip that grows past its floor stays a pill. The close button's corner does not follow
shape: the sm row gives it the chip's 6, while md and lg
state none, so it keeps Button sm's own 8 (on an lg chip the X is 8, not 10). The focus ring follows whichever corner the face is drawn with.
Precedence — the corner resolves defaultsBySize[size] → defaultsByShape[shape]
→ theme → chipStyle. A borderRadius stated in chipStyle, or anywhere in the theme (CoreChipTheme.style
/ colorStyles / variantStyles), wins over both shapes; with one stated, shape
changes nothing:
// skill-example: no-compile — `shape` is new in coui_flutter 0.164.0 / coui_web 0.163.0, after the 0.163.0 basis
Chip(
label: 'USD',
shape: CoreChipShape.pill,
// Wins over the shape: this chip draws an 8 corner, not a pill.
chipStyle: const CoreChipStyle(
borderRadius: CoreBorderRadius.all(CoreRadius.radius8),
),
)
Pill before shape
On coui_flutter 0.163.0 / coui_web 0.162.0 there is no shape parameter, and the default corner is the size row's (6 · 8 · 10). State the full corner instead — both renderers clamp it to half the height the same way:
Chip(
label: 'USD',
chipStyle: const CoreChipStyle(
borderRadius: CoreBorderRadius.all(CoreRadius.radius9999),
),
)
Before coui_core 0.162.0 · coui_flutter 0.163.0 · coui_web 0.162.0 the default corner was already the pill (CoreChipStyle.defaultBorderRadius,
radius9999), so there a chip is a pill with nothing stated and the corner above is redundant.
Do not read CoreChipStyle.defaultBorderRadius for this: it is deprecated (since 0.162) because the default corner is per size (CoreChipStyle.defaultsBySize[size]!.borderRadius).
Selectable Chip#
Chip(
label: '디자인',
chipColor: CoreChipColor.primary,
selected: isSelected,
onTap: () => toggleSelection(),
)
Selection state is driven by selected; toggling happens in onTap — the chip never toggles itself. The resolver picks the colours per
chipColor × emphasis × state (see Color / Size token tables); the selected border mirrors the selected fill. Colour changes animate through the composed
OutlinedContainer over transitionDuration (default CoreDuration.instant, 100ms; zero under reduced motion). While hovered, focused or pressed, a tappable chip washes its own foreground over its fill (stateLayer, default
CoreStateLayer.subtle).
Filter Chip with a Key — prefixLabel#
Chip(
variant: CoreChipVariant.filled,
prefixLabel: 'Status',
label: isFiltered ? 'Open' : 'All',
trailing: const Icon(LucideIcons.chevronDown),
selected: isFiltered,
onTap: openStatusFilter,
)
The key is drawn one text role below the label on every size, at the role's own weight 500 (the label is 600):
| Size | Key (prefixLabel) | Label |
|---|---|---|
sm | labelXs (11) | labelSmall (12) |
md | labelSmall (12) | labelMedium (13) |
lg | labelMedium (13) | labelLarge (14) |
At rest the key reads onSurfaceVariant — quieter than a high-emphasis label, the same ink as a low-emphasis one (there the pair is told apart by size and weight). Once the chip is selected or disabled the key takes the label's colour. The key stays on one line and never shrinks; the label is the half that truncates. Override it through
chipStyle.prefixLabelStyle (a colour stated there wins in every state); chipStyle.foregroundColor
recolours the key along with the label. Use prefixLabel rather than a Row inside
child — neither the size ladder nor the key's colour rule reaches a hand-built row.
Removable Chip#
onRemove renders an internal close (X) button — but only while enabled is true. The X is the unified
Button(variant: .ghost, size: .sm) wrapping Icon(LucideIcons.x), so hover / focus / disabled states are handled by the design system. It is sized by the size row (button 18 / 20 / 24, glyph 16 / 18 / 20) and named for assistive technology from the active locale —
chipRemoveLabel with the key and label ("Remove {label}" in English, "{label} 제거" in Korean).
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. Each is wrapped in an IconTheme
carrying the size row's icon size (12 / 14 / 14) and the chip's state-resolved foreground (or the slot style's own colour), so you do not set them manually. The slot spacers are
Gap widgets driven by the size-aware iconLabelGapStyle slot (4 / 4 / 6).
Label Truncation#
Chip(label: 'A very long tag name', maxLength: 10) // renders 'A very lon...'
Chip(
label: 'A label allowed to wrap onto two lines',
labelMaxLines: 2,
labelOverflow: CoreTextOverflow.ellipsis,
)
By default the label is one line with an ellipsis (CoreChipContract.defaultLabelMaxLines
/ defaultLabelOverflow). maxLength cuts the string itself, so a screen reader hears the cut string. None of the three apply to a custom
child.
Disabled Chip#
Chip(label: 'Unavailable', enabled: false)
Disabled chips ignore onTap, hide the close button, and paint dedicated disabled tokens rather than an opacity: an unselected disabled chip is a
surface fill behind a disabledOutline hairline, a selected one a disabledContainer
fill with no border, both with a disabledContent label. variant and chipColor
do not touch a disabled chip. A tappable disabled chip shows the forbidden cursor through its composed
Clickable.
Per-instance Style — chipStyle: CoreChipStyle(...)#
Every chrome / dimensional / slot tweak flows through the single chipStyle slot. Semantic enums (chipColor
/ variant / emphasis / size / shape / selected) stay as widget parameters — do not pack them into the style.
Chip(
label: 'Custom chrome',
chipStyle: const CoreChipStyle(
borderRadius: CoreBorderRadius.all(CoreRadius.radius8),
borderWidth: CoreStrokeWidth.stroke1,
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space12,
vertical: CoreSpace.space4,
),
labelStyle: CoreTextStyle(fontWeight: CoreFontWeight.medium),
leadingIconStyle: CoreIconStyle(size: CoreSize.size16),
iconLabelGapStyle: CoreGapStyle(size: CoreSpace.space4),
),
)
CoreChipStyle fields:
| Field | Type | Description |
|---|---|---|
backgroundColor |
CoreColor? |
Fill override — wins across every state |
foregroundColor |
CoreColor? |
Label colour override — wins across every state; also the colour the key / leading / trailing / close slots inherit |
borderColor | CoreColor? | Border stroke colour |
borderWidth |
double? |
Border stroke width (logical px; default CoreStrokeWidth.stroke1) |
borderRadius |
CoreBorderRadius? |
Corner —
null
defers to the shape (pill) or the size row (6 / 8 / 10); a value wins over both shapes
|
padding |
CoreEdgeInsets? |
Padding (default per size) |
minHeight |
double? |
Height floor (default per size: 28 / 32 / 40) |
iconLabelGapStyle |
CoreGapStyle? |
Gap between leading / key / label / trailing / close slots |
closeIconStyle |
CoreIconStyle? |
Close (X) icon style |
transitionDuration |
Duration? |
Hover / focus / state colour transition (default CoreDuration.instant, 100ms) |
castHideDuration |
Duration? |
Clock for the cast hide and press travel (default CoreDuration.ms50) |
castMotion |
CoreCastMotion? |
What the cast does on engage —
defaultMotion
/
reverse
/
noShadow
|
labelStyle |
CoreTextStyle? |
Label text style (default per size:
labelSmall
/
labelMedium
/
labelLarge
, weight 600)
|
prefixLabelStyle |
CoreTextStyle? |
Key text style (default per size:
labelXs
/
labelSmall
/
labelMedium
)
|
leadingIconStyle |
CoreIconStyle? |
Leading icon style |
trailingIconStyle |
CoreIconStyle? |
Trailing icon style (does not affect the close button) |
closeButtonStyle |
CoreButtonStyle? |
Close-button chrome forwarded to the internal ghost Button |
clickableStyle |
CoreClickableStyle? |
Chrome for the Clickable composed around a tappable chip |
customDisabledBackgroundColor |
CoreColor? |
Disabled selected custom-seed fill |
customDisabledUnselectedBackgroundColor |
CoreColor? |
Disabled unselected custom-seed fill |
customDisabledForegroundColor |
CoreColor? |
Disabled custom-seed label |
customDisabledBorderColor |
CoreColor? |
Disabled unselected custom-seed border |
customSelectedForegroundColor |
CoreColor? |
Selected custom-seed label at high (default: the seed's on-colour) |
customLowSelectedBackgroundColor |
CoreColor? |
Selected custom-seed fill at low (default surfaceContainer) |
customLowSelectedForegroundColor |
CoreColor? |
Selected custom-seed label at low (default onSurface) |
lowUnselectedForegroundColor |
CoreColor? |
Unselected label at low |
lowUnselectedBorderColor |
CoreColor? |
Unselected border at low |
filledUnselectedBackgroundColor |
CoreColor? |
Unselected fill at filled (the border follows it) |
disabledSelectedBorderColor |
CoreColor? |
Selected disabled border (default none) |
stateLayer |
CoreStateLayer? |
Hover / focus / pressed wash (default CoreStateLayer.subtle) |
It also exposes merge, copyWith, and the shared design-system defaults as statics:
defaultsBySize (Map<CoreChipSize, CoreChipStyle>), defaultsByShape
(Map<CoreChipShape, CoreChipStyle>, coui_core 0.163.0+), defaultsByColor
(Map<CoreChipColor, CoreChipColorStyle> — neutral and primary;
custom derives from seedColor), defaultBorderWidth (stroke1),
defaultLabelStyle (labelMedium, weight 600), defaultPrefixLabelStyle
(labelSmall), defaultTransitionDuration (100ms), defaultCastHideDuration
(50ms), defaultCastMotion, defaultStateLayer, defaultDisabledOpacity
(1 — nothing dims the disabled tokens), defaultClickableStyle, defaultCloseButtonStyle, the custom-seed / low / filled / disabled colour defaults (defaultCustomDisabledBackgroundColor,
defaultCustomSelectedForegroundColor (white), defaultCustomSelectedDarkForegroundColor
(black), defaultLowUnselectedBorderColor, defaultFilledUnselectedBackgroundColor,
defaultDisabledSelectedBorderColor, …), and defaultCustomSelectedForegroundColorFor(fill).
defaultBorderRadius is deprecated (since 0.162).
Chain sugar — withStyle
Chip(label: 'Tag').radius16.primary
Chip(label: 'Tag').withStyle(
const CoreChipStyle(
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space12,
vertical: CoreSpace.space4,
),
),
)
withStyle(patch) merges a CoreChipStyle patch into chipStyle (later calls win). The getters are shorter sugar named after the token they set:
radius0 … radius9999 set borderRadius; primary, secondary,
tertiary, surface, surfaceContainer, error, success,
warning, info, outline and transparent set backgroundColor, which wins in every state.
.secondary / .tertiary are background tokens, not CoreChipColor
values.
Chip Group Pattern#
Wrap(
spacing: CoreSpace.space8,
runSpacing: CoreSpace.space8,
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) 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?. Its constructor is not
const. It renders a <div> with Tailwind classes plus inline styles synthesized by the resolver — the corner, padding and height floor always go out inline.
Chip(label: 'Flutter')
Shape (Web)#
// skill-example: no-compile — `shape` is new in coui_web 0.163.0, after the 0.162.0 Web package of the basis
Chip(label: 'USD', shape: CoreChipShape.pill)
Same values, default and precedence as Flutter. The pill goes out inline as the full corner (var(--coui-radius-9999)), and the browser scales overlapping corners down to half the box's height, so each end is a half circle. On coui_web 0.162.0, state
chipStyle: CoreChipStyle(borderRadius: CoreBorderRadius.all(CoreRadius.radius9999)) instead; before coui_web 0.162.0 a Web chip is already a pill with nothing stated.
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), named through aria-label
from the active locale.
Selectable Chip#
Chip(
label: '디자인',
chipColor: CoreChipColor.primary,
selected: isSelected,
onTap: () => toggleSelection(),
)
onTap composes a Clickable around the chip body: a <div role="button">
that is focusable (tabindex="0") while enabled, owns click / keyboard activation, the focus ring and press feedback, and carries
cursor-pointer (or cursor-not-allowed when disabled). onTap fires only when
enabled. The hover / focus / pressed wash is emitted as scoped <style>
rules (color-mix) keyed to a class on that root, and state colours fade with a CSS transition on the same
transitionDuration as Flutter.
Leading Slot (Component)#
Chip(
label: 'Verified',
leading: const Icon(LucideIcons.check),
chipColor: CoreChipColor.primary,
)
Leading / trailing slots are wrapped in inline-flex <span>s and handed the size row's icon style as their nearest
IconTheme; the slot 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(),
)
The remove button's name comes from prefixLabel / label only — a chip with just a
child gets a remove button whose name carries no item name, so fill label too on removable chips.
Disabled / Accessibility#
Chip(label: 'Unavailable', enabled: false)
A disabled Web chip gets aria-disabled="true", does not fire onTap, and hides the close button. Neither platform exposes
selected to assistive technology (no aria-pressed / aria-selected
on Web, no Semantics(selected:) on Flutter) — pass it yourself through attributes
on Web or a Semantics wrapper on Flutter.
Web-only extras#
The Web constructor additionally accepts the UiComponent passthrough id, classes,
css, attributes and eventHandlers. On a tappable chip id
and attributes land on the Clickable root, otherwise on the chip <div>;
classes and css always land on the chip <div>. eventHandlers
reach the DOM only on a display-only chip: once onTap is set, the chip <div>
gets no events and the composed Clickable is not given them, so they are silently dropped — handle the tap through
onTap. Keep classes / css to layout — chrome goes through chipStyle.
Chip(
label: 'Flutter',
id: 'tag-flutter',
classes: 'shrink-0',
attributes: {'data-testid': 'tag-flutter'},
)
Parameters (both platforms)#
| Parameter | Type (Flutter) | Type (Web) | Default | Notes |
|---|---|---|---|---|
prefixLabel |
String? |
String? |
null |
Key text before the label |
label |
String? |
String? |
null |
Plain-text label |
labelMaxLines |
int? |
int? |
null |
null = one line |
labelOverflow |
CoreTextOverflow? |
CoreTextOverflow? |
null |
null = ellipsis |
child |
Widget? |
Component? |
null |
Custom content; wins over label |
chipColor |
CoreChipColor |
CoreChipColor |
defaultColor (= neutral) |
custom / neutral / primary |
variant |
CoreChipVariant |
CoreChipVariant |
defaultVariant (= outlined) |
outlined / filled |
emphasis |
CoreChipEmphasis |
CoreChipEmphasis |
defaultEmphasis (= high) |
low / high |
selected |
bool |
bool |
false |
Selected state |
enabled |
bool |
bool |
true |
Gates onTap and the close button |
size |
CoreChipSize |
CoreChipSize |
defaultSize (= md) |
sm / md / lg |
shape |
CoreChipShape |
CoreChipShape |
defaultShape (= rounded) |
rounded / pill — coui_flutter 0.164.0 / coui_web 0.163.0 |
leading |
Widget? |
Component? |
null |
Before the label |
trailing |
Widget? |
Component? |
null |
After the label |
onTap |
VoidCallback? |
CoreVoidCallback? |
null |
Tap / click |
onRemove |
VoidCallback? |
CoreVoidCallback? |
null |
Shows close (X) button when non-null and enabled |
seedColor |
CoreColor? |
CoreColor? |
null |
For chipColor: custom |
maxLength |
int? |
int? |
null |
Label cut with ... |
chipStyle |
CoreChipStyle? |
CoreChipStyle? |
null |
Per-instance chrome/slot style |
Both platforms: copyWith(...) and the withStyle chain. Web-only: id,
classes, css, attributes, eventHandlers.
Color / Size token tables#
Both platforms resolve from the same single source of truth in coui_core (CoreChipStyle.defaultsByColor
/ defaultsBySize / defaultsByShape, plus the flat colour defaults on CoreChipStyle).
Colours (CoreChipColor — full set):
| State | neutral |
primary |
custom (with seedColor) |
|---|---|---|---|
Unselected · outlined (fill / border) |
transparent / outlineVariant |
same | same |
Unselected · filled (fill = border) |
surfaceContainerHigh |
same | same |
Unselected label · high / low |
onSurface / onSurfaceVariant |
same | same |
Unselected key (prefixLabel) |
onSurfaceVariant |
same | same |
Selected · high (fill / label) |
inverseSurface / inverseOnSurface |
primary / onPrimary |
seed / the seed's on-colour |
Selected · low (fill / label) |
surfaceContainer / onSurface |
primaryContainer / onPrimaryContainer |
surfaceContainer / onSurface |
| Disabled · unselected (fill / label / border) | surface / disabledContent / disabledOutline |
same | same |
| Disabled · selected (fill / label / border) | disabledContainer / disabledContent / none |
same | same |
A selected border mirrors the selected fill at either emphasis, and a selected chip draws its key in the label's colour.
custom has no defaultsByColor entry — its selected high colours derive from
seedColor, its selected low colours are the flat defaultCustomLowSelected*
defaults, and its unselected ones are the neutral row.
Sizes (CoreChipSize — full set; token values, pre-scaling):
| Size | Height floor | Corner (rounded / pill) |
Padding (H × V) | Label role | Key role | Leading/Trailing icon | Close button / icon | Gap |
|---|---|---|---|---|---|---|---|---|
sm |
size28 |
radius6 / 14 |
space8 × space2 |
labelSmall (12) |
labelXs (11) |
size12 |
18 × 18 (corner radius6, the chip's) / size16 |
space4 |
md |
size32 |
radius8 / 16 |
space10 × space2 |
labelMedium (13) |
labelSmall (12) |
size14 |
20 × 20 (corner 8, Button sm's) / size18 |
space4 |
lg |
size40 |
radius10 / 20 |
space12 × space2 |
labelLarge (14) |
labelMedium (13) |
size14 |
24 × 24 (corner 8, Button sm's) / size20 |
space6 |
Every label role carries weight 600 (CoreFontWeight.semiBold) on top of the role; the key keeps its role's 500. The pill column is the full corner
radius9999 as the renderer clamps it to half the floor height. Border width default CoreStrokeWidth.stroke1.
Project-level theming — CoreChipTheme#
CoreChipTheme has three slots — style, variantStyles (keyed by
CoreChipVariant) and colorStyles (keyed by CoreChipColor). Semantic identifiers (chipColor
/ variant / emphasis / size / shape) are widget-only; the theme cannot set them, and
emphasis / size / shape key no theme map. Resolve chain:
CoreChipStyle.defaultsBySize[size] → CoreChipStyle.defaultsByShape[shape] // design-system default
→ CoreChipTheme.style // 프로젝트 공통
→ CoreChipTheme.colorStyles[widget.chipColor]
→ CoreChipTheme.variantStyles[widget.variant]
→ widget.chipStyle // 인스턴스별
const CoreChipTheme(
style: CoreChipStyle(
transitionDuration: Duration(milliseconds: CoreDuration.fast),
),
variantStyles: {
CoreChipVariant.filled: CoreChipStyle(
filledUnselectedBackgroundColor: CoreColor.token(
CoreColors.surfaceContainer,
),
),
},
colorStyles: {
CoreChipColor.primary: CoreChipStyle(
borderWidth: CoreStrokeWidth.stroke1,
),
},
)
Register it as CoreComponentTheme(chip: …) on the app theme's coreComponentTheme
— both platform resolvers read coreComponentTheme?.chip. A borderRadius in any theme slot draws every chip it reaches with that corner, whatever
shape the widget states.
Flutter ↔ Web Differences#
These are the behavioural differences confirmed in source — everything else (enums, parameter names, defaults, the
chipStyle slot) is identical.
| Aspect | Flutter | Web |
|---|---|---|
| Widget class |
Chip
(StatelessWidget,
const
ctor,
CoreChipContract<Widget>
)
|
Chip
(UiComponent, non-
const
ctor,
CoreChipContract<Component>
)
|
child / leading / trailing type |
Widget? |
Component? |
| Callback type | VoidCallback? | CoreVoidCallback? |
| Label render |
Text(label)
in a
Flexible
,
maxLines
/
overflow
from
labelMaxLines
/
labelOverflow
(one line + ellipsis by default)
|
<span>
with
truncate
by default; the
Text
's inline clamp once
labelMaxLines
/
labelOverflow
is stated
|
| Tap wiring | composed Clickable (inside a TouchTarget) when onTap != null |
composed
Clickable
root
<div role="button">
when
onTap != null
|
| Disabled affordance | Clickable forbidden cursor (tappable chips) |
aria-disabled="true"
;
cursor-not-allowed
on the
Clickable
(tappable chips)
|
| State transition | OutlinedContainer animates over transitionDuration |
CSS transition on the same duration |
| Hover / focus / pressed wash | four pre-resolved container styles, selected by state | scoped <style> rules (color-mix) on the Clickable root |
pill ends |
the painted corner radius9999 scales to half the height |
inline var(--coui-radius-9999); the browser scales overlapping corners |
filled + low report (debug only) |
FlutterError.reportError |
debugWarn (console.warn in the browser, nothing on the server) |
| Remove-button name | Semantics(label:) | aria-label |
| Extra ctor params | — |
id
,
classes
,
css
,
attributes
,
eventHandlers
|
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) -
Filter chips that show what is filtered and its current value (
prefixLabel+label) -
A row of filter / sort / currency chips with half-circle ends (
shape: CoreChipShape.pill, coui_flutter 0.164.0 / coui_web 0.163.0) - User / contact / email selection chips with removal (
onRemove) - Removable search criteria / token input chips
- Managed selection over a whole set →
ChipGroup<T>instead - Status strings, counts or dots without selection →
Badgeinstead
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. -
shapedoes not exist on coui_flutter 0.163.0 / coui_web 0.162.0 (the skills' basis) — state the full corner throughchipStyle.borderRadiusthere. -
A stated
borderRadius(chipStyle, the theme, or a.radiusNchain getter) wins overshape;shape: CoreChipShape.pillthen changes nothing. -
CoreChipStyle.defaultBorderRadiusis deprecated — it is a size-independent pill, while the default corner is per size (defaultsBySize[size]!.borderRadius). -
The close (X) button only renders when
onRemoveis non-null andenabledis true — settingonRemovealone is not enough on a disabled chip. -
CoreChipColorhas onlycustom,neutralandprimary, andsizeisCoreChipSize(sm/md/lg) —CoreComponentSizedoes not type-check. -
chipColor: customonly paintsseedColoron a selected chip atemphasis: high— a selectedlowcustom chip issurfaceContainer/onSurfaceunlesscustomLowSelectedBackgroundColor/customLowSelectedForegroundColorsay otherwise; without aseedColorthe chip draws the neutral row. -
variant: CoreChipVariant.filledwithemphasis: CoreChipEmphasis.lowtells the two states apart by hue alone; debug builds report it. Pairfilledwithhigh, or useoutlined. -
The chain colour getters (
.primary,.success, …) andchipStyle.backgroundColor/foregroundColorwin across every state, so selection stops changing what they pin. -
Semantic enums (
chipColor/variant/emphasis/size/shape/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/closeIconStyle. -
Web
Chiphas noconstconstructor, and itscopyWith— sowithStyleand the chain getters too — carriesidover but notclasses/css/attributes/eventHandlers. It declaresclasses,cssandattributesparameters but ignores them, so evenchip.copyWith(classes: 'x')drops the value you pass; set those on the constructor. -
On a tappable Web chip (
onTapset)eventHandlersare dropped — they reach the DOM only on a display-only chip.
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) 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 legacy surface (
Chip.button(...)factory, standaloneChipStyle/ChipTheme/ChipCloseButtonclasses) no longer exists. UseChip+ its semantic params /chipStyle(CoreChipStyle) /CoreChipThemeonly.CoreChipSelectedStyle/CoreChipUnselectedStyleare still exported by coui_core, butChipreads neither — useemphasis/variantinstead. -
coui_flutter 0.141:
sizebecameCoreChipSize(sm·md·lg) → ❌CoreComponentSize.xs…xlon a chip no longer type-checks. -
coui_flutter 0.161:
CoreChipColornarrowed tocustom·neutral·primary(❌secondary/tertiaryremoved — usecustom+seedColor),variant: CoreChipVariantwas added,CoreChipTheme.variantStylesis now keyed byCoreChipVariantwith per-colour overrides moved tocolorStyles, andCoreChipStyle.defaultsByVariantbecamedefaultsByColor. -
coui_core 0.162.0 · coui_flutter 0.163.0 · coui_web 0.162.0: the default corner became per size (6 · 8 · 10) — before it every chip was a pill — and
CoreChipStyle.defaultBorderRadius(that size-independent pill) is deprecated (since 0.162). A chip that has to stay a pill on these releases statesradius9999throughchipStyle.borderRadius. -
coui_core 0.163.0 · coui_flutter 0.164.0 · coui_web 0.163.0:
shape: CoreChipShape(rounded·pill) — a pill chip no longer needs a statedradius9999corner. -
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;).