MultiSelect
MultiSelect는 사용자가 여러 옵션 중에서 다중 선택할 수 있는 드롭다운 컴포넌트입니다. 선택된 값들은 배지 형태로 표시되며, 태그 선택, 필터링, 카테고리 선택 등에 사용됩니다.import { MultiSelect } from '@vapor-ui/core';
const fonts = [
{ label: 'Sans-serif', value: 'sans' },
{ label: 'Serif', value: 'serif' },
{ label: 'Monospace', value: 'mono' },
{ label: 'Cursive', value: 'cursive' },
];
export default function DefaultMultiSelect() {
return (
<MultiSelect.Root items={fonts} placeholder="폰트를 선택하세요">
<MultiSelect.Trigger>
<MultiSelect.Value />
<MultiSelect.TriggerIcon />
</MultiSelect.Trigger>
<MultiSelect.Content>
{fonts.map((font) => (
<MultiSelect.Item key={font.value} value={font.value}>
{font.label}
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
))}
</MultiSelect.Content>
</MultiSelect.Root>
);
}
Property
Size
MultiSelect의 크기는 sm, md, lg, xl 로 제공합니다. 크기에 따라 선택된 값들의 배지 크기도 자동으로 조정됩니다.
import { Flex, MultiSelect } from '@vapor-ui/core';
const options = [
{ label: '옵션 1', value: 'option1' },
{ label: '옵션 2', value: 'option2' },
{ label: '옵션 3', value: 'option3' },
];
export default function MultiSelectSize() {
return (
<Flex gap="$200" className="flex-wrap" width="400px">
<MultiSelect.Root placeholder="Small" size="sm" items={options}>
<MultiSelect.Trigger>
<MultiSelect.Value />
<MultiSelect.TriggerIcon />
</MultiSelect.Trigger>
<MultiSelect.Content>
{options.map((option) => (
<MultiSelect.Item key={option.value} value={option.value}>
{option.label}
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
))}
</MultiSelect.Content>
</MultiSelect.Root>
<MultiSelect.Root placeholder="Medium" size="md" items={options}>
<MultiSelect.Trigger>
<MultiSelect.Value />
<MultiSelect.TriggerIcon />
</MultiSelect.Trigger>
<MultiSelect.Content>
{options.map((option) => (
<MultiSelect.Item key={option.value} value={option.value}>
{option.label}
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
))}
</MultiSelect.Content>
</MultiSelect.Root>
<MultiSelect.Root placeholder="Large" size="lg" items={options}>
<MultiSelect.Trigger>
<MultiSelect.Value />
<MultiSelect.TriggerIcon />
</MultiSelect.Trigger>
<MultiSelect.Content>
{options.map((option) => (
<MultiSelect.Item key={option.value} value={option.value}>
{option.label}
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
))}
</MultiSelect.Content>
</MultiSelect.Root>
<MultiSelect.Root placeholder="Extra Large" size="xl" items={options}>
<MultiSelect.Trigger>
<MultiSelect.Value />
<MultiSelect.TriggerIcon />
</MultiSelect.Trigger>
<MultiSelect.Content>
{options.map((option) => (
<MultiSelect.Item key={option.value} value={option.value}>
{option.label}
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
))}
</MultiSelect.Content>
</MultiSelect.Root>
</Flex>
);
}
Controlled State
MultiSelect의 선택 상태를 외부에서 제어할 수 있습니다. 값은 배열 형태로 관리됩니다.
'use client';
import { useState } from 'react';
import { Button, HStack, MultiSelect, Text, VStack } from '@vapor-ui/core';
const fonts = [
{ label: 'Sans-serif', value: 'sans' },
{ label: 'Serif', value: 'serif' },
{ label: 'Monospace', value: 'mono' },
{ label: 'Cursive', value: 'cursive' },
];
export default function MultiSelectControlled() {
const [value, setValue] = useState<string[]>([]);
const handleValueChange = (newValue: unknown) => {
setValue(newValue as string[]);
};
return (
<VStack gap="$200" width="400px">
<MultiSelect.Root
items={fonts}
value={value}
onValueChange={handleValueChange}
placeholder="폰트 선택"
>
<MultiSelect.Trigger>
<MultiSelect.Value />
<MultiSelect.TriggerIcon />
</MultiSelect.Trigger>
<MultiSelect.Content>
{fonts.map((font) => (
<MultiSelect.Item key={font.value} value={font.value}>
{font.label}
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
))}
</MultiSelect.Content>
</MultiSelect.Root>
<Text typography="body2" foreground="secondary-200">
선택된 값:{' '}
<code className="bg-gray-100 px-1 rounded">
{value.length > 0 ? value.join(', ') : '없음'}
</code>
</Text>
<HStack gap="$100">
<Button color="primary" onClick={() => setValue(['serif', 'mono'])}>
Serif, Mono 선택
</Button>
<Button color="secondary" onClick={() => setValue([])}>
모두 해제
</Button>
</HStack>
</VStack>
);
}
States
MultiSelect의 다양한 상태(비활성화, 읽기 전용, 오류)를 설정할 수 있습니다.
import type { MultiSelectRootProps } from '@vapor-ui/core';
import { MultiSelect, VStack } from '@vapor-ui/core';
const options = [
{ label: '옵션 1', value: 'option1' },
{ label: '옵션 2', value: 'option2' },
{ label: '옵션 3', value: 'option3' },
];
export default function MultiSelectStates() {
return (
<VStack gap="$200" width="400px">
<MultiSelectTemplate placeholder="기본 상태" />
<MultiSelectTemplate placeholder="비활성화" disabled />
<MultiSelectTemplate placeholder="읽기 전용" readOnly />
<MultiSelectTemplate placeholder="오류 상태" invalid />
</VStack>
);
}
export const MultiSelectTemplate = (props: MultiSelectRootProps<string>) => {
return (
<MultiSelect.Root {...props}>
<MultiSelect.Trigger>
<MultiSelect.Value />
<MultiSelect.TriggerIcon />
</MultiSelect.Trigger>
<MultiSelect.Content>
{options.map((option) => (
<MultiSelect.Item key={option.value} value={option.value}>
{option.label}
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
))}
</MultiSelect.Content>
</MultiSelect.Root>
);
};
Examples
Items Configuration
MultiSelect는 배열 형태와 객체 형태의 아이템 데이터를 모두 지원합니다. items prop을 사용하면 자동으로 값이 포맷팅되어 배지로 표시됩니다.
import { HStack, MultiSelect, Text, VStack } from '@vapor-ui/core';
const fonts = [
{ label: 'Sans-serif', value: 'sans' },
{ label: 'Serif', value: 'serif' },
{ label: 'Monospace', value: 'mono' },
{ label: 'Cursive', value: 'cursive' },
];
const languages = {
javascript: 'JavaScript',
typescript: 'TypeScript',
python: 'Python',
java: 'Java',
go: 'Go',
};
export default function MultiSelectItems() {
return (
<HStack gap="$500">
<VStack gap="$100" width="300px">
<MultiSelect.Root placeholder="폰트 선택" items={fonts}>
<Text typography="body2">배열 형태의 아이템</Text>
<MultiSelect.Trigger>
<MultiSelect.Value />
<MultiSelect.TriggerIcon />
</MultiSelect.Trigger>
<MultiSelect.Content>
{fonts.map((font) => (
<MultiSelect.Item key={font.value} value={font.value}>
{font.label}
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
))}
</MultiSelect.Content>
</MultiSelect.Root>
</VStack>
<VStack gap="$100" width="300px">
<MultiSelect.Root placeholder="언어 선택" items={languages}>
<Text typography="body2">객체 형태의 아이템</Text>
<MultiSelect.Trigger>
<MultiSelect.Value />
<MultiSelect.TriggerIcon />
</MultiSelect.Trigger>
<MultiSelect.Content>
{Object.entries(languages).map(([value, label]) => (
<MultiSelect.Item key={value} value={value}>
{label}
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
))}
</MultiSelect.Content>
</MultiSelect.Root>
</VStack>
</HStack>
);
}
Grouping Options
관련된 옵션들을 그룹으로 묶어 구조화할 수 있습니다. Group과 GroupLabel, Separator를 사용하여 명확한 구조를 만들 수 있습니다.
import { Box, MultiSelect } from '@vapor-ui/core';
export default function MultiSelectGrouping() {
return (
<MultiSelect.Root placeholder="개발 기술 선택">
<Box render={<MultiSelect.Trigger />} width="400px">
<MultiSelect.Value />
<MultiSelect.TriggerIcon />
</Box>
<MultiSelect.Content>
<MultiSelect.Group>
<MultiSelect.GroupLabel>프론트엔드</MultiSelect.GroupLabel>
<MultiSelect.Item value="react">
React
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
<MultiSelect.Item value="vue">
Vue
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
<MultiSelect.Item value="angular">
Angular
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
<MultiSelect.Item value="svelte">
Svelte
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
</MultiSelect.Group>
<MultiSelect.Separator />
<MultiSelect.Group>
<MultiSelect.GroupLabel>백엔드</MultiSelect.GroupLabel>
<MultiSelect.Item value="nodejs">
Node.js
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
<MultiSelect.Item value="python">
Python
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
<MultiSelect.Item value="java">
Java
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
<MultiSelect.Item value="go">
Go
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
</MultiSelect.Group>
<MultiSelect.Separator />
<MultiSelect.Group>
<MultiSelect.GroupLabel>데이터베이스</MultiSelect.GroupLabel>
<MultiSelect.Item value="mysql">
MySQL
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
<MultiSelect.Item value="postgresql">
PostgreSQL
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
<MultiSelect.Item value="mongodb">
MongoDB
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
<MultiSelect.Item value="redis">
Redis
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
</MultiSelect.Group>
</MultiSelect.Content>
</MultiSelect.Root>
);
}
Custom Value Display
MultiSelect.Value에 함수형 children을 제공하여 선택된 값들의 표시 방법을 커스터마이징할 수 있습니다. 기본적으로는 배지 형태로 표시되지만, 문자열이나 커스텀 컴포넌트로 변경할 수 있습니다.
import { Badge, Flex, Grid, MultiSelect, Text, VStack } from '@vapor-ui/core';
const languages = {
javascript: 'JavaScript',
typescript: 'TypeScript',
python: 'Python',
java: 'Java',
go: 'Go',
rust: 'Rust',
};
const renderRestValue = (value: string[]) => {
if (!value.length) {
return <MultiSelect.Placeholder>언어 선택</MultiSelect.Placeholder>;
}
const displayValues = value.slice(0, 2);
const remainingCount = value.length - 2;
return (
<Flex gap="$050" className="flex-wrap">
{displayValues.map((val) => (
<Badge key={val} size="sm">
{languages[val as keyof typeof languages]}
</Badge>
))}
{remainingCount > 0 && (
<Badge size="sm" color="hint">
+{remainingCount} more
</Badge>
)}
</Flex>
);
};
const renderStringValue = (value: string[]) => {
if (!value.length) {
return <MultiSelect.Placeholder>언어 선택</MultiSelect.Placeholder>;
}
return value.map((v) => languages[v as keyof typeof languages]).join(', ');
};
export default function MultiSelectCustomValue() {
return (
<Grid.Root templateColumns="1fr 1fr" gap="$300">
<VStack gap="$100" width="250px">
<Text typography="body2">커스텀 값 표시 (최대 2개 + 더보기)</Text>
<MultiSelect.Root items={languages} placeholder="언어 선택">
<MultiSelect.Trigger>
<MultiSelect.Value>{renderRestValue}</MultiSelect.Value>
<MultiSelect.TriggerIcon />
</MultiSelect.Trigger>
<MultiSelect.Content>
{Object.entries(languages).map(([value, label]) => (
<MultiSelect.Item key={value} value={value}>
{label}
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
))}
</MultiSelect.Content>
</MultiSelect.Root>
</VStack>
<VStack gap="$100" width="250px">
<Text typography="body2">문자열 형태 표시</Text>
<MultiSelect.Root items={languages} placeholder="언어 선택">
<MultiSelect.Trigger>
<MultiSelect.Value>{renderStringValue}</MultiSelect.Value>
<MultiSelect.TriggerIcon />
</MultiSelect.Trigger>
<MultiSelect.Content>
{Object.entries(languages).map(([value, label]) => (
<MultiSelect.Item key={value} value={value}>
{label}
<MultiSelect.ItemIndicator />
</MultiSelect.Item>
))}
</MultiSelect.Content>
</MultiSelect.Root>
</VStack>
</Grid.Root>
);
}
Props Table
MultiSelect.Root
MultiSelect의 루트 컨테이너로, 전체 MultiSelect 컴포넌트의 상태와 동작을 관리합니다.
Prop | Default | Type |
---|---|---|
items? | null | Array<{label: string, value: string}>Record<string, string> |
placeholder? | null | React.ReactNode |
size? | 'md' | 'sm''md''lg''xl' |
invalid? | false | boolean |
value? | [] | unknown[] |
defaultValue? | [] | unknown[] |
onValueChange? | null | (value: unknown[], event?: Event) => void |
disabled? | false | boolean |
readOnly? | false | boolean |
open? | false | boolean |
defaultOpen? | false | boolean |
onOpenChange? | null | (open: boolean, event?: Event) => void |
MultiSelect.Trigger
MultiSelect 드롭다운을 여는 트리거 요소입니다.
Prop | Default | Type |
---|---|---|
render? | button | React.ReactElement |
className? | null | string |
nativeButton? | true | boolean |
MultiSelect.Value
선택된 값들을 표시하는 컴포넌트입니다. 기본적으로 배지 형태로 표시되며, 함수형 children을 통해 커스텀 값 표시가 가능합니다.
Prop | Default | Type |
---|---|---|
children? | null | React.ReactNode(value: unknown[]) => React.ReactNode |
render? | span | React.ReactElement |
className? | null | string |
MultiSelect.Placeholder
값이 선택되지 않았을 때 표시되는 플레이스홀더 컴포넌트입니다.
Prop | Default | Type |
---|---|---|
render? | span | React.ReactElement |
className? | null | string |
MultiSelect.TriggerIcon
트리거 버튼의 드롭다운 아이콘을 표시하는 컴포넌트입니다.
Prop | Default | Type |
---|---|---|
children? | ChevronDownOutlineIcon | React.ReactNode |
render? | div | React.ReactElement |
className? | null | string |
MultiSelect.Portal
MultiSelect 드롭다운을 DOM의 다른 위치에 렌더링하는 포털 컴포넌트입니다.
Prop | Default | Type |
---|---|---|
container? | document.body | HTMLElement() => HTMLElement |
MultiSelect.Positioner
MultiSelect 드롭다운의 위치를 설정하는 컴포넌트입니다.
Prop | Default | Type |
---|---|---|
side? | 'bottom' | 'top''right''bottom''left' |
align? | 'start' | 'start''center''end' |
sideOffset? | 4 | number |
alignOffset? | 0 | number |
alignItemWithTrigger? | false | boolean |
className? | null | string |
MultiSelect.Popup
MultiSelect의 실제 드롭다운 팝업 영역입니다.
Prop | Default | Type |
---|---|---|
render? | div | React.ReactElement |
className? | null | string |
MultiSelect.Content
MultiSelect의 드롭다운 콘텐츠를 담는 컨테이너입니다. Portal과 Positioner를 조합하여 구성됩니다.
Prop | Default | Type |
---|---|---|
portalProps? | null | MultiSelectPortalProps |
positionerProps? | null | MultiSelectPositionerProps |
render? | div | React.ReactElement |
className? | null | string |
MultiSelect.Item
개별 선택 옵션을 나타내는 컴포넌트입니다. 다중 선택이 가능합니다.
Prop | Default | Type |
---|---|---|
value? | null | unknown |
render? | div | React.ReactElement |
className? | null | string |
children? | null | React.ReactNode |
MultiSelect.ItemIndicator
선택된 아이템에 표시되는 인디케이터 아이콘 컴포넌트입니다.
Prop | Default | Type |
---|---|---|
children? | ConfirmOutlineIcon | React.ReactNode |
render? | span | React.ReactElement |
className? | null | string |
MultiSelect.Group
관련된 아이템들을 그룹화하는 컴포넌트입니다.
Prop | Default | Type |
---|---|---|
render? | div | React.ReactElement |
className? | null | string |
children? | null | React.ReactNode |
MultiSelect.GroupLabel
그룹의 라벨을 표시하는 컴포넌트입니다.
Prop | Default | Type |
---|---|---|
render? | div | React.ReactElement |
className? | null | string |
children? | null | React.ReactNode |
MultiSelect.Separator
그룹 간의 구분선을 표시하는 컴포넌트입니다.
Prop | Default | Type |
---|---|---|
render? | div | React.ReactElement |
className? | null | string |