Filter#
여러 옵션을 라디오 그룹처럼 상호 배타적으로 선택하는 분절형 필터 컨트롤입니다. Flutter/Web 양쪽에서 동일한 API(Filter)를 제공합니다.
Filter는 controlled 컴포넌트입니다 — 선택값은 부모가 groupValue로 소유하고, onValueChanged
콜백으로 상태를 갱신합니다. 선택적으로 reset 버튼을 통해 선택을 해제할 수 있습니다.
Live Preview#
class FilterDefaultExample extends StatefulComponent {
const FilterDefaultExample({super.key});
@override
State<FilterDefaultExample> createState() => _FilterDefaultExampleState();
}
class _FilterDefaultExampleState extends State<FilterDefaultExample> {
String? _selected = 'svelte';
void handleValueChanged(String value) {
setState(() => _selected = value);
}
void handleReset() {
setState(() => _selected = null);
}
@override
Component build(BuildContext context) {
return 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'),
],
);
}
}
class FilterDefaultExample extends StatefulWidget {
const FilterDefaultExample({super.key});
@override
State<FilterDefaultExample> createState() => _FilterDefaultExampleState();
}
class _FilterDefaultExampleState extends State<FilterDefaultExample> {
String? _selected = 'svelte';
void handleValueChanged(String value) {
setState(() => _selected = value);
}
void handleReset() {
setState(() => _selected = null);
}
@override
Widget build(BuildContext context) {
return 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'),
],
);
}
}
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',
);
}
}
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,
),
),
),
],
);
}
}
사용 시기 (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.style → CoreFilterTheme.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#
선택 해제가 유효한 상태라면 showResetButton과 onReset을 함께 제공
Filter<String>(
groupValue: _selected,
onValueChanged: (v) => setState(() => _selected = v),
onReset: () => setState(() => _selected = null),
showResetButton: true,
items: items,
)
리셋 아이콘 버튼은 showResetButton과 onReset이 모두 있어야만 렌더링됩니다 — 하나만 설정하면 리셋 기능이 화면에 나타나지 않습니다.
❌ Don't#
다중 선택에 Filter를 사용하지 않기
// ❌ Filter 는 controlled 단일 선택 전용 — 여러 값을 동시에 선택할 수 없음
Filter<String>(
groupValue: _selected, // T? — 단일 값만 보유
onValueChanged: (v) => setState(() => _selected = v),
items: items,
)
groupValue는 T? 단일 값이라 라디오 그룹처럼 상호 배타적으로만 동작합니다. 다중 선택이 필요하면 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"
관련 컴포넌트 (Related Components)#
- RadioGroup: 폼 안의 세로 라디오 선택
- Tabs: 콘텐츠 패널 전환
- ChipGroup: 다중 선택 칩 그룹