Select
Data Input
정해진 옵션 목록에서 값을 선택하는 셀렉트.
Usage
선택지가 미리 정해져 있을 때 라디오보다 선택지가 많아 공간을 줄여야 할 때
import
import
import { Select } from '@mildang/design-system/Select';타입
import type { SelectItem, SelectProps } from '@mildang/design-system/Select';Select 하나만 가져오면 Select.Root · Select.Control · Select.Trigger · Select.ValueText · Select.ChipSlot · Select.SubText · Select.Indicator · Select.ClearTrigger · Select.Positioner · Select.Content · Select.ItemGroup · Select.ItemGroupLabel · Select.Item · Select.ItemText · Select.ItemIndicator · Select.HiddenSelect · Select.Separator · Select.Option 를 그 아래에서 쓸 수 있다.
예제를 복사해 쓸 때 필요한 준비
• @mildang/icons 를 따로 설치한다. DS 패키지에 아이콘 컴포넌트가 포함되지 않는다.
• @mildang/styled-system 은 이 저장소에서 Panda 가 생성하는 산출물이다. 저장소 안에서는 turbo run ship 이후 쓸 수 있고, 패키지 소비자는 자기 Panda 산출물이나 다른 레이아웃 수단으로 바꿔야 한다.
Anatomy
tsx
import { Select } from '@mildang/design-system/Select';
export default function Example() {
return (
<Select>
<Select.Control>
<Select.Trigger>
<Select.ValueText />
</Select.Trigger>
<Select.ChipSlot />
<Select.SubText />
<Select.Indicator />
<Select.ClearTrigger />
</Select.Control>
<Select.Positioner>
<Select.Content>
<Select.ItemGroup>
<Select.ItemGroupLabel />
<Select.Item>
<Select.ItemText />
<Select.ItemIndicator />
</Select.Item>
</Select.ItemGroup>
<Select.Option />
<Select.Separator />
</Select.Content>
</Select.Positioner>
<Select.HiddenSelect />
</Select>
);
}부품
필수 여부
반복
위치
Select.Root
Select.Control
Select.Root 안에 둔다.
Select.Trigger
Select.Control 안에 둔다.
Select.ValueText
Select.Trigger 안에 둔다.
Select.ChipSlot
Select.Control 안에 둔다.
Select.SubText
Select.Control 안에 둔다.
Select.Indicator
Select.Control 안에 둔다.
Select.ClearTrigger
Select.Control 안에 둔다.
Select.Positioner
직접 compound 조립으로 Content를 배치할 때 필요하다.
Select.Root 안에서 Content를 감싼다.
Select.Content
옵션을 직접 조립하는 경우에만 렌더한다.
Select.Positioner 안에 둔다.
Select.ItemGroup
여러 개 가능
Select.Content 안에 둔다.
Select.ItemGroupLabel
ItemGroup에 그룹 이름을 표시할 때 사용한다.
Select.ItemGroup 안에 둔다.
Select.Item
여러 개 가능
Select.Content 또는 ItemGroup 안에 둔다.
Select.ItemText
Select.Item 안에 둔다.
Select.ItemIndicator
Select.Item 안에 둔다.
Select.Option
여러 개 가능
Select.Content 또는 ItemGroup 안에 옵션마다 둔다.
Select.Separator
여러 개 가능
Select.Content 안에서 그룹 사이에 둔다.
Select.HiddenSelect
Select.Root 안에 둔다.
- Select.Root에 collection을 연결하고 Select.Control 안에 Trigger를 배치한다.
- 옵션 목록은 Content 안에 Item 또는 Option을 반복해 구성한다.
- Item과 Option은 같은 옵션을 중복 렌더하지 않도록 한 가지 방식을 선택한다.
- 다중 선택값을 Chip으로 표시할 때는 ChipSlot 또는 renderValue와 함께 사용한다.
API Reference
Select Props
Prop
Type
Default
React.ReactNode
지정 안 함
React.RefObject<HTMLElement | null>
지정 안 함
string | number
지정 안 함
boolean
지정 안 함
boolean
지정 안 함
boolean
지정 안 함
(value: string) => void
지정 안 함
string
지정 안 함
(items: T[]) => React.ReactNode
지정 안 함
boolean
지정 안 함
string
지정 안 함
"sm" | "md" | "lg" | "xl"
md
React.ReactNode
지정 안 함
React.ReactNode
지정 안 함
boolean
지정 안 함
ValueFormatOptions
지정 안 함
boolean
지정 안 함
Select.Control
트리거와 선택값, 보조 액션을 감싸는 컨트롤 영역이다.
공개 Props 없음
Select.Trigger
옵션 목록을 열고 닫는 버튼이다.
공개 Props 없음
Select.ValueText
현재 선택값 또는 placeholder를 표시한다.
공개 Props 없음
Select.ChipSlot
다중 선택값을 Chip 콘텐츠로 표시하는 슬롯이다.
공개 Props 없음
Select.SubText
선택 컨트롤의 보조 문구를 표시한다.
공개 Props 없음
Select.Indicator
선택 컨트롤의 현재 상태나 펼침 표시를 담당한다.
공개 Props 없음
Select.ClearTrigger
현재 선택값을 지우는 액션 버튼이다.
공개 Props 없음
Select.Positioner
옵션 목록의 위치를 트리거 기준으로 배치한다.
Prop
Type
Default
React.RefObject<HTMLElement | null>
지정 안 함
Select.Content
열린 옵션 목록을 표시하는 표면이다.
공개 Props 없음
Select.ItemGroup
관련 옵션을 하나의 그룹으로 묶는다.
공개 Props 없음
Select.ItemGroupLabel
옵션 그룹의 이름을 표시한다.
공개 Props 없음
Select.Item
선택 가능한 옵션의 상태와 상호작용을 제공한다.
공개 Props 없음
Select.ItemText
옵션의 기본 텍스트를 표시한다.
공개 Props 없음
Select.ItemIndicator
옵션이 선택됐음을 표시한다.
공개 Props 없음
Select.Option
collection 항목을 간단히 렌더하는 옵션 편의 컴포넌트다.
Prop
Type
Default
Select.Separator
옵션 목록의 그룹 사이를 시각적으로 구분한다.
공개 Props 없음
Select.HiddenSelect
네이티브 폼 제출과 브라우저 접근성을 위한 숨은 select다.
공개 Props 없음
관련 타입
Select 와 Combobox 고르는 기준
Select 와 Combobox 는 둘 다 드롭다운 안에서 검색(필터)을 지원한다 — Select 는 searchable
prop, Combobox 는 트리거 자체가 입력 필드다. 그래서 검색이 되는지만으로는 둘을 못 가른다.
실제 구분선은 목록에 없는 값을 사용자가 직접 입력해 새 값으로 써야 하는가 다.
- Select — 언제나
collection안의 값만 고를 수 있다. 트리거는 버튼이라searchable을 켜도 검색은 드롭다운 안의 목록을 좁힐 뿐 값 자체를 바꾸지 않는다. - Combobox — 트리거 자체가 입력창이다.
allowCustomValue를 켜면 목록에 없는 텍스트도 그대로 값으로 쓸 수 있다.
옵션이 많아 스크롤만으로 고르기 어려우면 Combobox 로 바꾸지 말고 Select 의 searchable
을 켠다. 서브그룹 이상 깊은 계층은 group 이 한 단계뿐이라 다루지 못하니 트리·카테고리
컴포넌트로 분리한다.
옵션 콘텐츠 구성
SelectItem 은 Figma _menu_item 의 content_type 을 그대로 받는다.
| Figma content_type | SelectItem 필드 |
|---|---|
| text_only | label |
| icon_left | startAdornment (아이콘) |
| with_avatar | startAdornment (Avatar 컴포넌트) |
| with_avatar_caption | startAdornment + caption |
| bottom_description | description (기본, 라벨 아래) |
| right_description | description + descriptionPosition: 'right' |
| chip | Select.Option children 으로 Chip 컴포넌트 삽입 |
caption 이 배열이면 항목 사이에 divider(2px 원형)가 자동 삽입된다.
옵션이 많아지면 group 필드로 묶고 Select.ItemGroup/Select.ItemGroupLabel/
Select.Separator 로 그룹 헤더와 구분선을 그린다 — group 은 한 단계뿐이라 서브그룹은
표현하지 못한다.
옵션 상태는 두 갈래다.
selected— Ark Select 컨텍스트가 현재 선택값 기준으로 자동 반영한다.active— prop 으로 직접 켠다. 현재 컨텍스트(예: 현재 페이지, 현재 프로젝트)를 강조하는 지속 상태이고 CSS:active와는 무관하다.
콘텐츠 타입
description 옵션과 시작 아이콘을 함께 사용하고 선택된 항목의 보조 문구를 표시합니다.
import Calendar from '@mildang/icons/react/calendar';
import { Select, createSelectCollection, type SelectItem, type SelectProps } from '@mildang/design-system/Select';
import { useState } from 'react';
const defaultCollection = createSelectCollection<SelectItem>({
items: [
{ label: 'React', value: 'react', description: 'A' },
{ label: 'Vue', value: 'vue', description: 'B' },
{ label: 'Svelte', value: 'svelte', description: 'C' },
],
});
export default function SelectContentTypesExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection = defaultCollection, ...args } = props;
const items = collection.items;
const [value, setValue] = useState<string[]>([]);
const selectedItem = items.find((item) => item.value === value[0]);
return (
<Select
{...args}
collection={collection}
startAdornment={<Calendar />}
subText={selectedItem?.description}
placeholder="프레임워크를 선택하세요"
value={value}
onValueChange={(details) => setValue(details.value)}
>
{items.map((item) => <Select.Option key={item.value} item={item} />)}
</Select>
);
}
시작 아이콘
트리거 앞에 보조 아이콘을 표시합니다.
import Calendar from '@mildang/icons/react/calendar';
import { Select, createSelectCollection, type SelectItem, type SelectProps } from '@mildang/design-system/Select';
const defaultCollection = createSelectCollection<SelectItem>({
items: [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
{ label: 'Angular', value: 'angular' },
{ label: 'Solid', value: 'solid' },
],
});
export default function StartAdornmentExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection = defaultCollection, ...args } = props;
return (
<Select {...args} collection={collection} startAdornment={<Calendar />} placeholder="프레임워크를 선택하세요">
{collection.items.map((item) => <Select.Option key={item.value} item={item} />)}
</Select>
);
}
Chip 옵션
트리거와 옵션 목록을 Chip 콘텐츠로 표시합니다.
import { Chip } from '@mildang/design-system/Chip';
import {
Select,
createSelectCollection,
type SelectItem,
type SelectProps,
} from '@mildang/design-system/Select';
type ChipStatusItem = SelectItem & { color: 'green' | 'blue' | 'orange' | 'red' | 'brand' };
const defaultCollection = createSelectCollection<ChipStatusItem>({
items: [
{ label: '완료', value: 'done', color: 'green' },
{ label: '진행 중', value: 'in_progress', color: 'blue' },
{ label: '대기', value: 'pending', color: 'orange' },
{ label: '오류', value: 'error', color: 'red' },
{ label: '보류', value: 'pending2', color: 'brand' },
{ label: '취소', value: 'cancel', color: 'red' },
],
});
const STORY_DEFAULT_ARGS = { size: 'md' } satisfies Partial<SelectProps<SelectItem>>;
export default function ChipOptionsExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection: incomingCollection, ...args } = { ...STORY_DEFAULT_ARGS, ...props };
const collection = (incomingCollection ?? defaultCollection) as typeof defaultCollection;
return (
<Select
{...args}
collection={collection}
placeholder="상태를 선택하세요"
renderValue={(items) => {
const item = items[0] as ChipStatusItem | undefined;
if (!item) return null;
return <Chip label={item.label as string} color={item.color} type="text" size="sm" />;
}}
>
{collection.items.map((item) => (
<Select.Option key={item.value} item={item}>
<Chip label={item.label as string} color={item.color} type="text" size="sm" />
</Select.Option>
))}
</Select>
);
}
Chip과 텍스트 조합
Chip과 이름과 팀 정보를 한 옵션에 조합합니다.
import { Chip } from '@mildang/design-system/Chip';
import {
Select,
createSelectCollection,
type SelectItem,
type SelectProps,
} from '@mildang/design-system/Select';
import { css } from '@mildang/styled-system/css';
type UserItem = SelectItem & {
color: 'green' | 'blue' | 'orange' | 'red' | 'brand';
role: string;
team: string;
};
const defaultCollection = createSelectCollection<UserItem>({
items: [
{ label: '홍길동', value: 'user1', color: 'green', role: 'Manager', team: 'IHFB-개발팀' },
{ label: '김철수', value: 'user2', color: 'blue', role: 'Dev', team: 'IHFB-개발팀' },
{ label: '이영희', value: 'user3', color: 'orange', role: 'Designer', team: 'IHFB-디자인팀' },
{ label: '박민준', value: 'user4', color: 'red', role: 'Blocked', team: 'IHFB-기획팀' },
],
});
const STORY_DEFAULT_ARGS = { size: 'md' } satisfies Partial<SelectProps<SelectItem>>;
export default function ChipWithTextAndSubTextExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection: incomingCollection, ...args } = { ...STORY_DEFAULT_ARGS, ...props };
const collection = (incomingCollection ?? defaultCollection) as typeof defaultCollection;
return (
<Select
{...args}
collection={collection}
placeholder="담당자를 선택하세요"
renderValue={(items) => {
const item = items[0] as UserItem | undefined;
if (!item) return null;
return (
<>
<Chip
label={item.role}
color={item.color}
type="text"
size="sm"
className={css({ flexShrink: 0 })}
/>
<span className={css({ whiteSpace: 'nowrap', flexShrink: 0 })}>{item.label as string}</span>
<span
className={css({
color: 'neutral.text.lowest',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
minWidth: 0,
})}
>
{item.team}
</span>
</>
);
}}
>
{collection.items.map((item) => (
<Select.Option key={item.value} item={item}>
<div className={css({ display: 'flex', alignItems: 'center', gap: '8' })}>
<Chip label={item.role} color={item.color} type="text" size="sm" />
<span>{item.label as string}</span>
<span className={css({ color: 'neutral.text.lowest', marginLeft: 'auto' })}>{item.team}</span>
</div>
</Select.Option>
))}
</Select>
);
}
다중 선택 값을 Chip 으로 표시
multiple 모드에서 선택된 값을 트리거에 chip 으로 보여주려면 renderValue 에
renderChipValues 를 넘긴다. SelectItem 이 { label, color } 규약을 따르면 items 를 받아
Chip 리스트로 자동 변환한다.
Combobox 의 renderChipTag 와 대칭 API다 — Select multiple / Combobox multiple 어느
쪽이든 같은 { label, color } 규약만 지키면 동일하게 쓴다.
item 규약이 다르거나 커스텀 렌더가 필요하면 renderValue 에 직접 함수를 넘기면 된다.
드롭다운 안의 옵션 자체를 Chip 으로 보여주려면(Figma content_type=chip) Select.Option 의
children 으로 Chip 컴포넌트를 직접 넣는다.
값을 Chip 으로 표시
multiple + renderChipValues 로 트리거를 chip 리스트로, Select.Option children 으로 드롭다운 옵션을 chip 으로 렌더합니다.
import { Chip } from '@mildang/design-system/Chip';
import {
Select,
createSelectCollection,
renderChipValues,
type SelectItem,
type SelectProps,
} from '@mildang/design-system/Select';
type ChipStatusItem = SelectItem & { color: 'green' | 'blue' | 'orange' | 'red' | 'brand' };
const defaultCollection = createSelectCollection<ChipStatusItem>({
items: [
{ label: '완료', value: 'done', color: 'green' },
{ label: '진행 중', value: 'in_progress', color: 'blue' },
{ label: '대기', value: 'pending', color: 'orange' },
{ label: '오류', value: 'error', color: 'red' },
{ label: '보류', value: 'pending2', color: 'brand' },
{ label: '취소', value: 'cancel', color: 'red' },
],
});
const STORY_DEFAULT_ARGS = { size: 'md' } satisfies Partial<SelectProps<SelectItem>>;
export default function SelectChipValuesExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection: incomingCollection, ...args } = { ...STORY_DEFAULT_ARGS, ...props };
const collection = (incomingCollection ?? defaultCollection) as unknown as typeof defaultCollection;
return (
<Select
{...args}
multiple
collection={collection}
placeholder="상태를 선택하세요"
renderValue={renderChipValues}
>
{(collection.items as ChipStatusItem[]).map((item) => (
<Select.Option key={item.value} item={item}>
<Chip label={item.label as string} color={item.color} type="text" size="sm" />
</Select.Option>
))}
</Select>
);
}
검색 가능한 옵션
searchable을 켜면 드롭다운 상단에 검색 입력이 생긴다. onSearchChange로 검색값을 받아 필터링한 항목으로 collection과 Option 목록을 함께 갱신한다.\n\n필터링으로 collection이 재생성되므로 선택값은 controlled로 유지한다. 검색 결과가 없을 때는 빈 상태를 보여주고, 메뉴가 닫히면 검색값은 자동 초기화된다.
검색 가능한 옵션
검색어로 collection을 필터링하고 결과가 없을 때 빈 상태를 보여준다.
import { Select, createSelectCollection, SelectItem } from '@mildang/design-system/Select';
import { Text } from '@mildang/design-system/Text';
import { Grid, VStack } from '@mildang/styled-system/jsx';
import { useState } from 'react';
const items: SelectItem[] = [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
{ label: 'Angular', value: 'angular' },
{ label: 'Solid', value: 'solid' },
{ label: 'Preact', value: 'preact' },
{ label: 'Ember', value: 'ember' },
{ label: 'Alpine', value: 'alpine' },
{ label: 'Backbone', value: 'backbone' },
{ label: 'Marko', value: 'marko' },
];
export default function SelectSearchableExample() {
const [search, setSearch] = useState('');
const [value, setValue] = useState<string[]>([]);
const filteredItems = items.filter((item) => String(item.label).toLowerCase().includes(search.toLowerCase()));
const collection = createSelectCollection<SelectItem>({ items: filteredItems });
return (
<Grid gap="8px" style={{ width: 280 }}>
<Select collection={collection} value={value} onValueChange={(data) => setValue(data.value)} placeholder="프레임워크를 선택하세요" searchable searchPlaceholder="검색" onSearchChange={setSearch} fullWidth>
{filteredItems.length === 0 ? (
<VStack alignItems="center" gap="8" padding="16">
<Text>검색 결과 없음</Text>
</VStack>
) : (
filteredItems.map((item) => <Select.Option key={item.value} item={item} />)
)}
</Select>
</Grid>
);
}
다중 선택과 검색
multiple과 searchable을 함께 쓰면 메뉴를 닫지 않고 여러 항목을 연속으로 고를 수 있다. 선택 이벤트 뒤 검색어가 초기화되어 다음 검색을 바로 시작할 수 있다.\n\n다중 선택값은 value로 제어하고, 트리거에 Chip 목록이 필요하면 renderChipValues를 함께 사용한다.
다중 선택 + 검색
메뉴를 닫지 않고 검색과 다중 선택을 이어서 수행한다.
import { Select, createSelectCollection, SelectItem } from '@mildang/design-system/Select';
import { Text } from '@mildang/design-system/Text';
import { Grid, VStack } from '@mildang/styled-system/jsx';
import { useState } from 'react';
const items: SelectItem[] = [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
{ label: 'Angular', value: 'angular' },
{ label: 'Solid', value: 'solid' },
{ label: 'Preact', value: 'preact' },
{ label: 'Ember', value: 'ember' },
{ label: 'Alpine', value: 'alpine' },
{ label: 'Backbone', value: 'backbone' },
{ label: 'Marko', value: 'marko' },
];
export default function SelectSearchableMultipleExample() {
const [search, setSearch] = useState('');
const [value, setValue] = useState<string[]>([]);
const filteredItems = items.filter((item) => String(item.label).toLowerCase().includes(search.toLowerCase()));
const collection = createSelectCollection<SelectItem>({ items: filteredItems });
return (
<Grid gap="8px" style={{ width: 280 }}>
<Select multiple collection={collection} value={value} onValueChange={(data) => setValue(data.value)} placeholder="여러 개 선택하세요" searchable searchPlaceholder="검색" onSearchChange={setSearch} fullWidth>
{filteredItems.length === 0 ? (
<VStack alignItems="center" gap="8" padding="16">
<Text>검색 결과 없음</Text>
</VStack>
) : (
filteredItems.map((item) => <Select.Option key={item.value} item={item} />)
)}
</Select>
</Grid>
);
}
사용 가이드
권장
- placeholder로 사용자가 고를 값의 성격을 설명한다.
- 옵션 레이블은 서로 구분되게 작성한다.
지양
- 자유 입력이 필요한 값을 Select로 제한하지 않는다.
예제
기본 사용
createSelectCollection 으로 옵션을 만들고 하나를 고릅니다 — 옵션이 적어 검색이 필요 없을 때 씁니다.
import { Select, createSelectCollection, type SelectItem, type SelectProps } from '@mildang/design-system/Select';
const defaultCollection = createSelectCollection<SelectItem>({
items: [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
{ label: 'Angular', value: 'angular' },
{ label: 'Solid', value: 'solid' },
],
});
export default function SelectBasicExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection = defaultCollection, ...rest } = props;
return (
<Select {...rest} collection={collection} placeholder="프레임워크를 선택하세요">
{collection.items.map((item) => (
<Select.Option key={item.value} item={item} />
))}
</Select>
);
}
Sizes
sm·md·lg·xl 의 트리거 높이를 비교합니다. 같은 폼의 Input 과 같은 size 로 맞춥니다.
코드
import {
Select,
createSelectCollection,
renderChipValues,
type SelectItem,
type SelectProps,
} from '@mildang/design-system/Select';
import type { ChipVariantProps } from '@mildang/styled-system/recipes';
import { Chip } from '@mildang/design-system/Chip';
import { css } from '@mildang/styled-system/css';
import * as React from 'react';
const frameworkChipItems: Array<SelectItem & { color: NonNullable<ChipVariantProps['color']> }> = [
{ label: 'React', value: 'react', color: 'blue' },
{ label: 'Vue', value: 'vue', color: 'green' },
{ label: 'Svelte', value: 'svelte', color: 'orange' },
{ label: 'Angular', value: 'angular', color: 'red' },
{ label: 'Solid', value: 'solid', color: 'brand' },
];
const SIZE_ORDER = ['sm', 'md', 'lg', 'xl'] as const;
type Size = (typeof SIZE_ORDER)[number];
type ChipStatusItem = SelectItem & { color: NonNullable<ChipVariantProps['color']> };
type SizeMatrixVariant = { label: string; render: (size: Size) => React.ReactNode };
const SizeMatrixSection = ({ title, variants }: { title: string; variants: SizeMatrixVariant[] }) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<div className={css({ textStyle: 'title-xs', color: 'neutral.text.base' })}>{title}</div>
<div
style={{
display: 'grid',
gridTemplateColumns: `80px repeat(${variants.length}, 320px)`,
columnGap: '16px',
rowGap: '12px',
alignItems: 'center',
}}
>
<div />
{variants.map((variant) => (
<div
key={variant.label}
className={css({ textStyle: 'caption-lg-medium', color: 'neutral.text.low' })}
>
{variant.label}
</div>
))}
{SIZE_ORDER.map((size) => (
<React.Fragment key={size}>
<div className={css({ textStyle: 'caption-lg-medium', color: 'neutral.text.lowest' })}>
size={size}
</div>
{variants.map((variant) => (
<div key={`${size}-${variant.label}`}>{variant.render(size)}</div>
))}
</React.Fragment>
))}
</div>
</div>
);
const collection = createSelectCollection<SelectItem>({ items: frameworkChipItems });
const STORY_DEFAULT_ARGS = { size: 'md' } satisfies Partial<SelectProps<SelectItem>>;
export default function SelectSizesExample(props: Partial<SelectProps<SelectItem>> = {}) {
const args = { ...STORY_DEFAULT_ARGS, ...props };
const basicVariants: SizeMatrixVariant[] = [
{
label: 'Single',
render: (size) => (
<Select {...args} size={size} collection={collection} placeholder={`${size}`} fullWidth>
{collection.items.map((item) => (
<Select.Option key={item.value} item={item} />
))}
</Select>
),
},
{
label: 'Multiple',
render: (size) => (
<Select {...args} size={size} collection={collection} multiple placeholder={`${size}`} fullWidth>
{collection.items.map((item) => (
<Select.Option key={item.value} item={item} />
))}
</Select>
),
},
];
const chipVariants: SizeMatrixVariant[] = [
{
label: 'Chip · Single',
render: (size) => (
<Select
{...args}
size={size}
collection={collection}
placeholder={`${size}`}
fullWidth
renderValue={(items) => {
const item = items[0] as ChipStatusItem | undefined;
return item ? (
<Chip label={item.label as string} color={item.color} type="text" size="sm" />
) : null;
}}
>
{(collection.items as ChipStatusItem[]).map((item) => (
<Select.Option key={item.value} item={item}>
<Chip label={item.label as string} color={item.color} type="text" size="sm" />
</Select.Option>
))}
</Select>
),
},
{
label: 'Chip · Multiple',
render: (size) => (
<Select
{...args}
size={size}
collection={collection}
multiple
placeholder={`${size}`}
fullWidth
renderValue={renderChipValues}
>
{(collection.items as ChipStatusItem[]).map((item) => (
<Select.Option key={item.value} item={item}>
<Chip label={item.label as string} color={item.color} type="text" size="sm" />
</Select.Option>
))}
</Select>
),
},
];
return (
<div className={css({ display: 'flex', flexDirection: 'column', gap: '24' })}>
<SizeMatrixSection title="Basic" variants={basicVariants} />
<SizeMatrixSection title="Chip" variants={chipVariants} />
</div>
);
}
열린 상태
코드
import { Select, createSelectCollection, type SelectItem, type SelectProps } from '@mildang/design-system/Select';
const defaultCollection = createSelectCollection<SelectItem>({
items: [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
{ label: 'Angular', value: 'angular' },
{ label: 'Solid', value: 'solid' },
],
});
export default function SelectOpenExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection = defaultCollection, ...args } = props;
return (
<Select {...args} collection={collection} placeholder="프레임워크를 선택하세요" open>
{collection.items.map((item) => <Select.Option key={item.value} item={item} />)}
</Select>
);
}
상태
기본·오류·비활성을 나란히 둡니다. 미선택 검증 실패는 error 로 표시하고 사유는 Field 의 오류 문구로 적습니다.
import { Select, createSelectCollection, type SelectItem, type SelectProps } from '@mildang/design-system/Select';
import { Grid } from '@mildang/styled-system/jsx';
const defaultCollection = createSelectCollection<SelectItem>({
items: [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
{ label: 'Angular', value: 'angular' },
{ label: 'Solid', value: 'solid' },
],
});
export default function SelectStatesExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection = defaultCollection, ...args } = props;
return (
<Grid gap="8px">
<Select {...args} collection={collection} error placeholder="프레임워크를 선택하세요">
{collection.items.map((item) => <Select.Option key={item.value} item={item} />)}
</Select>
<Select {...args} collection={collection} warning placeholder="프레임워크를 선택하세요">
{collection.items.map((item) => <Select.Option key={item.value} item={item} />)}
</Select>
<Select {...args} collection={collection} success placeholder="프레임워크를 선택하세요">
{collection.items.map((item) => <Select.Option key={item.value} item={item} />)}
</Select>
</Grid>
);
}
다중 선택
여러 옵션을 연속으로 선택합니다.
import { Select, createSelectCollection, type SelectItem, type SelectProps } from '@mildang/design-system/Select';
const defaultCollection = createSelectCollection<SelectItem>({
items: [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
{ label: 'Angular', value: 'angular' },
{ label: 'Solid', value: 'solid' },
],
});
export default function MultipleExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection = defaultCollection, ...args } = props;
return (
<Select {...args} collection={collection} multiple placeholder="프레임워크를 선택하세요">
{collection.items.map((item) => <Select.Option key={item.value} item={item} />)}
</Select>
);
}
전체 너비
트리거를 부모 영역 전체 너비로 확장합니다.
import { Select, createSelectCollection, type SelectItem, type SelectProps } from '@mildang/design-system/Select';
const defaultCollection = createSelectCollection<SelectItem>({
items: [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
{ label: 'Angular', value: 'angular' },
{ label: 'Solid', value: 'solid' },
],
});
export default function FullWidthExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection = defaultCollection, ...args } = props;
return (
<Select {...args} collection={collection} fullWidth placeholder="프레임워크를 선택하세요">
{collection.items.map((item) => <Select.Option key={item.value} item={item} />)}
</Select>
);
}
지우기 버튼 숨김
선택값을 지우는 버튼을 숨깁니다.
import { Select, createSelectCollection, type SelectItem, type SelectProps } from '@mildang/design-system/Select';
const defaultCollection = createSelectCollection<SelectItem>({
items: [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
{ label: 'Angular', value: 'angular' },
{ label: 'Solid', value: 'solid' },
],
});
export default function HideClearButtonExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection = defaultCollection, ...args } = props;
return (
<Select {...args} collection={collection}>
{collection.items.map((item) => <Select.Option key={item.value} item={item} />)}
</Select>
);
}
선택값 표시
열린 목록에서 현재 선택값을 강조합니다.
import { Select, createSelectCollection, type SelectItem, type SelectProps } from '@mildang/design-system/Select';
const defaultCollection = createSelectCollection<SelectItem>({
items: [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
{ label: 'Angular', value: 'angular' },
{ label: 'Solid', value: 'solid' },
],
});
export default function SelectedValueExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection = defaultCollection, ...args } = props;
return (
<Select {...args} collection={collection} placeholder="프레임워크를 선택하세요" value={['react']} open>
{collection.items.map((item) => <Select.Option key={item.value} item={item} />)}
</Select>
);
}
긴 옵션 라벨
긴 선택값과 옵션 목록의 표시를 확인합니다.
코드
import { Select, createSelectCollection, type SelectItem } from '@mildang/design-system/Select';
const longLabelCollection = createSelectCollection<SelectItem>({
items: [
{ label: '짧은 옵션', value: 'short' },
{ label: '아주 길고 긴 옵션 라벨 텍스트 예시입니다 1234567890 abcdefg', value: 'long' },
{ label: '중간 길이 옵션 라벨', value: 'medium' },
],
});
export default function LongLabelExample() {
return (
<div style={{ width: 240 }}>
<Select collection={longLabelCollection} placeholder="선택" fullWidth defaultValue={['long']} defaultOpen>
{longLabelCollection.items.map((item) => <Select.Option key={item.value} item={item} />)}
</Select>
</div>
);
}
비활성 옵션
선택할 수 없는 옵션을 목록에 표시합니다.
import { Select, createSelectCollection, type SelectItem, type SelectProps } from '@mildang/design-system/Select';
const defaultCollection = createSelectCollection<SelectItem>({
items: [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
{ label: 'Angular', value: 'angular' },
{ label: 'Solid', value: 'solid', disabled: true },
],
});
export default function DisabledOptionsExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection = defaultCollection, ...args } = props;
return (
<Select {...args} collection={collection} open placeholder="프레임워크를 선택하세요">
{collection.items.map((item) => <Select.Option key={item.value} item={item} />)}
</Select>
);
}
호버 강조
특정 옵션을 강조된 상태로 표시합니다.
import { Select, createSelectCollection, type SelectItem, type SelectProps } from '@mildang/design-system/Select';
const defaultCollection = createSelectCollection<SelectItem>({
items: [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
{ label: 'Angular', value: 'angular' },
{ label: 'Solid', value: 'solid' },
],
});
export default function HoverHighlightExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection = defaultCollection, ...args } = props;
return (
<Select {...args} collection={collection} placeholder="프레임워크를 선택하세요" open>
{collection.items.map((item) => (
<Select.Option key={item.value} item={item} data-highlighted={item.value === 'react' || undefined} />
))}
</Select>
);
}
키보드 강조
키보드 탐색으로 옵션을 강조합니다.
import { Select, createSelectCollection, type SelectItem, type SelectProps } from '@mildang/design-system/Select';
const defaultCollection = createSelectCollection<SelectItem>({
items: [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Svelte', value: 'svelte' },
{ label: 'Angular', value: 'angular' },
{ label: 'Solid', value: 'solid' },
],
});
export default function KeyboardHighlightExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection = defaultCollection, ...args } = props;
return (
<Select {...args} collection={collection} placeholder="프레임워크를 선택하세요">
{collection.items.map((item) => <Select.Option key={item.value} item={item} />)}
</Select>
);
}
목록 최대 높이
긴 옵션 목록을 제한된 높이로 스크롤합니다.
import { Select, createSelectCollection, type SelectItem, type SelectProps } from '@mildang/design-system/Select';
const defaultCollection = createSelectCollection<SelectItem>({
items: Array.from({ length: 16 }, (_, index) => ({ label: `옵션 ${index + 1}`, value: `option-${index + 1}` })),
});
export default function ContentMaxHeightExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection = defaultCollection, ...args } = props;
return (
<Select {...args} collection={collection} placeholder="스크롤 가능한 옵션 목록">
{collection.items.map((item) => <Select.Option key={item.value} item={item} />)}
</Select>
);
}
옵션 그룹
관련 옵션을 그룹과 구분선으로 나눕니다.
import CheckIcon from '@mildang/icons/react/check';
import {
Select,
createSelectCollection,
type SelectItem,
type SelectProps,
} from '@mildang/design-system/Select';
const defaultCollection = createSelectCollection<SelectItem & { group: string }>({
items: [
{ label: 'React', value: 'react', group: 'Frontend' },
{ label: 'Vue', value: 'vue', group: 'Frontend' },
{ label: 'Angular', value: 'angular', group: 'Frontend' },
{ label: 'Node', value: 'node', group: 'Backend' },
{ label: 'Nest', value: 'nest', group: 'Backend' },
{ label: 'Express', value: 'express', group: 'Backend' },
],
groupBy: (item) => item.group,
});
const STORY_DEFAULT_ARGS = {
size: 'md',
placeholder: '기술 스택 선택',
} satisfies Partial<SelectProps<SelectItem>>;
export default function GroupedExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection: incomingCollection, ...args } = { ...STORY_DEFAULT_ARGS, ...props };
const collection = (incomingCollection ?? defaultCollection) as typeof defaultCollection;
return (
<Select {...args} collection={collection}>
<Select.ItemGroup>
<Select.ItemGroupLabel>Frontend</Select.ItemGroupLabel>
{collection.items
.filter((item) => item.group === 'Frontend')
.map((item) => (
<Select.Item key={item.value} item={item}>
<Select.ItemText>{item.label}</Select.ItemText>
<Select.ItemIndicator>
<CheckIcon />
</Select.ItemIndicator>
</Select.Item>
))}
</Select.ItemGroup>
<Select.Separator />
<Select.ItemGroup>
<Select.ItemGroupLabel>Backend</Select.ItemGroupLabel>
{collection.items
.filter((item) => item.group === 'Backend')
.map((item) => (
<Select.Item key={item.value} item={item}>
<Select.ItemText>{item.label}</Select.ItemText>
<Select.ItemIndicator>
<CheckIcon />
</Select.ItemIndicator>
</Select.Item>
))}
</Select.ItemGroup>
</Select>
);
}
그룹 선택 개수 표시
여러 선택값을 그룹 기준 개수로 요약합니다.
import {
Select,
createSelectCollection,
type SelectItem,
type SelectProps,
} from '@mildang/design-system/Select';
const defaultCollection = createSelectCollection<SelectItem & { group: string }>({
items: [
{ label: 'React', value: 'react', group: 'Frontend' },
{ label: 'Vue', value: 'vue', group: 'Frontend' },
{ label: 'Angular', value: 'angular', group: 'Frontend' },
{ label: 'Node', value: 'node', group: 'Backend' },
{ label: 'Nest', value: 'nest', group: 'Backend' },
{ label: 'Express', value: 'express', group: 'Backend' },
],
groupBy: (item) => item.group,
});
const STORY_DEFAULT_ARGS = {
size: 'md',
multiple: true,
placeholder: '기술 스택 선택',
} satisfies Partial<SelectProps<SelectItem>>;
export default function ValueFormatCountExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection: incomingCollection, ...args } = { ...STORY_DEFAULT_ARGS, ...props };
const collection = (incomingCollection ?? defaultCollection) as typeof defaultCollection;
return (
<Select
{...args}
collection={collection}
value={['react', 'vue', 'node']}
valueFormat={{ summaryMode: 'count', formatCount: (label, count) => `${label} 외 ${count}개` }}
>
{collection.group().map(([groupName, items], index, groups) => (
<Select.ItemGroup key={groupName}>
<Select.ItemGroupLabel>{groupName}</Select.ItemGroupLabel>
{items.map((item) => (
<Select.Option key={item.value} item={item} />
))}
{index < groups.length - 1 && <Select.Separator />}
</Select.ItemGroup>
))}
</Select>
);
}
보조 문구
트리거 아래에 보조 문구를 표시합니다.
import Calendar from '@mildang/icons/react/calendar';
import { Select, createSelectCollection, type SelectItem, type SelectProps } from '@mildang/design-system/Select';
const defaultCollection = createSelectCollection<SelectItem>({
items: [
{ label: '2025-03-01', value: '2025-03-01' },
{ label: '2025-03-15', value: '2025-03-15' },
{ label: '2025-04-01', value: '2025-04-01' },
],
});
export default function WithSubTextExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection = defaultCollection, ...args } = props;
return (
<Select {...args} collection={collection} startAdornment={<Calendar />} subText="Sub-Text" placeholder="날짜를 선택하세요">
{collection.items.map((item) => <Select.Option key={item.value} item={item} />)}
</Select>
);
}
그룹 포함 선택값
선택값 요약에 그룹 이름을 포함합니다.
import {
Select,
createSelectCollection,
type SelectItem,
type SelectProps,
} from '@mildang/design-system/Select';
const defaultCollection = createSelectCollection<SelectItem & { group: string }>({
items: [
{ label: 'React', value: 'react', group: 'Frontend' },
{ label: 'Vue', value: 'vue', group: 'Frontend' },
{ label: 'Angular', value: 'angular', group: 'Frontend' },
{ label: 'Node', value: 'node', group: 'Backend' },
{ label: 'Nest', value: 'nest', group: 'Backend' },
{ label: 'Express', value: 'express', group: 'Backend' },
],
groupBy: (item) => item.group,
});
const STORY_DEFAULT_ARGS = {
size: 'md',
multiple: true,
placeholder: '기술 스택 선택',
} satisfies Partial<SelectProps<SelectItem>>;
export default function ValueFormatWithGroupExample(props: Partial<SelectProps<SelectItem>> = {}) {
const { collection: incomingCollection, ...args } = { ...STORY_DEFAULT_ARGS, ...props };
const collection = (incomingCollection ?? defaultCollection) as typeof defaultCollection;
return (
<Select
{...args}
collection={collection}
value={['react', 'vue', 'node']}
valueFormat={{
summaryMode: 'count',
includeGroupInLabel: true,
formatCount: (label, count) => `${label} 외 ${count}개`,
}}
>
{collection.group().map(([groupName, items], index, groups) => (
<Select.ItemGroup key={groupName}>
<Select.ItemGroupLabel>{groupName}</Select.ItemGroupLabel>
{items.map((item) => (
<Select.Option key={item.value} item={item} />
))}
{index < groups.length - 1 && <Select.Separator />}
</Select.ItemGroup>
))}
</Select>
);
}
검색과 Chip 조합
검색 가능한 다중 선택값을 Chip으로 표시합니다.
import Search from '@mildang/icons/react/search';
import { Select, createSelectCollection, renderChipValues, type SelectItem } from '@mildang/design-system/Select';
import { Chip } from '@mildang/design-system/Chip';
import { Grid } from '@mildang/styled-system/jsx';
import { useMemo, useState } from 'react';
type ChipStatusItem = SelectItem & { color: 'green' | 'blue' | 'orange' | 'red' | 'brand' };
const items: ChipStatusItem[] = [
{ label: '완료', value: 'done', color: 'green' },
{ label: '진행 중', value: 'in_progress', color: 'blue' },
{ label: '대기', value: 'pending', color: 'orange' },
{ label: '오류', value: 'error', color: 'red' },
{ label: '보류', value: 'pending2', color: 'brand' },
{ label: '취소', value: 'cancel', color: 'red' },
];
export default function SearchableWithChipExample() {
const [search, setSearch] = useState('');
const [value, setValue] = useState<string[]>([]);
const filteredItems = useMemo(
() => items.filter((item) => String(item.label).toLowerCase().includes(search.toLowerCase())),
[search],
);
const collection = useMemo(() => createSelectCollection<SelectItem>({ items: filteredItems }), [filteredItems]);
return (
<Grid gap="8px" style={{ width: 280 }}>
<Select
multiple
collection={collection}
value={value}
onValueChange={(details) => setValue(details.value)}
placeholder="상태를 선택하세요"
renderValue={renderChipValues}
searchable
searchPlaceholder="검색"
onSearchChange={setSearch}
fullWidth
>
{filteredItems.length === 0 ? (
<div><Search />검색 결과 없음</div>
) : (
filteredItems.map((item) => (
<Select.Option key={item.value} item={item}>
<Chip label={item.label as string} color={item.color} type="text" size="sm" />
</Select.Option>
))
)}
</Select>
</Grid>
);
}