Chip#
태그, 필터 옵션, 선택 상태 등을 표시하는 칩 컴포넌트입니다. 삭제 버튼, 아바타, 선택 상태를 지원합니다.
Live Preview#
class ChipSelectedExample extends StatefulComponent {
const ChipSelectedExample({super.key});
@override
State<ChipSelectedExample> createState() => _ChipSelectedExampleState();
}
class _ChipSelectedExampleState extends State<ChipSelectedExample> {
bool _selected = true;
@override
Component build(BuildContext context) {
return Chip(
label: 'Tag',
chipColor: CoreChipColor.primary,
selected: _selected,
onTap: () => setState(() => _selected = !_selected),
);
}
}
class ChipSelectedExample extends StatefulWidget {
const ChipSelectedExample({super.key});
@override
State<ChipSelectedExample> createState() => _ChipSelectedExampleState();
}
class _ChipSelectedExampleState extends State<ChipSelectedExample> {
bool _selected = true;
@override
Widget build(BuildContext context) {
return Chip(
label: 'Tag',
chipColor: CoreChipColor.primary,
selected: _selected,
onTap: () => setState(() => _selected = !_selected),
);
}
}
class ChipRemovableExample extends StatefulComponent {
const ChipRemovableExample({super.key});
@override
State<ChipRemovableExample> createState() => _ChipRemovableExampleState();
}
class _ChipRemovableExampleState extends State<ChipRemovableExample> {
bool _visible = true;
@override
Component build(BuildContext context) {
if (!_visible) return Text('');
return Chip(
label: 'Removable',
onRemove: () => setState(() => _visible = false),
onTap: () {},
);
}
}
class ChipRemovableExample extends StatefulWidget {
const ChipRemovableExample({super.key});
@override
State<ChipRemovableExample> createState() => _ChipRemovableExampleState();
}
class _ChipRemovableExampleState extends State<ChipRemovableExample> {
bool _visible = true;
@override
Widget build(BuildContext context) {
if (!_visible) return const SizedBox.shrink();
return Chip(
label: 'Removable',
onRemove: () => setState(() => _visible = false),
onTap: () {},
);
}
}
class ChipDisabledExample extends StatefulComponent {
const ChipDisabledExample({super.key});
@override
State<ChipDisabledExample> createState() => _ChipDisabledExampleState();
}
class _ChipDisabledExampleState extends State<ChipDisabledExample> {
@override
Component build(BuildContext context) {
return Chip(
label: 'Tag',
enabled: false,
onTap: () {
setState(() {});
},
);
}
}
class ChipDisabledExample extends StatefulWidget {
const ChipDisabledExample({super.key});
@override
State<ChipDisabledExample> createState() => _ChipDisabledExampleState();
}
class _ChipDisabledExampleState extends State<ChipDisabledExample> {
@override
Widget build(BuildContext context) {
return Chip(
label: 'Tag',
enabled: false,
onTap: () {
setState(() {});
},
);
}
}
class ChipCastMotionExample extends StatefulComponent {
const ChipCastMotionExample({super.key});
@override
State<ChipCastMotionExample> createState() => _ChipCastMotionExampleState();
}
class _ChipCastMotionExampleState extends State<ChipCastMotionExample> {
@override
Component build(BuildContext context) {
return div(
[
Chip(
label: 'Default',
chipStyle: const CoreChipStyle(
castMotion: CoreCastMotion.defaultMotion,
),
onTap: () {},
),
const Gap.space12(),
Chip(
label: 'No shadow',
chipStyle: const CoreChipStyle(castMotion: CoreCastMotion.noShadow),
onTap: () {},
),
const Gap.space12(),
Chip(
label: 'Reverse',
chipStyle: const CoreChipStyle(castMotion: CoreCastMotion.reverse),
onTap: () {},
),
],
classes: 'flex flex-row items-center',
);
}
}
class ChipCastMotionExample extends StatefulWidget {
const ChipCastMotionExample({super.key});
@override
State<ChipCastMotionExample> createState() => _ChipCastMotionExampleState();
}
class _ChipCastMotionExampleState extends State<ChipCastMotionExample> {
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Chip(
label: 'Default',
chipStyle: const CoreChipStyle(
castMotion: CoreCastMotion.defaultMotion,
),
onTap: () {},
),
const Gap.space12(),
Chip(
label: 'No shadow',
chipStyle: const CoreChipStyle(castMotion: CoreCastMotion.noShadow),
onTap: () {},
),
const Gap.space12(),
Chip(
label: 'Reverse',
chipStyle: const CoreChipStyle(castMotion: CoreCastMotion.reverse),
onTap: () {},
),
],
);
}
}
class ChipChainExample extends StatefulComponent {
const ChipChainExample({super.key});
@override
State<ChipChainExample> createState() => _ChipChainExampleState();
}
class _ChipChainExampleState extends State<ChipChainExample> {
bool _comboSelected = false;
bool _fullSelected = false;
@override
Component build(BuildContext context) {
return div(
[
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
Chip(
label: 'Tag',
selected: _comboSelected,
onTap: () => setState(() => _comboSelected = !_comboSelected),
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
Chip(
label: 'Full control',
selected: _fullSelected,
onTap: () => setState(() => _fullSelected = !_fullSelected),
).withStyle(
const CoreChipStyle(
backgroundColor: CoreColor.token(CoreColors.tertiaryContainer),
foregroundColor: CoreColor.token(CoreColors.onTertiaryContainer),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space12,
vertical: CoreSpace.space4,
),
),
),
],
classes: 'flex flex-col items-start',
);
}
}
class ChipChainExample extends StatefulWidget {
const ChipChainExample({super.key});
@override
State<ChipChainExample> createState() => _ChipChainExampleState();
}
class _ChipChainExampleState extends State<ChipChainExample> {
bool _comboSelected = false;
bool _fullSelected = false;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
Chip(
label: 'Tag',
selected: _comboSelected,
onTap: () => setState(() => _comboSelected = !_comboSelected),
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
Chip(
label: 'Full control',
selected: _fullSelected,
onTap: () => setState(() => _fullSelected = !_fullSelected),
).withStyle(
const CoreChipStyle(
backgroundColor: CoreColor.token(CoreColors.tertiaryContainer),
foregroundColor: CoreColor.token(CoreColors.onTertiaryContainer),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space12,
vertical: CoreSpace.space4,
),
),
),
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 카테고리, 태그, 키워드를 시각적으로 표현할 때
- 필터 옵션을 선택/해제할 수 있는 토글 가능한 레이블이 필요할 때
- 선택된 항목 목록을 삭제 가능한 태그로 표시할 때 (예: 이메일 수신자 목록)
- 다중 선택 UI에서 선택된 항목을 표시할 때
대신 다른 컴포넌트를 사용하세요:
Badge: 상태나 숫자를 표시하는 작은 표시자가 필요할 때 (상호작용 없음)Button: 클릭 시 명확한 단일 액션이 실행되어야 할 때ChipInput: 텍스트를 직접 입력하여 칩을 생성하는 입력 필드가 필요할 때
기본 사용법 (Basic Usage)#
// 기본 칩
Chip(
label: 'Flutter',
)
// 삭제 가능한 칩
Chip(
label: 'Dart',
onRemove: handleDeleteDart,
)
// leading 위젯 포함 칩
Chip(
label: '홍길동',
leading: Avatar(
imageUrl: 'https://example.com/avatar.jpg',
size: CoreComponentSize.xs,
),
onRemove: handleDeleteUser,
)
// 선택 가능한 칩
Chip(
label: '디자인',
chipColor: CoreChipColor.primary,
selected: isDesignSelected,
onTap: handleSelectDesign,
)
// 기본 칩
Chip(label: 'Flutter')
// 삭제 가능한 칩
Chip(
label: 'Dart',
onRemove: handleDeleteDart,
)
// leading 포함 칩
Chip(
label: '홍길동',
leading: Avatar(
initials: '홍',
size: CoreComponentSize.xs,
),
onRemove: handleDeleteUser,
)
// 선택 가능한 칩
Chip(
label: '디자인',
chipColor: CoreChipColor.primary,
selected: isDesignSelected,
onTap: handleSelectDesign,
)
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
label |
String? |
null |
칩에 표시할 텍스트 |
child |
Widget? / Component? |
null |
커스텀 내용 (label 대체) |
chipColor |
CoreChipColor |
.defaultColor (neutral) |
칩 색상 변형 |
size |
CoreComponentSize |
md |
칩 크기 |
selected | bool | false | 선택 상태 여부 |
enabled | bool | true | 활성화 여부 |
onTap |
VoidCallback? |
null |
탭 핸들러 |
onRemove |
VoidCallback? |
null |
삭제 버튼 클릭 핸들러. null이면 삭제 버튼 미표시 |
leading |
Widget? / Component? |
null |
레이블 앞 위젯 |
trailing |
Widget? / Component? |
null |
레이블 뒤 위젯 |
seedColor |
CoreColor? |
null |
커스텀 시드 색상 |
maxLength |
int? |
null |
레이블 최대 글자 수 |
chipStyle |
CoreChipStyle? |
null |
인스턴스 스타일 (Style 시스템 참조) |
스타일 시스템 (Style System)#
Chip 의 모든 chrome / dimensional / nested-slot 오버라이드는 CoreChipStyle 단일 슬롯으로 흐릅니다. 시맨틱 enum (chipColor
/ size / selected / enabled) 과 behaviour 필드는 위젯 파라미터로 직접 전달합니다.
시맨틱 vs 스타일#
-
시맨틱 enum / behaviour: 위젯 파라미터로 직접 (
chipColor,size,selected,enabled,onTap,onRemove,label,leading,trailing,seedColor,maxLength) - chrome / dimensional / 슬롯 스타일:
CoreChipStyle한 곳으로 (아래 필드 표 참고) -
close 버튼 변형 교체: asChild — 직접
Button(variant: ghost, ...)위젯 주입은 권장 X.closeButtonStyle로 chrome 만 조정하면 됩니다. 내부적으로Button(variant: .ghost, size: .sm)가 사용됩니다.
Resolve chain#
design system default for chip
→ CoreChipTheme.style // 프로젝트 공통
→ parent component slot override
→ widget.chipStyle // 인스턴스별
각 nested 슬롯 스타일 (labelStyle / leadingIconStyle / trailingIconStyle
/ closeIconStyle / closeButtonStyle / clickableStyle) 은 자기 컴포넌트의 자체 resolve chain 으로 다시 한 번 머지됩니다.
CoreChipStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
backgroundColor |
CoreColor? |
Panel background fill colour override — wins across every state.
null
defers to the state-keyed [defaultsByVariant] base; see the note above for why there is no
default*
.
|
foregroundColor |
CoreColor? |
Foreground (label) colour override — wins across every state, and is also the fallback the label / leading / trailing / close slots inherit when they carry no colour of their own.
null
defers to the state-keyed [defaultsByVariant] base; see the note above.
Deliberately has no default* — absence is the design.
Same state-keyed table (selectedFg / unselectedFg / disabledFg / seed) and the same absence test. Additionally it is the fallback the label, leading, trailing and close-button slots inherit when they carry no colour of their own, so pinning it silently freezes four nested slots as well.
|
borderColor | CoreColor? | Border stroke colour. |
borderWidth |
double? |
Border stroke width (logical px). |
borderRadius |
CoreBorderRadius? |
Border radius. When null falls back to the chip design-token radius. |
padding | CoreEdgeInsets? | Padding. |
minHeight |
double? |
Minimum pill height (logical px). Per-size default from [defaultsBySize] — the spec's height ladder, stated as a floor so the pill matches the control tier beside it rather than its own label. |
iconLabelGapStyle |
CoreGapStyle? |
Nested [CoreGapStyle] slot for the leading / label / trailing spacers. Forwarded straight to the
Gap(gapStyle: …)
widgets that separate the slots by the Flutter resolver; consumed by the Web resolver to emit the inline
gap
rem rule.
null
defers to
CoreChipStyle.defaultsBySize[size]!.iconLabelGapStyle!
.
|
closeIconStyle |
CoreIconStyle? |
Close-button (X) icon style. Distinct from [leadingIconStyle] / [trailingIconStyle] which target the leading / trailing content slots; this drives the icon rendered inside the
onRemove
close button — forwarded straight to
Icon(iconStyle: …)
by the resolver.
|
transitionDuration |
Duration? |
Transition duration for hover / focus / state colour changes. When null falls back to [defaultTransitionDuration]. |
castHideDuration |
Duration? |
The engage-squash pair's shared clock — how long the shadow takes to hide and the chip takes to travel into the space it vacated.
null
defers to [defaultCastHideDuration]. Reduced-motion collapses this to zero on both platforms — state (the hide, the travel) is unaffected.
|
labelStyle |
CoreTextStyle? |
Label text style override. |
leadingIconStyle |
CoreIconStyle? |
Leading icon style override. |
trailingIconStyle |
CoreIconStyle? |
Trailing icon style override (does not affect the close button — [closeButtonStyle] does). |
closeButtonStyle |
CoreButtonStyle? |
Close (
onRemove
) button chrome. Each rendered close button uses
Button(variant: ghost)
internally; this slot lets the caller tweak chrome (padding / iconStyle).
|
clickableStyle |
CoreClickableStyle? |
Nested [CoreClickableStyle] slot for the
Clickable
composed around a tappable (
onTap != null
) chip (press scale / durations / focus ring / disabled opacity). Merged on top of [defaultClickableStyle] and raw-forwarded — the Clickable's own resolver fills the rest.
|
customDisabledBackgroundColor |
CoreColor? |
Disabled custom-seed chip background override.
null
→ [defaultCustomDisabledBackgroundColor].
|
customDisabledForegroundColor |
CoreColor? |
Disabled custom-seed chip foreground override.
null
→ [defaultCustomDisabledForegroundColor].
|
customDisabledBorderColor |
CoreColor? |
Disabled custom-seed chip border override. null → [defaultCustomDisabledBorderColor]. |
customSelectedForegroundColor |
CoreColor? |
Selected-state custom-seed chip label colour override.
null
→ [defaultCustomSelectedForegroundColor].
|
lowUnselectedForegroundColor |
CoreColor? |
Label colour override for an unselected chip at [CoreChipEmphasis.low]; falls back to [defaultLowUnselectedForegroundColor]. |
lowUnselectedBorderColor |
CoreColor? |
Border colour override for an unselected chip at [CoreChipEmphasis.low]; falls back to [defaultLowUnselectedBorderColor]. |
customLowSelectedBackgroundColor |
CoreColor? |
Fill override for a selected custom-seed chip at [CoreChipEmphasis.low] — the tint a raw seed cannot derive on its own; falls back to [defaultCustomLowSelectedBackgroundColor]. |
customLowSelectedForegroundColor |
CoreColor? |
Label override for a selected custom-seed chip at [CoreChipEmphasis.low]; falls back to [defaultCustomLowSelectedForegroundColor]. |
stateLayer |
CoreStateLayer? |
Ramp laid over the chip's own fill while hovered, focused or pressed. Defaults to [defaultStateLayer]. One ramp for every fill this chip can wear — the overlay is the chip's current foreground over its current background, so a selected chip and an unselected one wash from their own ground rather than from a shared one. |
castMotion |
CoreCastMotion? |
What this chip's cast does when the chip is engaged. Null takes [defaultCastMotion]. See [CoreCastMotion] — the value says when the cast changes; what it is worth stays with the surface style. |
CoreChipStyle 변형별 기본값 (CoreChipColorStyle)#
| 필드 | neutral |
primary |
secondary |
tertiary |
|---|---|---|---|---|
selectedBg |
surfaceContainerHigh | primary | secondary | tertiary |
selectedFg |
onSurface | onPrimary | onSecondary | onTertiary |
lowSelectedBg |
surfaceContainer | primaryContainer | secondaryContainer | tertiaryContainer |
lowSelectedFg |
onSurface | onPrimaryContainer | onSecondaryContainer | onTertiaryContainer |
unselectedBg |
transparent | transparent | transparent | transparent |
unselectedFg |
onSurface | primary | secondary | tertiary |
borderColor | outline | primary | secondary | tertiary |
disabledBg |
disabledContainer | disabledContainer | disabledContainer | disabledContainer |
disabledFg |
onSurfaceVariant | onSurfaceVariant | onSurfaceVariant | onSurfaceVariant |
disabledBorderColor |
disabledOutline | disabledOutline | disabledOutline | disabledOutline |
사용 예 (Flutter)#
Chip(
label: 'Selected',
chipColor: CoreChipColor.primary,
selected: true,
onRemove: handleRemove,
chipStyle: CoreChipStyle(
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space12,
vertical: CoreSpace.space6,
),
borderRadius: CoreBorderRadius.all(CoreRadius.radius16),
labelStyle: CoreTextStyle(fontWeight: CoreFontWeight.semiBold),
closeButtonStyle: CoreButtonStyle(
padding: CoreEdgeInsets.all(CoreSpace.space4),
),
),
)
사용 예 (Web)#
Chip(
label: 'Selected',
chipColor: CoreChipColor.primary,
selected: true,
onRemove: handleRemove,
chipStyle: CoreChipStyle(
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space12,
vertical: CoreSpace.space6,
),
borderRadius: CoreBorderRadius.all(CoreRadius.radius16),
),
)
빠른 오버라이드 (Chain)#
이미 만든 Chip 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
.radius8처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius8 ==
CoreRadius.radius8) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.
class ChipChainExample extends StatefulWidget {
const ChipChainExample({super.key});
@override
State<ChipChainExample> createState() => _ChipChainExampleState();
}
class _ChipChainExampleState extends State<ChipChainExample> {
bool _comboSelected = false;
bool _fullSelected = false;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
Chip(
label: 'Tag',
selected: _comboSelected,
onTap: () => setState(() => _comboSelected = !_comboSelected),
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
Chip(
label: 'Full control',
selected: _fullSelected,
onTap: () => setState(() => _fullSelected = !_fullSelected),
).withStyle(
const CoreChipStyle(
backgroundColor: CoreColor.token(CoreColors.tertiaryContainer),
foregroundColor: CoreColor.token(CoreColors.onTertiaryContainer),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space12,
vertical: CoreSpace.space4,
),
),
),
],
);
}
}
class ChipChainExample extends StatefulComponent {
const ChipChainExample({super.key});
@override
State<ChipChainExample> createState() => _ChipChainExampleState();
}
class _ChipChainExampleState extends State<ChipChainExample> {
bool _comboSelected = false;
bool _fullSelected = false;
@override
Component build(BuildContext context) {
return div(
[
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
Chip(
label: 'Tag',
selected: _comboSelected,
onTap: () => setState(() => _comboSelected = !_comboSelected),
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
Chip(
label: 'Full control',
selected: _fullSelected,
onTap: () => setState(() => _fullSelected = !_fullSelected),
).withStyle(
const CoreChipStyle(
backgroundColor: CoreColor.token(CoreColors.tertiaryContainer),
foregroundColor: CoreColor.token(CoreColors.onTertiaryContainer),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space12,
vertical: CoreSpace.space4,
),
),
),
],
classes: 'flex flex-col items-start',
);
}
}
변형 (Variants)#
Neutral (기본)#
중립 색상의 기본 칩입니다.
Chip(label: 'Neutral', chipColor: CoreChipColor.neutral)
Primary#
강조 색상 칩입니다.
Chip(label: 'Primary', chipColor: CoreChipColor.primary)
Secondary / Tertiary#
보조 강조 칩입니다.
Chip(label: 'Secondary', chipColor: CoreChipColor.secondary)
Chip(label: 'Tertiary', chipColor: CoreChipColor.tertiary)
Custom (seedColor)#
seedColor 로 임의 색을 파생시킵니다. chipColor 를 custom 으로 두고 씨드 색을 넘깁니다.
Chip(
label: 'Custom',
chipColor: CoreChipColor.custom,
seedColor: CoreColor.token(CoreColors.tertiary),
)
칩 그룹#
여러 칩을 필터 그룹으로 묶어 사용할 수 있습니다.
Wrap(
spacing: CoreSpace.space8,
children: [
Chip(label: 'Flutter', onRemove: () => handleDelete('flutter')),
Chip(label: 'Dart', onRemove: () => handleDelete('dart')),
Chip(label: 'UI', onRemove: () => handleDelete('ui')),
],
)
동작 스펙 (Behavior)#
인터랙션#
- 클릭/탭 (선택형):
onTap콜백 실행.selected상태 갱신은 부모에서 처리 - 삭제 버튼 클릭:
onRemove콜백 실행. 칩 제거는 부모에서 처리 - 호버: 배경색 미세 변화로 상호작용 가능 상태 표시
- 포커스: 합성된
Clickable의 포커스 링 표시
상태 전환#
default→hover→pressed→defaultselected상태 렌더는selected파라미터가 결정 (컴포넌트가 자체 토글하지 않음)-
enabled: false: 클릭 불가, 전용 disabled 토큰(disabledBg/disabledFg/disabledBorderColor)으로 렌더
애니메이션#
- 선택/호버 색 전환:
chipStyle.transitionDuration(기본CoreDuration.instant= 100ms) - 삭제 시 (부모 목록에서):
AnimatedSize또는AnimatedOpacity사용 권장
캐스트 모션 (castMotion)#
chipStyle.castMotion: CoreCastMotion? 은 칩이 engage 될 때 그림자(캐스트)가 언제 · 어느 쪽으로 바뀌는지를 정합니다. 세 값입니다:
| 값 | rest | engage | 참조 이름 |
|---|---|---|---|
defaultMotion (기본) |
캐스트가 있음 | 캐스트가 사라지고, 칩이 그 자리로 이동 | default |
reverse |
캐스트가 없음 | 캐스트가 생기고, 칩이 반대 방향으로 이동 | reverse |
noShadow | 캐스트 없음 | 변화 없음 (그림자도 이동도) | noShadow |
defaultMotion 과 reverse 는 그림자 유무와 이동 방향이 하나의 짝으로 묶여 있습니다 — 숨김과 이동을 다른 시계에 태우면 한 번의 누름이 두 개의 사건으로 보입니다. 탭 핸들러가 없는(onTap == null, 예:
ChipInput 안의 표시 전용 칩) 칩은 애초에 engage 할 수단이 없으므로, reverse 를 고르면 캐스트가 rest 그대로 계속 숨어 있습니다 — engage 없이 자라날 수 없기 때문입니다.
engage — hover 인가 press 인가
Button 과 동일하게, "engage" 는 하나의 입력이 아닙니다. 포인터가 있는 기기에서는 hover, 없는 기기에서는 press 가 engage 를 켭니다.
| 기기 | engage 트리거 |
|---|---|
| 포인터 있음 (마우스 · 트랙패드) | hover |
| 포인터 없음 (터치 전용) | press |
이 축이 참조하는 디자인은 웹 전용 라이브러리라 :hover 만 말합니다. 그대로 옮기면 터치 기기에서 깨집니다 — 터치의 :hover
는 브라우저가 탭을 흉내 내어 켜고 다음 탭까지 꺼지지 않으므로, 칩이 탭 이후 계속 눌린 모양으로 굳어 있게 됩니다. 그래서 데스크톱은 hover, 터치는 press 로 갈립니다 — Web 은
@media (hover: hover), Flutter 는 hover 이벤트가 데스크톱에만 도달하는 성질을 그대로 씁니다.
Button 과 다른 점 — 채움 게이트가 없습니다
Button 은 배경도 테두리도 그리지 않는 variant(ghost/link/text/plain)에서 캐스트를 걸러내는 채움 게이트를 갖고, 호출자가 명시한
boxShadow 가 이 축을 이깁니다. Chip 에는 둘 다 없습니다 — CoreChipStyle 에 boxShadow
슬롯 자체가 없고(칩은 자기 그림자를 직접 조합하는 용도가 아님), 모든 chipColor variant 가 기본으로 테두리(borderWidth
기본값 stroke1)를 그리므로 "던질 박스가 없는" 상태가 되지 않습니다. 그래서 castMotion 은 활성 스타일이 캐스트를 publish 하기만 하면 배경 채움 여부와 무관하게 항상 적용됩니다 —
chipStyle.borderWidth 를 0 으로 직접 낮추고 배경도 투명으로 둔 경우는 예외이며, 그 조합에서는 박스 없이 그림자만 뜬 모양이 될 수 있습니다.
눈에 보이려면 스타일이 캐스트를 publish 해야 합니다
castMotion 은 언제 캐스트가 바뀌는지만 말합니다. 캐스트 자체(오프셋 · 색 · 두께)는 서페이스 스타일
이 진술합니다. 오늘은 Neo-brutalism 프리셋만 이 값을 publish 합니다 — 나머지(Default · Liquid Glass · Neumorphism · Claymorphism) 아래에서는
castMotion 을 무엇으로 바꾸든 화면이 그대로입니다. 위 라이브 프리뷰가 아무 변화도 안 보인다면 결함이 아니라 활성 프리셋이 이 축을 publish 하지 않는다는 뜻입니다 — 프리셋 스위처로 Neo-brutalism 을 켜면 세 값의 차이가 드러납니다.
사용 가이드라인 (Usage Guidelines)#
✅ Do#
필터 그룹에서 선택형 칩 사용
Wrap(
spacing: CoreSpace.space8,
children: categories.map((cat) => Chip(
label: cat.name,
chipColor: CoreChipColor.primary,
selected: selectedCategories.contains(cat.id),
onTap: () => handleToggleCategory(cat.id),
)).toList(),
)
선택형 칩은 필터 옵션을 토글하는 데 직관적인 UI를 제공합니다.
❌ Don't#
칩에 긴 텍스트 사용 금지
// ❌ 너무 긴 레이블
Chip(
label: '사용자가 최근에 방문한 카테고리 항목', // 너무 길다
)
칩은 짧고 간결한 레이블에 최적화되어 있습니다. 긴 텍스트는 레이아웃을 깨뜨리고 가독성을 저해합니다. 2~3단어 이내로 제한하세요.
✅ Do#
삭제 가능한 칩 목록에 애니메이션 적용
AnimatedList(
initialItemCount: tags.length,
itemBuilder: (context, index, animation) => SizeTransition(
sizeFactor: animation,
child: Chip(
label: tags[index],
onRemove: () => handleRemoveTag(index),
),
),
)
칩 삭제 시 자연스러운 애니메이션은 사용자에게 변화를 시각적으로 전달합니다.
❌ Don't#
비활성화된 칩에 삭제 버튼 표시 금지
// ❌ enabled: false 이지만 onRemove 가 있음
Chip(
label: '읽기 전용 태그',
enabled: false,
onRemove: handleDelete, // disabled 상태에서는 의미 없음
)
비활성화된 칩에 삭제 버튼이 있으면 사용자가 혼란스러워합니다. enabled: false 일 때는 onRemove 를 null 로 설정하세요.
✅ Do#
Chip의 label은 간결하게 작성하세요.
// ✅ 짧고 명확한 레이블
Chip(label: '모바일', onTap: handleMobileSelected)
Chip(label: 'UX', onTap: handleUxSelected)
Chip(label: 'Flutter', onTap: handleFlutterSelected)
Chip은 키워드나 태그처럼 짧은 텍스트를 표시하는 용도입니다. 간결한 레이블이 스캔과 선택을 쉽게 만듭니다.
❌ Don't#
Chip에 긴 문장을 넣지 마세요.
// ❌ 너무 긴 Chip 레이블
Chip(
label: '사용자가 직접 등록한 관심 카테고리', // 너무 길다
onTap: handleCategorySelected,
)
긴 텍스트는 Chip의 시각적 일관성을 깨고 레이아웃을 혼란스럽게 만듭니다. Badge나 Tag 컴포넌트를 고려하세요.
접근성 (Accessibility)#
키보드 인터랙션#
| 키 | 동작 |
|---|---|
Enter / Space | 선택형 칩 토글 |
Delete / Backspace | 삭제 가능한 칩 제거 |
Tab | 다음 칩 또는 삭제 버튼으로 이동 |
스크린 리더#
-
Flutter:
Semantics로role="checkbox"(선택형) 또는role="button"(삭제형) 전달 -
Web: 선택형 칩에
aria-selected, 삭제 버튼에aria-label="[레이블] 제거"적용
터치 타겟#
-
칩은 작은 사이즈에서 24 (
CoreTouchTarget.minimum) 아래로 그려지므로,TouchTarget(Flutter) /coui-touch-target(Web)이 그리는 크기는 그대로 둔 채 닿는 범위만 그 하한까지 넓힙니다. - 삭제 버튼 별도 터치 영역: 최소 24x24dp
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 그룹 레이아웃 | Wrap 위젯 | flexbox 자동 처리 |
| 색 전환 | AnimatedContainer (transitionDuration) |
CSS transition (동일 토큰) |
관련 컴포넌트 (Related Components)#
- ChipInput: 텍스트 입력으로 칩을 동적으로 추가하는 입력 필드
- Badge: 상호작용 없이 상태나 숫자만 표시할 때 사용
- Button: 클릭 시 명확한 단일 액션이 실행되어야 할 때
조합 예제#
// Chip + ChipInput 조합으로 태그 입력 UI 구현
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: CoreSpace.space8,
runSpacing: CoreSpace.space4,
children: selectedTags.map((tag) => Chip(
label: tag,
onRemove: () => handleRemoveTag(tag),
)).toList(),
),
Gap(gapStyle: CoreGapStyle(size: CoreSpace.space8)),
ChipInput(
placeholder: '태그 추가...',
onSubmitted: handleAddTag,
),
],
)