문서 목차
이 페이지에서목차

coui-chip

CoUI로 태그나 선택·제거 가능한 작은 라벨을 만들 때 사용합니다. Chip의 색·형태·강조와 닫기 동작을 구성합니다.

Quick Reference#

  • Widget: Chip
  • Colour when selected: CoreChipColor — custom, neutral, primary (default neutral)
  • Unselected look: CoreChipVariant — outlined, filled (default outlined)
  • Emphasis: CoreChipEmphasis — low, high (default high)
  • Size: CoreChipSize — sm / md / lg = 28 / 32 / 40 (default md)
  • Shape: CoreChipShape — rounded, pill (default rounded) — 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:

ParameterDecidesValues
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.
  • shape is 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 before shape.

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
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,
)
variantUnselected chipFits
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):

selectedemphasisFillBorderLabel
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)
shapeCornerFits
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):

SizeKey (prefixLabel)Label
smlabelXs (11)labelSmall (12)
mdlabelSmall (12)labelMedium (13)
lglabelMedium (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:

FieldTypeDescription
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
borderColorCoreColor?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)#

ParameterType (Flutter)Type (Web)DefaultNotes
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.

AspectFlutterWeb
Widget class Chip (StatelessWidget, const ctor, CoreChipContract<Widget> ) Chip (UiComponent, non- const ctor, CoreChipContract<Component> )
child / leading / trailing type Widget? Component?
Callback typeVoidCallback?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 nameSemantics(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 → Badge 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.
  • shape does not exist on coui_flutter 0.163.0 / coui_web 0.162.0 (the skills' basis) — state the full corner through chipStyle.borderRadius there.
  • A stated borderRadius (chipStyle, the theme, or a .radiusN chain getter) wins over shape; shape: CoreChipShape.pill then changes nothing.
  • CoreChipStyle.defaultBorderRadius is deprecated — it is a size-independent pill, while the default corner is per size (defaultsBySize[size]!.borderRadius).
  • 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.
  • CoreChipColor has only custom, neutral and primary, and size is CoreChipSize (sm / md / lg) — CoreComponentSize does not type-check.
  • chipColor: custom only paints seedColor on a selected chip at emphasis: high — a selected low custom chip is surfaceContainer / onSurface unless customLowSelectedBackgroundColor / customLowSelectedForegroundColor say otherwise; without a seedColor the chip draws the neutral row.
  • variant: CoreChipVariant.filled with emphasis: CoreChipEmphasis.low tells the two states apart by hue alone; debug builds report it. Pair filled with high, or use outlined.
  • The chain colour getters (.primary, .success, …) and chipStyle.backgroundColor / foregroundColor win across every state, so selection stops changing what they pin.
  • Semantic enums (chipColor / variant / emphasis / size / shape / 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 / closeIconStyle.
  • Web Chip has no const constructor, and its copyWith — so withStyle and the chain getters too — carries id over but not classes / css / attributes / eventHandlers. It declares classes, css and attributes parameters but ignores them, so even chip.copyWith(classes: 'x') drops the value you pass; set those on the constructor.
  • On a tappable Web chip (onTap set) eventHandlers are 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, standalone ChipStyle / ChipTheme / ChipCloseButton classes) no longer exists. Use Chip + its semantic params / chipStyle (CoreChipStyle) / CoreChipTheme only. CoreChipSelectedStyle / CoreChipUnselectedStyle are still exported by coui_core, but Chip reads neither — use emphasis / variant instead.
  • coui_flutter 0.141: size became CoreChipSize (sm · md · lg) → ❌ CoreComponentSize.xs … xl on a chip no longer type-checks.
  • coui_flutter 0.161: CoreChipColor narrowed to custom · neutral · primary (❌ secondary / tertiary removed — use custom + seedColor), variant: CoreChipVariant was added, CoreChipTheme.variantStyles is now keyed by CoreChipVariant with per-colour overrides moved to colorStyles, and CoreChipStyle.defaultsByVariant became defaultsByColor.
  • 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 states radius9999 through chipStyle.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 stated radius9999 corner.
  • 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;).

검색어를 입력하면 문서를 찾습니다.

↑ ↓ 이동 · Enter 열기Esc 닫기