Filter | CoUI
LogoCoUI

Filter

상호 배타적인 옵션을 토글 버튼 그룹으로 묶는 분절형 필터 컨트롤

Filter#

여러 옵션을 라디오 그룹처럼 상호 배타적으로 선택하는 분절형 필터 컨트롤입니다. Flutter/Web 양쪽에서 동일한 API(Filter)를 제공합니다.

Filtercontrolled 컴포넌트입니다 — 선택값은 부모가 groupValue로 소유하고, onValueChanged 콜백으로 상태를 갱신합니다. 선택적으로 reset 버튼을 통해 선택을 해제할 수 있습니다.

Live Preview#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 한정된 옵션 중 하나를 빠르게 전환해야 할 때 (프레임워크, 정렬 기준, 카테고리 등)
  • 옵션이 항상 화면에 모두 노출되어야 할 때
  • 선택 해제(reset) 기능이 필요할 때

대신 다른 컴포넌트를 사용하세요:

  • RadioGroup: 폼 안에서 세로 정렬된 라디오 선택
  • Tabs: 콘텐츠 패널 전환이 함께 필요할 때
  • ChipGroup: 다중 선택이 필요할 때

기본 사용법 (Basic Usage)#

Filter<String>(
  groupValue: _selected,
  onValueChanged: (value) => setState(() => _selected = value),
  onReset: () => setState(() => _selected = null),
  showResetButton: true,
  items: const [
    CoreFilterItem(value: 'svelte', label: 'Svelte'),
    CoreFilterItem(value: 'vue', label: 'Vue'),
    CoreFilterItem(value: 'react', label: 'React'),
  ],
)
Filter<String>(
  groupValue: _selected,
  onValueChanged: (value) => setState(() => _selected = value),
  onReset: () => setState(() => _selected = null),
  showResetButton: true,
  items: const [
    CoreFilterItem(value: 'svelte', label: 'Svelte'),
    CoreFilterItem(value: 'vue', label: 'Vue'),
    CoreFilterItem(value: 'react', label: 'React'),
  ],
)

빠른 오버라이드 (Chain)#

이미 만든 Filter 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다. .radius16처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius16 == CoreRadius.radius16) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.

class FilterChainExample extends StatefulWidget {
  const FilterChainExample({super.key});

  @override
  State<FilterChainExample> createState() => _FilterChainExampleState();
}

class _FilterChainExampleState extends State<FilterChainExample> {
  String? _selected = 'svelte';

  void handleValueChanged(String value) {
    setState(() => _selected = value);
  }

  void handleReset() {
    setState(() => _selected = null);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        Filter<String>(
          groupValue: _selected,
          onValueChanged: handleValueChanged,
          onReset: handleReset,
          showResetButton: true,
          items: const [
            CoreFilterItem(value: 'svelte', label: 'Svelte'),
            CoreFilterItem(value: 'vue', label: 'Vue'),
            CoreFilterItem(value: 'react', label: 'React'),
          ],
        ).radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
        Filter<String>(
          groupValue: _selected,
          onValueChanged: handleValueChanged,
          onReset: handleReset,
          showResetButton: true,
          items: const [
            CoreFilterItem(value: 'svelte', label: 'Svelte'),
            CoreFilterItem(value: 'vue', label: 'Vue'),
            CoreFilterItem(value: 'react', label: 'React'),
          ],
        ).withStyle(
          const CoreFilterStyle(
            backgroundColor: CoreColor.token(CoreColors.tertiary),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            itemBorderRadius: CoreBorderRadius.all(CoreRadius.radius8),
            padding: CoreEdgeInsets.symmetric(
              horizontal: CoreSpace.space24,
              vertical: CoreSpace.space8,
            ),
          ),
        ),
      ],
    );
  }
}
class FilterChainExample extends StatefulComponent {
  const FilterChainExample({super.key});

  @override
  State<FilterChainExample> createState() => _FilterChainExampleState();
}

class _FilterChainExampleState extends State<FilterChainExample> {
  String? _selected = 'svelte';

  void handleValueChanged(String value) {
    setState(() => _selected = value);
  }

  void handleReset() {
    setState(() => _selected = null);
  }

  @override
  Component build(BuildContext context) {
    return div(
      [
        // 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
        Filter<String>(
          groupValue: _selected,
          onValueChanged: handleValueChanged,
          onReset: handleReset,
          showResetButton: true,
          items: const [
            CoreFilterItem(value: 'svelte', label: 'Svelte'),
            CoreFilterItem(value: 'vue', label: 'Vue'),
            CoreFilterItem(value: 'react', label: 'React'),
          ],
        ).radius16.primary,
        const Gap.space12(),
        // withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
        Filter<String>(
          groupValue: _selected,
          onValueChanged: handleValueChanged,
          onReset: handleReset,
          showResetButton: true,
          items: const [
            CoreFilterItem(value: 'svelte', label: 'Svelte'),
            CoreFilterItem(value: 'vue', label: 'Vue'),
            CoreFilterItem(value: 'react', label: 'React'),
          ],
        ).withStyle(
          const CoreFilterStyle(
            backgroundColor: CoreColor.token(CoreColors.tertiary),
            borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
            itemBorderRadius: CoreBorderRadius.all(CoreRadius.radius8),
            padding: CoreEdgeInsets.symmetric(
              horizontal: CoreSpace.space24,
              vertical: CoreSpace.space8,
            ),
          ),
        ),
      ],
      classes: 'flex flex-col items-start',
    );
  }
}

Props / Parameters#

Filter는 Flutter/Web에서 동일한 파라미터 이름을 사용합니다. 타입만 플랫폼별로 다릅니다.

속성Flutter 타입Web 타입기본값설명
variant CoreFilterVariant CoreFilterVariant .segmented 시각 변형
groupValue T? T? 현재 선택된 값
items List<CoreFilterItem<T, Widget>> List<CoreFilterItem<T, Component>> 필터 옵션 목록 (CoreFilterItem<T, W> — 아래 표 참조)
onValueChanged void Function(T) void Function(T) 항목 선택 콜백
onReset VoidCallback? CoreVoidCallback? null reset 버튼 콜백
showResetButton bool bool false reset 버튼 표시 여부
filterStyle CoreFilterStyle? CoreFilterStyle? null 인스턴스별 스타일 오버라이드

CoreFilterItem#

CoreFilterItem<T, W>는 세그먼트 하나를 기술합니다. W는 플랫폼 위젯 타입(Flutter Widget / Web Component)이며 items: const [CoreFilterItem(value: …, label: …)]처럼 인라인으로 쓰면 타입 추론이 채워 줍니다.

속성타입기본값설명
value T 이 세그먼트가 나타내는 값. groupValue==로 비교해 선택 여부를 정합니다.
label String 세그먼트 라벨. showLabel: false 여도 접근성 이름( aria-label / Semantics(label:) )으로 남습니다.
icon W? null 라벨 앞에 그리는 호출자 제공 아이콘. 선택 상태에 따라 iconStyle / selectedIconStyle 의 색·크기가 ambient IconTheme 으로 전달됩니다.
showLabel bool true 라벨 박스를 그릴지 여부. icon != null && showLabel == false 이면 아이콘 전용 세그먼트(8 + 16 + 8 = 32×32 정사각)가 됩니다.
Filter<String>(
  groupValue: _selected,
  onValueChanged: (value) => setState(() => _selected = value),
  items: [
    const CoreFilterItem(value: 'all', label: 'All'),
    CoreFilterItem(
      value: 'starred',
      label: 'Starred',
      icon: const Icon(LucideIcons.star),
      showLabel: false, // icon-only 32×32 segment
    ),
  ],
)

스타일 커스터마이징 (Style)#

모든 시각 chrome은 단일 filterStyle: CoreFilterStyle? 슬롯으로 들어갑니다.

Filter<String>(
  groupValue: _selected,
  onValueChanged: handleValueChanged,
  items: items,
  filterStyle: const CoreFilterStyle(
    backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
    selectedColor: CoreColor.token(CoreColors.surface),
    itemSpacing: CoreSpace.space4,
  ),
)

CoreFilterStyle 필드#

필드타입설명
backgroundColor CoreColor? Background fill of the filter container.
selectedColor CoreColor? Background fill of the currently selected filter item.
borderRadius CoreBorderRadius? Container corner radius.
itemBorderRadius CoreBorderRadius? Per-item corner radius.
padding CoreEdgeInsets? Padding inside the filter container.
itemPadding CoreEdgeInsets? Padding inside each filter item — the segment's own uniform inset around [icon][label box] . Read by both platform resolvers and the Figma builder.
labelPadding CoreEdgeInsets? Inset of the label box inside a segment (painted only when the item shows its label). Read by both platform resolvers and the Figma builder.
itemHeight double? Fixed segment height (logical px); content centres vertically. Read by both platform resolvers and the Figma builder.
itemSpacing double? Spacing between filter items (logical px). Maps to native Row.spacing on Flutter and CSS flex gap on Web.
resetButtonStyle CoreButtonStyle? Reset (clear-selection) button style override. The single entry point for the reset-affordance chrome — square side ( width / height ), padding, corner radius, hover transition ( animationDuration ), rest / hover foreground, and the close glyph ( leadingIconStyle ). Recursively merged onto [defaultResetButtonStyle] and raw-forwarded to Button(variant:.ghost, size:.sm, shape:.square, buttonStyle: …) (the ghost variant owns the transparent-fill / hover semantics).
selectedFontWeight int? Font weight applied to the currently selected item label (CSS font-weight integer).
transitionDuration Duration? Selection-state colour transition duration.
labelTextStyle CoreTextStyle? Label text style override applied to every unselected filter item label and the reset icon. Text colour is carried via [CoreTextStyle.color] inside this slot (sb8 — raw foregroundColor field removed).
selectedLabelTextStyle CoreTextStyle? Label text style override applied to the currently selected item label. Text colour is carried via [CoreTextStyle.color] inside this slot (sb8 — raw selectedForegroundColor field removed).
iconStyle CoreIconStyle? Nested [CoreIconStyle] slot applied to an unselected segment's caller-supplied icon (size + colour). Merged onto [defaultIconStyle] and raw-forwarded as the icon slot's ambient icon theme on both platforms.
selectedIconStyle CoreIconStyle? Nested [CoreIconStyle] slot applied to the selected segment's caller-supplied icon. Merged onto [defaultSelectedIconStyle] and raw-forwarded on both platforms.
clickableStyle CoreClickableStyle? Nested [CoreClickableStyle] slot for the composed per-item Clickable (press scale / durations / focus ring / disabled opacity). Merged on top of [defaultClickableStyle] and raw-forwarded — the Clickable's own resolver fills the rest.

테마 커스터마이징 (Theme)#

CoreFilterTheme으로 프로젝트 레벨 스타일 오버라이드가 가능합니다.

CoreComponentTheme(
  filter: CoreFilterTheme(
    style: CoreFilterStyle(
      backgroundColor: CoreColor.token(CoreColors.surfaceContainerHigh),
    ),
  ),
)

Resolve 우선순위: 디자인 시스템 기본값 → CoreFilterTheme.styleCoreFilterTheme.variantStyles[variant] → 위젯 filterStyle.

동작 스펙 (Behavior)#

시각#

  • 배경색이 있는 컨테이너 + 토글 버튼 형태의 항목
  • 선택된 항목은 배경색 + 굵은(semibold) 텍스트로 강조
  • 선택 전환 시 150ms 색상 transition

레이아웃#

  • 항목들은 가로로 배치되며 itemSpacing 간격 유지
  • 각 세그먼트는 높이 itemHeight(32) 고정, itemPadding(8 all) 안에 [아이콘] + [라벨 박스(labelPadding 가로 8)]를 세로 중앙 정렬로 배치 — 아이콘 전용 세그먼트는 32×32 정사각, 라벨 전용 세그먼트는 텍스트 폭 + 32
  • showResetButton: true이고 onReset != null이면 항목들 앞에 X 아이콘 reset 버튼 표시 — Button(variant: .ghost, size: .sm, shape: .square) (32×32)

사용 가이드라인 (Usage Guidelines)#

✅ Do#

선택 해제가 유효한 상태라면 showResetButtononReset을 함께 제공

Filter<String>(
  groupValue: _selected,
  onValueChanged: (v) => setState(() => _selected = v),
  onReset: () => setState(() => _selected = null),
  showResetButton: true,
  items: items,
)

리셋 아이콘 버튼은 showResetButtononReset이 모두 있어야만 렌더링됩니다 — 하나만 설정하면 리셋 기능이 화면에 나타나지 않습니다.


❌ Don't#

다중 선택에 Filter를 사용하지 않기

// ❌ Filter 는 controlled 단일 선택 전용 — 여러 값을 동시에 선택할 수 없음
Filter<String>(
  groupValue: _selected, // T? — 단일 값만 보유
  onValueChanged: (v) => setState(() => _selected = v),
  items: items,
)

groupValueT? 단일 값이라 라디오 그룹처럼 상호 배타적으로만 동작합니다. 다중 선택이 필요하면 ChipGroup을 사용하세요.

접근성 (Accessibility)#

  • 컨테이너는 role="radiogroup"
  • 각 항목은 role="radio" + aria-checked + aria-label (Web), Semantics(selected:, label:) (Flutter) — showLabel: false인 아이콘 전용 세그먼트도 label이 접근성 이름으로 유지됩니다
  • reset 버튼은 role="button" + aria-label="Reset filter"