ChipGroup | CoUI
LogoCoUI

ChipGroup

선택 가능한 칩 그룹 컴포넌트

ChipGroup#

단일 또는 다중 선택을 지원하는 칩 그룹 컴포넌트입니다. Wrap/Scroll 레이아웃, 최대 선택 수 제한, 타이틀/힌트 헤더를 지원합니다. 칩 자체는 Chip 으로 렌더링되며 사용자는 chipBuilder 를 통해 다른 chip variant 도 사용할 수 있습니다.

Live Preview#

사용 시기 (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#

속성타입기본값설명
itemsList<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 최소 선택 수 (해제 차단)
titleW?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 가 돌려준 위젯의 것입니다 — 보통 ChipClickable 을 합성하므로 Enter / Space 가 거기서 옵니다.

포커스#

FocusNode · FocusTraversalGroup · tabindex · roving-tabindex 모델이 어느 쪽에도 없습니다. 그룹은 포커스 스코프도 아니고 자기가 포커스를 받지도 않으며, caller 가 만든 칩 하나하나가 독립된 탭 스톱입니다 — 칩이 N 개면 탭 스톱도 N 개이고 화살표 키로 건너뛰는 대안이 없습니다.

스크린 리더#

titleselectionHint 는 뒤따르는 칩들과 아무 프로그램적 관계가 없는 평범한 텍스트로 읽힙니다. 선택 상태는 caller 의 칩이 스스로 보고할 때만 보조 기술에 도달합니다 — 그룹은 selected:chipBuilder 에 넘길 뿐 자기가 표시하는 것은 없습니다. 그룹 소속·아이템 개수·단일/다중 모드·min/max 규칙 중 안내되는 것은 하나도 없습니다.

maxSelection 에 도달하면 maxSelectionBehavior: .disableRest 가 남은 칩들을 조용히 비활성화하며, 왜 비활성화됐는지는 알리지 않습니다.

알려진 제약#

  • group role 과 접근 가능한 이름이 없습니다 — title 슬롯은 시각 전용입니다.
  • selectionMode 가 multi 를 지원하는데도 aria-multiselectable 이 없습니다.
  • minSelection / maxSelection / allowDeselect 제약은 조용히 적용됩니다 — 거부된 탭은 어떤 형태의 피드백도 만들지 않습니다.
  • .scroll 레이아웃은 FadeScroll 로 감싸이며, 이는 포인터/스크롤 구동이라 키보드 스크롤 어포던스가 없고 페이드 너머에 내용이 더 있다는 표시도 없습니다.
  • 조작 표면 전체를 소비자가 chipBuilder 로 공급하므로, 접근성 없는 칩을 넘기면 그룹 전체가 접근 불가능해지고 컴포넌트는 이를 막지 않습니다. 그룹에 이름과 role 이 필요하다면 Web 은 attributes passthrough 로 넣고, Flutter 는 생성자에 해당 파라미터가 없으므로 바깥에서 Semantics 로 직접 감싸야 합니다.

모션 감소 · 고대비 · 최소 터치 타깃처럼 모든 컴포넌트에 공통으로 적용되는 축은 전역 접근성 축 에 정리되어 있습니다.

관련 컴포넌트#

  • Chip — 단일 칩 컴포넌트
  • ChipInput — 텍스트 입력으로 칩을 추가하는 입력 필드