ItemPicker#
검색 입력과 함께 문자열 아이템을 리스트 또는 그리드로 나열하여 단일 선택하는 인라인 피커입니다. 검색어를 입력하면 아이템이 대소문자 무시 contains
로 필터링되고, 클릭하면 선택됩니다.
Live Preview#
class ItemPickerDefaultExample extends StatefulComponent {
const ItemPickerDefaultExample({super.key});
@override
State<ItemPickerDefaultExample> createState() =>
_ItemPickerDefaultExampleState();
}
class _ItemPickerDefaultExampleState extends State<ItemPickerDefaultExample> {
String _selected = 'Apple';
@override
Component build(BuildContext context) {
return ItemPicker(
items: const ['Apple', 'Banana', 'Cherry', 'Mango', 'Orange'],
selectedValue: _selected,
title: 'Fruits',
onChanged: (value) => setState(() => _selected = value),
);
}
}
class ItemPickerDefaultExample extends StatefulWidget {
const ItemPickerDefaultExample({super.key});
@override
State<ItemPickerDefaultExample> createState() =>
_ItemPickerDefaultExampleState();
}
class _ItemPickerDefaultExampleState extends State<ItemPickerDefaultExample> {
String _selected = 'Apple';
@override
Widget build(BuildContext context) {
return ItemPicker(
items: const ['Apple', 'Banana', 'Cherry', 'Mango', 'Orange'],
selectedValue: _selected,
title: 'Fruits',
onChanged: (value) => setState(() => _selected = value),
);
}
}
class ItemPickerChainExample extends StatefulComponent {
const ItemPickerChainExample({super.key});
@override
State<ItemPickerChainExample> createState() => _ItemPickerChainExampleState();
}
class _ItemPickerChainExampleState extends State<ItemPickerChainExample> {
String _selected = 'Apple';
@override
Component build(BuildContext context) {
return div(
[
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
ItemPicker(
items: const ['Apple', 'Banana', 'Cherry', 'Mango', 'Orange'],
selectedValue: _selected,
title: 'Fruits',
onChanged: (value) => setState(() => _selected = value),
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(borderColor·borderWidth)까지 한 번에.
ItemPicker(
items: const ['Apple', 'Banana', 'Cherry', 'Mango', 'Orange'],
selectedValue: _selected,
title: 'Full control',
onChanged: (value) => setState(() => _selected = value),
).withStyle(
const CoreItemPickerStyle(
backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
borderColor: CoreColor.token(CoreColors.tertiary),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
),
),
],
classes: 'flex flex-col items-start',
);
}
}
class ItemPickerChainExample extends StatefulWidget {
const ItemPickerChainExample({super.key});
@override
State<ItemPickerChainExample> createState() => _ItemPickerChainExampleState();
}
class _ItemPickerChainExampleState extends State<ItemPickerChainExample> {
String _selected = 'Apple';
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
ItemPicker(
items: const ['Apple', 'Banana', 'Cherry', 'Mango', 'Orange'],
selectedValue: _selected,
title: 'Fruits',
onChanged: (value) => setState(() => _selected = value),
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(borderColor·borderWidth)까지 한 번에.
ItemPicker(
items: const ['Apple', 'Banana', 'Cherry', 'Mango', 'Orange'],
selectedValue: _selected,
title: 'Full control',
onChanged: (value) => setState(() => _selected = value),
).withStyle(
const CoreItemPickerStyle(
backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
borderColor: CoreColor.token(CoreColors.tertiary),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
),
),
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 미리 정의된 아이템 목록에서 검색하며 하나를 선택받는 경우
- 색상·아이콘·템플릿처럼 그리드로 보여줘야 하는 선택지
대신 다른 컴포넌트를 사용하세요:
Select: 드롭다운으로 공간을 절약해야 하는 경우Autocomplete: 자유 입력 + 제안이 필요한 경우
기본 사용법 (Basic Usage)#
// 검색 가능한 리스트 피커
ItemPicker(
items: const ['Apple', 'Banana', 'Cherry'],
selectedValue: selected,
onChanged: (value) => setState(() => selected = value),
)
// 그리드 레이아웃
ItemPicker(
items: items,
layout: CoreItemPickerLayout.grid,
gridColumns: 3,
title: 'Templates',
onChanged: (value) => pick(value),
)
// 검색 가능한 리스트 피커
ItemPicker(
items: const ['Apple', 'Banana', 'Cherry'],
selectedValue: selected,
onChanged: (value) => setState(() => selected = value),
)
// 그리드 레이아웃
ItemPicker(
items: items,
layout: CoreItemPickerLayout.grid,
gridColumns: 3,
title: 'Templates',
onChanged: (value) => pick(value),
)
빠른 오버라이드 (Chain)#
이미 만든 ItemPicker 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
.radius16처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius16 ==
CoreRadius.radius16) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.
class ItemPickerChainExample extends StatefulWidget {
const ItemPickerChainExample({super.key});
@override
State<ItemPickerChainExample> createState() => _ItemPickerChainExampleState();
}
class _ItemPickerChainExampleState extends State<ItemPickerChainExample> {
String _selected = 'Apple';
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
ItemPicker(
items: const ['Apple', 'Banana', 'Cherry', 'Mango', 'Orange'],
selectedValue: _selected,
title: 'Fruits',
onChanged: (value) => setState(() => _selected = value),
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(borderColor·borderWidth)까지 한 번에.
ItemPicker(
items: const ['Apple', 'Banana', 'Cherry', 'Mango', 'Orange'],
selectedValue: _selected,
title: 'Full control',
onChanged: (value) => setState(() => _selected = value),
).withStyle(
const CoreItemPickerStyle(
backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
borderColor: CoreColor.token(CoreColors.tertiary),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
),
),
],
);
}
}
class ItemPickerChainExample extends StatefulComponent {
const ItemPickerChainExample({super.key});
@override
State<ItemPickerChainExample> createState() => _ItemPickerChainExampleState();
}
class _ItemPickerChainExampleState extends State<ItemPickerChainExample> {
String _selected = 'Apple';
@override
Component build(BuildContext context) {
return div(
[
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
ItemPicker(
items: const ['Apple', 'Banana', 'Cherry', 'Mango', 'Orange'],
selectedValue: _selected,
title: 'Fruits',
onChanged: (value) => setState(() => _selected = value),
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(borderColor·borderWidth)까지 한 번에.
ItemPicker(
items: const ['Apple', 'Banana', 'Cherry', 'Mango', 'Orange'],
selectedValue: _selected,
title: 'Full control',
onChanged: (value) => setState(() => _selected = value),
).withStyle(
const CoreItemPickerStyle(
backgroundColor: CoreColor.token(CoreColors.surfaceContainer),
borderColor: CoreColor.token(CoreColors.tertiary),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
),
),
],
classes: 'flex flex-col items-start',
);
}
}
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
items |
List<String> |
[] |
선택 가능한 아이템 목록 |
selectedValue |
String? |
null |
현재 선택된 아이템 |
searchPlaceholder |
String? |
null |
검색 입력 placeholder |
showSearch |
bool |
true |
검색 입력 표시 여부 |
enabled | bool | true | 상호작용 가능 여부 |
layout |
CoreItemPickerLayout |
list |
레이아웃 (list / grid) |
gridColumns |
int |
4 |
그리드 레이아웃의 열 개수 |
title |
String? |
null |
아이템 위에 표시할 제목 |
emptyText |
String? |
null |
검색 결과 없을 때 표시 텍스트 |
onChanged |
ValueChanged<String>? |
null |
아이템 선택 콜백 |
itemPickerStyle |
CoreItemPickerStyle? |
null |
컨테이너 / 옵션 색상·테두리 오버라이드 |
스타일 시스템 (Style System)#
CoreItemPickerStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
backgroundColor |
CoreColor? |
Picker container background colour override. |
borderColor |
CoreColor? |
Picker container border colour override. |
borderRadius |
CoreBorderRadius? |
Picker container border radius override. |
borderWidth |
double? |
Picker container border width override (logical px). |
maxHeight |
double? |
Picker items scroll-area maximum height override (logical px). |
selectedColor |
CoreColor? |
Selected option background colour override. |
optionTextStyle |
CoreTextStyle? |
Option label text style override applied in the resting (neither selected nor hovered) state. Layers onto the [defaultOptionTextStyle] token role; the resting colour is carried via [CoreTextStyle.color] inside this slot. |
selectedTextStyle |
CoreTextStyle? |
Selected option text style override. Text colour is carried via [CoreTextStyle.color] inside this slot (sb8 — raw
selectedTextColor
field removed).
|
optionHoverColor |
CoreColor? |
Option hover background colour override. |
optionHoverTextStyle |
CoreTextStyle? |
Option text style override applied when the option is hovered. Text colour is carried via [CoreTextStyle.color] inside this slot (sb8 — raw
optionHoverTextColor
field removed).
|
titleTextStyle |
CoreTextStyle? |
Picker title text style override. Layers onto the [defaultTitleTextStyle] token role; the heading's font weight / colour are carried via [CoreTextStyle.fontWeight] / [CoreTextStyle.color] inside this slot (sb-text-style-repackage — raw
copyWith
removed).
|
emptyTextStyle |
CoreTextStyle? |
Empty-state text style override. Layers onto the [defaultEmptyTextStyle] token role; the placeholder's colour is carried via [CoreTextStyle.color] inside this slot (sb-text-style-repackage — raw
copyWith
removed).
|
titlePadding |
CoreEdgeInsets? |
Picker title padding override. |
itemsPadding |
CoreEdgeInsets? |
Picker items scroll-area padding override. |
optionPadding |
CoreEdgeInsets? |
Option row padding override. |
optionSpacing |
double? |
Inter-option spacing (logical px). Drives the grid layout
mainAxisSpacing
/
crossAxisSpacing
(Flutter
GridView.count
) /
gap
inline CSS (Web grid container) and the per-option row icon ↔ label gap.
null
defers to [defaultOptionSpacing].
|
optionBorderRadius |
CoreBorderRadius? |
Option row corner radius override. |
emptyPadding |
CoreEdgeInsets? |
Empty-state padding override. |
clickableStyle |
CoreClickableStyle? |
Nested [CoreClickableStyle] slot for the composed per-option
Clickable
(press scale / durations / focus ring / disabled opacity). Merged on top of [defaultClickableStyle] and raw-forwarded — the Clickable's own resolver fills the rest.
|
checkIconSize |
double? |
Selected-option check-glyph icon size (logical/CSS px). null → [defaultCheckIconSize]. |
searchHeight |
double? |
Search
<input>
row height (logical/CSS px).
null
→ [defaultSearchHeight].
|
동작 스펙 (Behavior)#
인터랙션#
- 검색: 검색어 입력 시 대소문자 무시
contains필터링 - 클릭: 아이템 클릭 시 선택, 선택 아이템은 체크 아이콘 표시
- 호버: 마우스 올린 아이템에 hover 색상 적용
enabled가false이면 비활성
사용 가이드라인 (Usage Guidelines)#
✅ Do#
항목이 적으면 검색 입력을 끄기
ItemPicker(
items: const ['Small', 'Medium', 'Large'],
showSearch: false,
selectedValue: selectedSize,
onChanged: (value) => setState(() => selectedSize = value),
)
이유: showSearch 의 기본값은 true 지만, 항목이 몇 개 안 되는 목록에서는 검색 입력이 불필요한 공간을 차지합니다. showSearch: false 로 끄면 리스트만 남아 더 간결해집니다.
❌ Don't#
목록에 없는 값을 입력받아야 하는 곳에 사용 금지
// ❌ 사전 정의된 items 목록에 없는 새 태그를 추가할 방법이 없음
ItemPicker(items: existingTags, onChanged: addTag)
이유: ItemPicker 는 items 로 주어진 고정 목록에서 대소문자 무시 contains 로 필터링하는 단일 선택 컴포넌트입니다. 사용자가 목록에 없는 값을 직접 입력해야 한다면 Autocomplete 를 사용해야 합니다.
접근성 (Accessibility)#
역할#
-
Web — 컨테이너
role="listbox"(비활성 시aria-disabled="true"), 각 아이템role="option"+aria-selected. - Flutter — role 없음. 계층은 위젯 구조로만 전달됩니다.
소유 관계가 끊겨 있습니다 — 어떤
option도listbox의 직계 자식이 아니라 role 없는 래퍼<div>안에 들어가며,aria-owns로 보정하지도 않습니다. 리더가 목록 구조를 올바로 읽지 못할 수 있습니다.
키보드#
| 키 | Flutter | Web |
|---|---|---|
Enter / Space | 포커스된 옵션 선택 | 포커스된 옵션 선택 |
Tab | 옵션마다 개별 tab stop | 옵션마다 개별 tab stop |
ArrowUp/ArrowDown |
옵션 간 포커스 이동 (아래 주의) | 없음 |
Home/End | 없음 | 없음 |
Flutter 의 화살표 이동은 이 컴포넌트가 아니라 프레임워크의 방향성 포커스 순회가 줍니다. 그래서 listbox 의미론이 없습니다 — 끝에서 순환하지 않고, picker 경계를 넘어 바깥 요소로도 넘어가며, 포커스가 선택을 따라가지도 않습니다.
스크린 리더#
-
Web — 컨테이너가 스스로 이름을 내보내지 않습니다. 호출자가
attributes: {'aria-label': ...}로 줄 수 있습니다. -
Flutter —
attributes에 해당하는 파라미터도Semantics래퍼도 없어 호출자 우회로가 없습니다. 검색 입력에는 텍스트 유무와 무관하게 접근 가능한 이름이 없습니다(placeholder 는 라벨이 아니라 형제 텍스트 노드입니다).
포커스 관리#
- 진입 / 이탈 — 인라인 컴포넌트라 열림·닫힘이 없습니다. 포커스를 옮기는 코드가 양쪽 다 없습니다.
- 트랩 — 없음.
Tab은 네이티브 순서대로 통과합니다.
알려진 제약#
-
옵션 수가 많으면 옵션마다 tab stop 이라
Tab만으로 목록을 지나가야 합니다 (Web 은 화살표 대안도 없습니다). -
Web 검색 입력은
type="search"라 브라우저가 자체 clear affordance 를 그리고, Chrome 에서는Escape가 값을 지웁니다 — 컴포넌트가 구현한 동작이 아니라 브라우저 기본 동작이며 Flutter 에는 없습니다. - Flutter 는 옵션의 선택 상태를 리더에 알리지 않습니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | ItemPicker | ItemPicker |
| 검색 입력 | TextField | Input(type: search) |
| 체크 아이콘 | Icon(LucideIcons.check) |
Icon(LucideIcons.check) |
관련 컴포넌트 (Related Components)#
- Select: 드롭다운 단일 선택
- Autocomplete: 자유 입력 + 제안