Chip#
태그, 필터 옵션, 선택 상태 등을 표시하는 칩 컴포넌트입니다. 삭제 버튼, 아바타, 선택 상태를 지원합니다.
Live Preview#
class ChipSelectedExample extends StatefulComponent {
const ChipSelectedExample({super.key});
@override
State<ChipSelectedExample> createState() => _ChipSelectedExampleState();
}
class _ChipSelectedExampleState extends State<ChipSelectedExample> {
bool _isSelected = true;
@override
Component build(BuildContext context) {
return CoChip(
label: 'Tag',
chipColor: CoreChipColor.primary,
isSelected: _isSelected,
onTap: () => setState(() => _isSelected = !_isSelected),
);
}
}
class ChipSelectedExample extends StatefulWidget {
const ChipSelectedExample({super.key});
@override
State<ChipSelectedExample> createState() => _ChipSelectedExampleState();
}
class _ChipSelectedExampleState extends State<ChipSelectedExample> {
bool _isSelected = true;
@override
Widget build(BuildContext context) {
return CoChip(
label: 'Tag',
chipColor: CoreChipColor.primary,
isSelected: _isSelected,
onTap: () => setState(() => _isSelected = !_isSelected),
);
}
}
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 CoChip(
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 CoChip(
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 CoChip(
label: 'Tag',
isDisabled: true,
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 CoChip(
label: 'Tag',
isDisabled: true,
onTap: () {
setState(() {});
},
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 카테고리, 태그, 키워드를 시각적으로 표현할 때
- 필터 옵션을 선택/해제할 수 있는 토글 가능한 레이블이 필요할 때
- 선택된 항목 목록을 삭제 가능한 태그로 표시할 때 (예: 이메일 수신자 목록)
- 다중 선택 UI에서 선택된 항목을 표시할 때
대신 다른 컴포넌트를 사용하세요:
Badge: 상태나 숫자를 표시하는 작은 표시자가 필요할 때 (상호작용 없음)Button: 클릭 시 명확한 단일 액션이 실행되어야 할 때ChipInput: 텍스트를 직접 입력하여 칩을 생성하는 입력 필드가 필요할 때
기본 사용법 (Basic Usage)#
// 기본 칩
CoChip(
label: 'Flutter',
)
// 삭제 가능한 칩
CoChip(
label: 'Dart',
onRemove: handleDeleteDart,
)
// leading 위젯 포함 칩
CoChip(
label: '홍길동',
leading: CoAvatar(
imageUrl: 'https://example.com/avatar.jpg',
size: CoreComponentSize.xs,
),
onRemove: handleDeleteUser,
)
// 선택 가능한 칩
CoChip(
label: '디자인',
chipColor: CoreChipColor.primary,
isSelected: isDesignSelected,
onTap: handleSelectDesign,
)
// 기본 칩
CoChip(label: 'Flutter')
// 삭제 가능한 칩
CoChip(
label: 'Dart',
onRemove: handleDeleteDart,
)
// leading 포함 칩
CoChip(
label: '홍길동',
leading: CoAvatar(
initials: '홍',
size: CoreComponentSize.xs,
),
onRemove: handleDeleteUser,
)
// 선택 가능한 칩
CoChip(
label: '디자인',
chipColor: CoreChipColor.primary,
isSelected: isDesignSelected,
onTap: handleSelectDesign,
)
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
label |
String? |
null |
칩에 표시할 텍스트 |
child |
Widget? |
null |
커스텀 내용 (label 대체) |
chipColor |
CoreChipColor |
neutral |
칩 색상 변형 |
size |
CoreComponentSize |
md |
칩 크기 |
isSelected |
bool |
false |
선택 상태 여부 |
isDisabled |
bool |
false |
비활성화 여부 |
onTap |
VoidCallback? |
null |
탭 핸들러 |
onRemove |
VoidCallback? |
null |
삭제 버튼 클릭 핸들러. null이면 삭제 버튼 미표시 |
leading |
Widget? |
null |
레이블 앞 위젯 |
trailing |
Widget? |
null |
레이블 뒤 위젯 |
seedColor |
Color? / CoreColor? |
null |
커스텀 시드 색상 |
maxLength |
int? |
null |
레이블 최대 글자 수 |
chipStyle |
CoreChipStyle<Color>? / CoreChipStyle<CoreColor>? |
null |
인스턴스 스타일 (Style 시스템 참조) |
스타일 시스템 (Style System)#
Chip 의 모든 chrome / dimensional / nested-slot 오버라이드는 CoreChipStyle<Clr> 단일 슬롯으로 흐릅니다 (Epic #1302 원칙 6/7/8). 시맨틱 enum (chipColor
/ size / isSelected / isDisabled) 과 behaviour 필드는 위젯 파라미터로 직접 전달합니다.
시맨틱 vs 스타일#
-
시맨틱 enum / behaviour: 위젯 파라미터로 직접 (
chipColor,size,isSelected,isDisabled,onTap,onRemove,label,leading,trailing,seedColor,maxLength) -
chrome / dimensional / 슬롯 스타일:
CoreChipStyle한 곳으로 (backgroundColor/foregroundColor/borderColor/borderWidth/borderRadius/paddingH/paddingV/gap/labelStyle/leadingIconStyle/trailingIconStyle/closeButtonStyle) -
close 버튼 변형 교체: asChild — 직접
CoButton(variant: ghost, ...)위젯 주입은 권장 X.closeButtonStyle로 chrome 만 조정하면 됩니다 (Epic #1302 원칙 8). 내부적으로CoButton(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 |
Color? |
칩 배경 (variant 기본값 위에 오버라이드) |
foregroundColor | Color? | 라벨 전경 색 |
borderColor | Color? | 보더 색 |
borderWidth | double? | 보더 두께 (px) |
borderRadius |
double? |
모서리 반지름 (px). null → 칩 토큰 기본값 |
paddingH | double? | 좌/우 패딩 (px) |
paddingV | double? | 위/아래 패딩 (px) |
gap | double? | leading / label / trailing 간격 (px) |
labelStyle | CoreTextStyle<Color>? | 라벨 텍스트 스타일 |
leadingIconStyle |
CoreIconStyle<Color>? |
leading 아이콘 슬롯 스타일 |
trailingIconStyle |
CoreIconStyle<Color>? |
trailing 아이콘 슬롯 스타일 (close 와 다름) |
closeButtonStyle |
CoreButtonStyle<Color>? |
onRemove close 버튼 chrome (CoButton(variant: ghost) 으로 렌더) |
Migration — 옛 평면 chrome → 새 위치 매핑#
CoreChipTheme 의 paddingH / paddingV 필드는 legacy 호환을 위해 보존되지만, 새 코드에서는
chipStyle.paddingH / paddingV 를 사용하세요.
| 옛 위치 (legacy) | 새 위치 |
|---|---|
CoreChipTheme.paddingH |
chipStyle.paddingH (인스턴스) 또는 CoreChipTheme.style.paddingH (프로젝트) |
CoreChipTheme.paddingV |
chipStyle.paddingV 또는 CoreChipTheme.style.paddingV |
| 인스턴스 라벨 텍스트 스타일 (불가능) | chipStyle.labelStyle |
| 인스턴스 아이콘 스타일 (불가능) | chipStyle.leadingIconStyle / trailingIconStyle |
| 인스턴스 close 버튼 chrome (불가능) | chipStyle.closeButtonStyle |
사용 예 (Flutter)#
CoChip(
label: 'Selected',
chipColor: CoreChipColor.primary,
isSelected: true,
onRemove: handleRemove,
chipStyle: CoreChipStyle(
paddingH: 12,
paddingV: 6,
borderRadius: 16,
labelStyle: CoreTextStyle(fontWeight: 600),
closeButtonStyle: CoreButtonStyle(
paddingH: 4,
paddingV: 4,
),
),
)
사용 예 (Web)#
CoChip(
label: 'Selected',
chipColor: CoreChipColor.primary,
isSelected: true,
onRemove: handleRemove,
chipStyle: CoreChipStyle<CoreColor>(
paddingH: 12,
paddingV: 6,
borderRadius: 16,
),
)
변형 (Variants)#
Neutral (기본)#
중립 색상의 기본 칩입니다.
CoChip(label: 'Neutral', chipColor: CoreChipColor.neutral)
Primary#
강조 색상 칩입니다.
CoChip(label: 'Primary', chipColor: CoreChipColor.primary)
Destructive#
위험/삭제 표시용 칩입니다.
CoChip(label: 'Destructive', chipColor: CoreChipColor.destructive)
칩 그룹#
여러 칩을 필터 그룹으로 묶어 사용할 수 있습니다.
Wrap(
spacing: 8,
children: [
CoChip(label: 'Flutter', onRemove: () => handleDelete('flutter')),
CoChip(label: 'Dart', onRemove: () => handleDelete('dart')),
CoChip(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) => CoChip(
label: cat.name,
chipColor: CoreChipColor.primary,
isSelected: selectedCategories.contains(cat.id),
onTap: () => handleToggleCategory(cat.id),
)).toList(),
)
선택형 칩은 필터 옵션을 토글하는 데 직관적인 UI를 제공합니다.
❌ Don't#
칩에 긴 텍스트 사용 금지
// ❌ 너무 긴 레이블
CoChip(
label: '사용자가 최근에 방문한 카테고리 항목', // 너무 길다
)
칩은 짧고 간결한 레이블에 최적화되어 있습니다. 긴 텍스트는 레이아웃을 깨뜨리고 가독성을 저해합니다. 2~3단어 이내로 제한하세요.
✅ Do#
삭제 가능한 칩 목록에 애니메이션 적용
AnimatedList(
initialItemCount: tags.length,
itemBuilder: (context, index, animation) => SizeTransition(
sizeFactor: animation,
child: CoChip(
label: tags[index],
onRemove: () => handleRemoveTag(index),
),
),
)
칩 삭제 시 자연스러운 애니메이션은 사용자에게 변화를 시각적으로 전달합니다.
❌ Don't#
비활성화된 칩에 삭제 버튼 표시 금지
// ❌ disabled이지만 onDelete가 있음
CoChip(
label: '읽기 전용 태그',
isDisabled: true,
onRemove: handleDelete, // disabled 상태에서는 의미 없음
)
비활성화된 칩에 삭제 버튼이 있으면 사용자가 혼란스러워합니다. disabled: true일 때는 onDelete를 null로 설정하세요.
✅ Do#
Chip의 label은 간결하게 작성하세요.
// ✅ 짧고 명확한 레이블
CoChip(label: '모바일', onTap: handleMobileSelected)
CoChip(label: 'UX', onTap: handleUxSelected)
CoChip(label: 'Flutter', onTap: handleFlutterSelected)
Chip은 키워드나 태그처럼 짧은 텍스트를 표시하는 용도입니다. 간결한 레이블이 스캔과 선택을 쉽게 만듭니다.
❌ Don't#
Chip에 긴 문장을 넣지 마세요.
// ❌ 너무 긴 Chip 레이블
CoChip(
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 |
|---|---|---|
| 클래스명 | CoChip | CoChip |
| 탭 핸들러 | 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) => CoChip(
label: tag,
onRemove: () => handleRemoveTag(tag),
)).toList(),
),
SizedBox(height: 8),
CoChipInput(
placeholder: '태그 추가...',
onAdd: handleAddTag,
),
],
)