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(() {});
},
);
}
}
사용 시기 (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? |
null |
커스텀 내용 (label 대체) |
chipColor |
CoreChipColor |
neutral |
칩 색상 변형 |
size |
CoreComponentSize |
md |
칩 크기 |
selected | bool | false | 선택 상태 여부 |
enabled | bool | true | 활성화 여부 |
onTap |
VoidCallback? |
null |
탭 핸들러 |
onRemove |
VoidCallback? |
null |
삭제 버튼 클릭 핸들러. null이면 삭제 버튼 미표시 |
leading |
Widget? |
null |
레이블 앞 위젯 |
trailing |
Widget? |
null |
레이블 뒤 위젯 |
seedColor |
CoreColor? |
null |
커스텀 시드 색상 |
maxLength |
int? |
null |
레이블 최대 글자 수 |
chipStyle |
CoreChipStyle? |
null |
인스턴스 스타일 (Style 시스템 참조) |
스타일 시스템 (Style System)#
Chip 의 모든 chrome / dimensional / nested-slot 오버라이드는 CoreChipStyle 단일 슬롯으로 흐릅니다 (Epic #1302 원칙 6/7/8). 시맨틱 enum (chipColor
/ size / selected / enabled) 과 behaviour 필드는 위젯 파라미터로 직접 전달합니다.
시맨틱 vs 스타일#
-
시맨틱 enum / behaviour: 위젯 파라미터로 직접 (
chipColor,size,selected,enabled,onTap,onRemove,label,leading,trailing,seedColor,maxLength) -
chrome / dimensional / 슬롯 스타일:
CoreChipStyle한 곳으로 (backgroundColor/foregroundColor/borderColor/borderWidth/borderRadius/padding/iconLabelGapStyle/labelStyle/leadingIconStyle/trailingIconStyle/closeButtonStyle) -
close 버튼 변형 교체: asChild — 직접
Button(variant: ghost, ...)위젯 주입은 권장 X.closeButtonStyle로 chrome 만 조정하면 됩니다 (Epic #1302 원칙 8). 내부적으로Button(variant: ghost)가 사용됩니다.
Resolve chain#
design system default for chip
→ CoreChipTheme.style // 프로젝트 공통
→ parent component slot override
→ widget.chipStyle // 인스턴스별
각 nested 슬롯 스타일 (labelStyle / leadingIconStyle / trailingIconStyle
/ closeButtonStyle) 은 자기 컴포넌트의 자체 resolve chain 으로 다시 한 번 머지됩니다.
슬롯 매핑 (CoreChipStyle 12 필드)#
| 필드 | 타입 (Flutter) | 적용 영역 |
|---|---|---|
backgroundColor |
CoreColor? |
칩 배경 (variant 기본값 위에 오버라이드) |
foregroundColor | CoreColor? | 라벨 전경 색 |
borderColor | CoreColor? | 보더 색 |
borderWidth | double? | 보더 두께 (px) |
borderRadius |
CoreBorderRadius? |
모서리 반지름. null → 칩 토큰 기본값 |
padding | CoreEdgeInsets? | 패딩 |
iconLabelGapStyle |
CoreGapStyle? |
leading / label / trailing 간격 슬롯 (per-size default 위에 오버라이드) |
labelStyle | CoreTextStyle? | 라벨 텍스트 스타일 |
leadingIconStyle |
CoreIconStyle? |
leading 아이콘 슬롯 스타일 |
trailingIconStyle |
CoreIconStyle? |
trailing 아이콘 슬롯 스타일 (close 와 다름) |
closeButtonStyle |
CoreButtonStyle? |
onRemove close 버튼 chrome (Button(variant: ghost) 으로 렌더) |
Migration — 옛 평면 chrome → 새 위치 매핑#
CoreChipTheme 의 paddingH / paddingV 평면 chrome 필드는 제거되었습니다 (verify.md 7-6 (i) —
style 슬롯과 chrome 평면 필드 공존 금지). 패딩 오버라이드는 chipStyle 슬롯 또는 CoreChipTheme.style
로 전달합니다.
| 옛 위치 (제거됨) | 새 위치 |
|---|---|
CoreChipTheme.paddingH / paddingV |
chipStyle.padding (인스턴스) 또는 CoreChipTheme.style.padding (프로젝트) |
| 인스턴스 라벨 텍스트 스타일 (불가능) | chipStyle.labelStyle |
| 인스턴스 아이콘 스타일 (불가능) | chipStyle.leadingIconStyle / trailingIconStyle |
| 인스턴스 close 버튼 chrome (불가능) | chipStyle.closeButtonStyle |
사용 예 (Flutter)#
Chip(
label: 'Selected',
chipColor: CoreChipColor.primary,
selected: true,
onRemove: handleRemove,
chipStyle: CoreChipStyle(
padding: CoreEdgeInsets.symmetric(horizontal: 12, vertical: 6),
borderRadius: CoreBorderRadius.all(16),
labelStyle: CoreTextStyle(fontWeight: 600),
closeButtonStyle: CoreButtonStyle(
padding: CoreEdgeInsets.all(4),
),
),
)
사용 예 (Web)#
Chip(
label: 'Selected',
chipColor: CoreChipColor.primary,
selected: true,
onRemove: handleRemove,
chipStyle: CoreChipStyle(
padding: CoreEdgeInsets.symmetric(horizontal: 12, vertical: 6),
borderRadius: CoreBorderRadius.all(16),
),
)
변형 (Variants)#
Neutral (기본)#
중립 색상의 기본 칩입니다.
Chip(label: 'Neutral', chipColor: CoreChipColor.neutral)
Primary#
강조 색상 칩입니다.
Chip(label: 'Primary', chipColor: CoreChipColor.primary)
Destructive#
위험/삭제 표시용 칩입니다.
Chip(label: 'Destructive', chipColor: CoreChipColor.destructive)
칩 그룹#
여러 칩을 필터 그룹으로 묶어 사용할 수 있습니다.
Wrap(
spacing: 8,
children: [
Chip(label: 'Flutter', onRemove: () => handleDelete('flutter')),
Chip(label: 'Dart', onRemove: () => handleDelete('dart')),
Chip(label: 'UI', onRemove: () => handleDelete('ui')),
],
)
동작 스펙 (Behavior)#
인터랙션#
- 클릭/탭 (선택형):
onSelected콜백으로selected상태 토글 - 삭제 버튼 클릭:
onDelete콜백 실행. 칩 제거는 부모에서 처리 - 호버: 배경색 미세 변화로 상호작용 가능 상태 표시
- 포커스: 포커스 링 표시
상태 전환#
default→hover→pressed→defaultselected상태에서 클릭:onSelected(false)호출disabled상태: 클릭 불가, 반투명 처리
애니메이션#
- 선택 상태 전환: 배경색 150ms ease
- 삭제 시 (부모 Wrap에서):
AnimatedSize또는AnimatedOpacity사용 권장
사용 가이드라인 (Usage Guidelines)#
✅ Do#
필터 그룹에서 선택형 칩 사용
Wrap(
spacing: 8,
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#
비활성화된 칩에 삭제 버튼 표시 금지
// ❌ disabled이지만 onDelete가 있음
Chip(
label: '읽기 전용 태그',
enabled: false,
onRemove: handleDelete, // disabled 상태에서는 의미 없음
)
비활성화된 칩에 삭제 버튼이 있으면 사용자가 혼란스러워합니다. disabled: true일 때는 onDelete를 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="[레이블] 제거"적용
터치 타겟#
- 최소 터치 타겟: 32px (칩 높이) + 패딩 포함 48dp 보장
- 삭제 버튼 별도 터치 영역: 최소 24x24dp
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | Chip | Chip |
| 탭 핸들러 | onTap | onTap |
| 삭제 핸들러 | onRemove | onRemove |
| 색상 변형 | chipColor: CoreChipColor |
chipColor: CoreChipColor |
| 그룹 레이아웃 | Wrap 위젯 | flexbox 자동 처리 |
관련 컴포넌트 (Related Components)#
- ChipInput: 텍스트 입력으로 칩을 동적으로 추가하는 입력 필드
- Badge: 상호작용 없이 상태나 숫자만 표시할 때 사용
- Button: 클릭 시 명확한 단일 액션이 실행되어야 할 때
조합 예제#
// Chip + ChipInput 조합으로 태그 입력 UI 구현
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 8,
runSpacing: 4,
children: selectedTags.map((tag) => Chip(
label: tag,
onRemove: () => handleRemoveTag(tag),
)).toList(),
),
SizedBox(height: 8),
ChipInput(
placeholder: '태그 추가...',
onAdd: handleAddTag,
),
],
)