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 컴포넌트의 상태와 동작을 관리합니다.

PropDefaultType
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 드롭다운을 여는 트리거 요소입니다.

PropDefaultType
render?
button
React.ReactElement
className?
null
string
nativeButton?
true
boolean

MultiSelect.Value

선택된 값들을 표시하는 컴포넌트입니다. 기본적으로 배지 형태로 표시되며, 함수형 children을 통해 커스텀 값 표시가 가능합니다.

PropDefaultType
children?
null
React.ReactNode(value: unknown[]) => React.ReactNode
render?
span
React.ReactElement
className?
null
string

MultiSelect.Placeholder

값이 선택되지 않았을 때 표시되는 플레이스홀더 컴포넌트입니다.

PropDefaultType
render?
span
React.ReactElement
className?
null
string

MultiSelect.TriggerIcon

트리거 버튼의 드롭다운 아이콘을 표시하는 컴포넌트입니다.

PropDefaultType
children?
ChevronDownOutlineIcon
React.ReactNode
render?
div
React.ReactElement
className?
null
string

MultiSelect.Portal

MultiSelect 드롭다운을 DOM의 다른 위치에 렌더링하는 포털 컴포넌트입니다.

PropDefaultType
container?
document.body
HTMLElement() => HTMLElement

MultiSelect.Positioner

MultiSelect 드롭다운의 위치를 설정하는 컴포넌트입니다.

PropDefaultType
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의 실제 드롭다운 팝업 영역입니다.

PropDefaultType
render?
div
React.ReactElement
className?
null
string

MultiSelect.Content

MultiSelect의 드롭다운 콘텐츠를 담는 컨테이너입니다. Portal과 Positioner를 조합하여 구성됩니다.

PropDefaultType
portalProps?
null
MultiSelectPortalProps
positionerProps?
null
MultiSelectPositionerProps
render?
div
React.ReactElement
className?
null
string

MultiSelect.Item

개별 선택 옵션을 나타내는 컴포넌트입니다. 다중 선택이 가능합니다.

PropDefaultType
value?
null
unknown
render?
div
React.ReactElement
className?
null
string
children?
null
React.ReactNode

MultiSelect.ItemIndicator

선택된 아이템에 표시되는 인디케이터 아이콘 컴포넌트입니다.

PropDefaultType
children?
ConfirmOutlineIcon
React.ReactNode
render?
span
React.ReactElement
className?
null
string

MultiSelect.Group

관련된 아이템들을 그룹화하는 컴포넌트입니다.

PropDefaultType
render?
div
React.ReactElement
className?
null
string
children?
null
React.ReactNode

MultiSelect.GroupLabel

그룹의 라벨을 표시하는 컴포넌트입니다.

PropDefaultType
render?
div
React.ReactElement
className?
null
string
children?
null
React.ReactNode

MultiSelect.Separator

그룹 간의 구분선을 표시하는 컴포넌트입니다.

PropDefaultType
render?
div
React.ReactElement
className?
null
string