ChipGroup#
단일 또는 다중 선택을 지원하는 칩 그룹 컴포넌트입니다. Wrap/Scroll 레이아웃,
최대 선택 수 제한, 타이틀/힌트 헤더를 지원합니다. 칩 자체는 Chip 으로
렌더링되며 사용자는 chipBuilder 를 통해 다른 chip variant 도 사용할 수
있습니다.
Live Preview#
class ChipGroupDefaultExample extends StatefulComponent {
const ChipGroupDefaultExample({super.key});
@override
State<ChipGroupDefaultExample> createState() =>
_ChipGroupDefaultExampleState();
}
class _ChipGroupDefaultExampleState extends State<ChipGroupDefaultExample> {
Set<String> _selected = const {'Flutter'};
@override
Component build(BuildContext context) {
return ChipGroup<String>(
items: const ['Flutter', 'React', 'Swift', 'Kotlin'],
initialValue: _selected,
selectionMode: CoreChipGroupSelectionMode.multi,
onChanged: (next) => setState(() => _selected = next),
chipBuilder:
(item, {required selected, required enabled, required onTap}) =>
Chip(
label: item,
selected: selected,
enabled: enabled,
chipColor: CoreChipColor.primary,
onTap: onTap,
),
);
}
}
class ChipGroupDefaultExample extends StatefulWidget {
const ChipGroupDefaultExample({super.key});
@override
State<ChipGroupDefaultExample> createState() =>
_ChipGroupDefaultExampleState();
}
class _ChipGroupDefaultExampleState extends State<ChipGroupDefaultExample> {
Set<String> _selected = const {'Flutter'};
@override
Widget build(BuildContext context) {
return ChipGroup<String>(
items: const ['Flutter', 'React', 'Swift', 'Kotlin'],
initialValue: _selected,
selectionMode: CoreChipGroupSelectionMode.multi,
onChanged: (next) => setState(() => _selected = next),
chipBuilder: (item, {required selected, required enabled, required onTap}) =>
Chip(
label: item,
selected: selected,
enabled: enabled,
chipColor: CoreChipColor.primary,
onTap: onTap,
),
);
}
}
class ChipGroupScrollExample extends StatefulComponent {
const ChipGroupScrollExample({super.key});
@override
State<ChipGroupScrollExample> createState() =>
_ChipGroupScrollExampleState();
}
class _ChipGroupScrollExampleState extends State<ChipGroupScrollExample> {
Set<String> _selected = const {};
@override
Component build(BuildContext context) {
return ChipGroup<String>(
items: const [
'All',
'Frontend',
'Backend',
'Mobile',
'Design',
'DevOps',
'Data',
'AI/ML',
],
layout: CoreChipGroupLayout.scroll,
selectionMode: CoreChipGroupSelectionMode.single,
initialValue: _selected,
onChanged: (next) => setState(() => _selected = next),
chipBuilder:
(item, {required selected, required enabled, required onTap}) =>
Chip(
label: item,
selected: selected,
enabled: enabled,
chipColor: CoreChipColor.neutral,
onTap: onTap,
),
);
}
}
class ChipGroupScrollExample extends StatefulWidget {
const ChipGroupScrollExample({super.key});
@override
State<ChipGroupScrollExample> createState() =>
_ChipGroupScrollExampleState();
}
class _ChipGroupScrollExampleState extends State<ChipGroupScrollExample> {
Set<String> _selected = const {};
@override
Widget build(BuildContext context) {
return ChipGroup<String>(
items: const [
'All',
'Frontend',
'Backend',
'Mobile',
'Design',
'DevOps',
'Data',
'AI/ML',
],
layout: CoreChipGroupLayout.scroll,
selectionMode: CoreChipGroupSelectionMode.single,
initialValue: _selected,
onChanged: (next) => setState(() => _selected = next),
chipBuilder: (item, {required selected, required enabled, required onTap}) =>
Chip(
label: item,
selected: selected,
enabled: enabled,
chipColor: CoreChipColor.neutral,
onTap: onTap,
),
);
}
}
class ChipGroupChainExample extends StatefulComponent {
const ChipGroupChainExample({super.key});
@override
State<ChipGroupChainExample> createState() => _ChipGroupChainExampleState();
}
class _ChipGroupChainExampleState extends State<ChipGroupChainExample> {
Set<String> _selected = const {'Flutter'};
@override
Component build(BuildContext context) {
return ChipGroup<String>(
items: const ['Flutter', 'React', 'Swift', 'Kotlin'],
initialValue: _selected,
selectionMode: CoreChipGroupSelectionMode.multi,
onChanged: (next) => setState(() => _selected = next),
chipBuilder: (item, {required selected, required enabled, required onTap}) => Chip(
label: item,
selected: selected,
enabled: enabled,
chipColor: CoreChipColor.primary,
onTap: onTap,
),
).withStyle(
const CoreChipGroupStyle(
itemSpacing: CoreSpace.space12,
rowSpacing: CoreSpace.space12,
titleGapStyle: CoreGapStyle(size: CoreSpace.space8),
titleTextStyle: CoreTextStyle.token(
CoreTextStyles.labelLarge,
color: CoreColor.token(CoreColors.onSurfaceVariant),
),
),
);
}
}
class ChipGroupChainExample extends StatefulWidget {
const ChipGroupChainExample({super.key});
@override
State<ChipGroupChainExample> createState() => _ChipGroupChainExampleState();
}
class _ChipGroupChainExampleState extends State<ChipGroupChainExample> {
Set<String> _selected = const {'Flutter'};
@override
Widget build(BuildContext context) {
return ChipGroup<String>(
items: const ['Flutter', 'React', 'Swift', 'Kotlin'],
initialValue: _selected,
selectionMode: CoreChipGroupSelectionMode.multi,
onChanged: (next) => setState(() => _selected = next),
chipBuilder: (item, {required selected, required enabled, required onTap}) => Chip(
label: item,
selected: selected,
enabled: enabled,
chipColor: CoreChipColor.primary,
onTap: onTap,
),
).withStyle(
const CoreChipGroupStyle(
itemSpacing: CoreSpace.space12,
rowSpacing: CoreSpace.space12,
titleGapStyle: CoreGapStyle(size: CoreSpace.space8),
titleTextStyle: CoreTextStyle.token(
CoreTextStyles.labelLarge,
color: CoreColor.token(CoreColors.onSurfaceVariant),
),
),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 여러 태그/카테고리 중 하나 또는 여러 개를 선택할 때
- 필터 옵션을 칩 형태로 제공할 때
- 가로 스크롤 가능한 태그 목록이 필요할 때
대신 다른 컴포넌트를 사용하세요:
RadioGroup: 라디오 버튼 형태의 단일 선택이 필요할 때Select: 드롭다운 형태의 선택이 필요할 때ChipInput: 사용자가 직접 텍스트를 입력하여 칩을 생성할 때
빠른 오버라이드 (Chain)#
이미 만든 ChipGroup 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class ChipGroupChainExample extends StatefulWidget {
const ChipGroupChainExample({super.key});
@override
State<ChipGroupChainExample> createState() => _ChipGroupChainExampleState();
}
class _ChipGroupChainExampleState extends State<ChipGroupChainExample> {
Set<String> _selected = const {'Flutter'};
@override
Widget build(BuildContext context) {
return ChipGroup<String>(
items: const ['Flutter', 'React', 'Swift', 'Kotlin'],
initialValue: _selected,
selectionMode: CoreChipGroupSelectionMode.multi,
onChanged: (next) => setState(() => _selected = next),
chipBuilder: (item, {required selected, required enabled, required onTap}) => Chip(
label: item,
selected: selected,
enabled: enabled,
chipColor: CoreChipColor.primary,
onTap: onTap,
),
).withStyle(
const CoreChipGroupStyle(
itemSpacing: CoreSpace.space12,
rowSpacing: CoreSpace.space12,
titleGapStyle: CoreGapStyle(size: CoreSpace.space8),
titleTextStyle: CoreTextStyle.token(
CoreTextStyles.labelLarge,
color: CoreColor.token(CoreColors.onSurfaceVariant),
),
),
);
}
}
class ChipGroupChainExample extends StatefulComponent {
const ChipGroupChainExample({super.key});
@override
State<ChipGroupChainExample> createState() => _ChipGroupChainExampleState();
}
class _ChipGroupChainExampleState extends State<ChipGroupChainExample> {
Set<String> _selected = const {'Flutter'};
@override
Component build(BuildContext context) {
return ChipGroup<String>(
items: const ['Flutter', 'React', 'Swift', 'Kotlin'],
initialValue: _selected,
selectionMode: CoreChipGroupSelectionMode.multi,
onChanged: (next) => setState(() => _selected = next),
chipBuilder: (item, {required selected, required enabled, required onTap}) => Chip(
label: item,
selected: selected,
enabled: enabled,
chipColor: CoreChipColor.primary,
onTap: onTap,
),
).withStyle(
const CoreChipGroupStyle(
itemSpacing: CoreSpace.space12,
rowSpacing: CoreSpace.space12,
titleGapStyle: CoreGapStyle(size: CoreSpace.space8),
titleTextStyle: CoreTextStyle.token(
CoreTextStyles.labelLarge,
color: CoreColor.token(CoreColors.onSurfaceVariant),
),
),
);
}
}
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
items | List<T> | 필수 | 표시할 아이템 목록 |
chipBuilder |
CoreChipGroupItemBuilder<T, W> |
필수 | 각 칩 위젯/컴포넌트 빌더 |
selectionMode |
CoreChipGroupSelectionMode? |
multi |
단일/다중 선택 모드 |
layout |
CoreChipGroupLayout? |
wrap |
wrap / scroll 레이아웃 |
initialValue |
Set<T>? |
null |
초기 선택 아이템 |
onChanged |
void Function(Set<T>)? |
null |
선택 변경 콜백 |
allowDeselect |
bool |
true |
single 모드에서 재탭 시 해제 허용 |
maxSelection |
int? |
null |
최대 선택 수 |
maxSelectionBehavior |
CoreChipGroupMaxBehavior? |
disableRest |
최대 도달 시 동작 |
minSelection |
int? |
null |
최소 선택 수 (해제 차단) |
title | W? | null | 헤더 타이틀 |
selectionHint |
W? |
null |
헤더 힌트 (예: 선택 개수 표시) |
showFade |
bool |
true |
scroll 모드에서 가장자리 페이드 표시 |
chipGroupStyle |
CoreChipGroupStyle? |
null |
인스턴스 스타일 슬롯 (itemSpacing/rowSpacing/titleGapStyle/fadeWidth/fadeColor + 타이틀/힌트 텍스트 스타일) |
CoreChipGroupStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
fadeWidth |
double? |
Edge fade width in scroll layout (logical px, pre-scaling). |
fadeColor |
CoreColor? |
Edge fade colour override (scroll layout). |
itemSpacing |
double? |
Horizontal gap between sibling chips (logical px, pre-scaling), forwarded to
Wrap.spacing
/
Row.spacing
(Flutter) and
column-gap
(Web). Native paint-API scalar — not a
Gap
.
|
rowSpacing |
double? |
Vertical gap between rows in wrap layout (logical px, pre-scaling), forwarded to
Wrap.runSpacing
(Flutter) /
row-gap
(Web).
|
titleGapStyle |
CoreGapStyle? |
Nested gap style for the spacer between title row and chips — forwarded to
Gap(gapStyle: …)
.
|
titleTextStyle |
CoreTextStyle? |
Title text style override (color + fontWeight + size + …). |
selectionHintTextStyle |
CoreTextStyle? |
Selection-hint text style override. |
예제#
ChipGroup<String>(
items: const ['Flutter', 'React', 'Swift', 'Kotlin'],
selectionMode: CoreChipGroupSelectionMode.multi,
onChanged: (next) => print(next),
chipBuilder:
(item, {required selected, required enabled, required onTap}) =>
Chip(
label: item,
selected: selected,
chipColor: CoreChipColor.primary,
onTap: onTap,
),
)
사용 가이드라인 (Usage Guidelines)#
✅ Do#
chipBuilder가 받은 selected/enabled/onTap을 칩에 그대로 연결
chipBuilder: (item, {required selected, required enabled, required onTap}) => Chip(
label: item,
selected: selected,
enabled: enabled,
onTap: onTap,
),
ChipGroup은 선택 규칙(단일/다중/allowDeselect/maxSelection)만 계산하고 실제 조작 표면은 chipBuilder가 돌려준 칩 전체입니다. 세 값 중 하나라도 연결하지 않으면 선택 상태 표시·비활성화·탭 핸들링이 그 칩에서 끊깁니다.
❌ Don't#
maxSelectionBehavior: .disableRest만 믿고 안내 없이 두지 않기
// ❌ 최대 선택 도달 시 칩이 조용히 비활성화 — 왜 안 눌리는지 안내 없음
ChipGroup<String>(
items: tags,
maxSelection: 3,
chipBuilder: buildChip,
)
// ✅ selectionHint로 제약을 텍스트로 함께 안내
ChipGroup<String>(
items: tags,
maxSelection: 3,
selectionHint: Text('최대 3개까지 선택 가능'),
chipBuilder: buildChip,
)
maxSelection에 도달하면 maxSelectionBehavior: .disableRest가 남은 칩을 조용히 비활성화할 뿐 왜 비활성화됐는지는 알리지 않습니다. selectionHint 슬롯으로 제약을 텍스트로 노출해야 사용자가 이유를 알 수 있습니다.
접근성 (Accessibility)#
역할 (Semantics)#
양 플랫폼 모두 role 을 emit 하지 않습니다. Flutter 는 Column / Row / Wrap /
Gap / FadeScroll 만 조립하고, Web 은 평범한 <div> 들에 caller attributes
만 통과시킵니다. role="group" 도, listbox / radiogroup 도, aria-multiselectable
도 없으며 title 에서 유도되는 접근 가능한 이름도 없습니다.
ChipGroup 은 선택 규칙을 관리하는 레이아웃 래퍼이고, 보조 기술이 만나는 조작 표면은 전부 chipBuilder 가 돌려준 칩입니다.
키보드#
컴포넌트가 소유한 키는 없습니다. Flutter 는 키 처리가 전혀 없고, Web 은 caller 의 onKeyDown / onKeyUp
을 이벤트 맵으로 전달만 할 뿐 스스로 처리하는 키가 없습니다. 실제 키 동작은 chipBuilder 가 돌려준 위젯의 것입니다 — 보통 Chip
이 Clickable 을 합성하므로 Enter / Space 가 거기서 옵니다.
포커스#
FocusNode · FocusTraversalGroup · tabindex · roving-tabindex 모델이 어느 쪽에도 없습니다. 그룹은 포커스 스코프도 아니고 자기가 포커스를 받지도 않으며, caller 가 만든 칩 하나하나가 독립된 탭 스톱입니다 — 칩이 N 개면 탭 스톱도 N 개이고 화살표 키로 건너뛰는 대안이 없습니다.
스크린 리더#
title 과 selectionHint 는 뒤따르는 칩들과 아무 프로그램적 관계가 없는 평범한 텍스트로 읽힙니다. 선택 상태는 caller 의 칩이 스스로 보고할 때만 보조 기술에 도달합니다 — 그룹은
selected: 를 chipBuilder 에 넘길 뿐 자기가 표시하는 것은 없습니다. 그룹 소속·아이템 개수·단일/다중 모드·min/max 규칙 중 안내되는 것은 하나도 없습니다.
maxSelection 에 도달하면 maxSelectionBehavior: .disableRest 가 남은 칩들을 조용히 비활성화하며, 왜 비활성화됐는지는 알리지 않습니다.
알려진 제약#
- group role 과 접근 가능한 이름이 없습니다 —
title슬롯은 시각 전용입니다. selectionMode가 multi 를 지원하는데도aria-multiselectable이 없습니다.-
minSelection/maxSelection/allowDeselect제약은 조용히 적용됩니다 — 거부된 탭은 어떤 형태의 피드백도 만들지 않습니다. -
.scroll레이아웃은FadeScroll로 감싸이며, 이는 포인터/스크롤 구동이라 키보드 스크롤 어포던스가 없고 페이드 너머에 내용이 더 있다는 표시도 없습니다. -
조작 표면 전체를 소비자가
chipBuilder로 공급하므로, 접근성 없는 칩을 넘기면 그룹 전체가 접근 불가능해지고 컴포넌트는 이를 막지 않습니다. 그룹에 이름과 role 이 필요하다면 Web 은attributespassthrough 로 넣고, Flutter 는 생성자에 해당 파라미터가 없으므로 바깥에서Semantics로 직접 감싸야 합니다.
모션 감소 · 고대비 · 최소 터치 타깃처럼 모든 컴포넌트에 공통으로 적용되는 축은 전역 접근성 축 에 정리되어 있습니다.