Multi Select
Multi Select фильтрует доступные варианты и хранит выбранный набор в поле как удаляемые чипы. Выбор одного пункта не закрывает меню.
Статус: experimental · в системе с 2026-07 · интерактивность: always (всегда)
Когда использовать
- Из длинного плоского списка нужно выбрать небольшой набор значений.
- Выбранный набор важно видеть и исправлять прямо в поле.
- Пунктам нужен короткий вспомогательный факт, например ID или размер аудитории.
Не использовать:
- Нужен один результат — используй Combobox.
- Все варианты должны оставаться видимыми — используй Checkbox Group.
- Нужно сравнивать большие доступный и выбранный наборы — используй Dualist.
- Варианты образуют иерархию — используй отдельный иерархический сценарий.
Альтернативы:
combobox— одно значение из длинного плоского спискаcheckbox-group— небольшой набор вариантов важно видеть целикомdualist— большие доступный и выбранный наборы требуют отдельных областей
Базовый
Выбранные навыки остаются видимыми в поле.
Исследования Прототипирование
<!--
🟢
Базовый пример Multi Select сохраняет выбранные навыки внутри поля.
Каждый чип можно убрать независимо, не открывая отдельный менеджер списка.
-->
<script>
import { t } from "$lib/spec/demo-lang.svelte.js";
import MultiSelect from "$lib/ui/MultiSelect.svelte";
let skills = $state(["research", "prototyping"]);
const options = [
{ value: "research", label: t("Исследования") },
{ value: "prototyping", label: t("Прототипирование") },
{ value: "systems", label: t("Дизайн-системы") },
{ value: "writing", label: t("UX-тексты") },
{ value: "analytics", label: t("Аналитика") },
];
</script>
<div style="width: var(--control-field-min);">
<MultiSelect label={t("Навыки")} options={options} bind:value={skills} />
</div>
Фильтрация
Поиск сужает список по label; короткая вторая строка остаётся вспомогательной.
<!--
🟢
Пример фильтрации показывает Multi Select со вспомогательным размером
аудитории. Запрос ищет по названию, очищается после выбора и не попадает
в итоговый набор.
-->
<script>
import { t } from "$lib/spec/demo-lang.svelte.js";
import MultiSelect from "$lib/ui/MultiSelect.svelte";
let audiences = $state([]);
const options = [
{ value: "active-7d", label: t("Активные за 7 дней"), description: t("24 580 человек") },
{ value: "buyers-30d", label: t("Покупатели за 30 дней"), description: t("8 420 человек") },
{ value: "cart", label: t("Оставили товар в корзине"), description: t("3 180 человек") },
{ value: "new", label: t("Новые посетители"), description: t("61 240 человек") },
{ value: "returning", label: t("Вернувшиеся посетители"), description: t("17 960 человек") },
];
</script>
<div style="width: var(--control-field-min);">
<MultiSelect
label={t("Аудитории")}
placeholder={t("Найти аудиторию")}
emptyMessage={t("Аудитория не найдена.")}
options={options}
bind:value={audiences}
/>
</div>
Поле
Подсказка, обязательность, описание и ошибка.
<!--
🟢
Пример поля Multi Select показывает служебные сообщения формы. Ошибка и
описание относятся ко всему набору, а не к отдельному выбранному чипу.
-->
<script>
import { t } from "$lib/spec/demo-lang.svelte.js";
import MultiSelect from "$lib/ui/MultiSelect.svelte";
let channels = $state([]);
let invalidChannels = $state([]);
const options = [
{ value: "search", label: t("Поиск") },
{ value: "social", label: t("Соцсети") },
{ value: "video", label: t("Видео") },
{ value: "native", label: t("Нативная реклама") },
];
</script>
<div style="display: grid; gap: var(--sp-3); width: var(--control-field-min);">
<MultiSelect
label={t("Каналы")}
hint={t("Можно выбрать несколько")}
description={t("Выбранные каналы останутся видимыми.")}
name="channels"
required
options={options}
bind:value={channels}
/>
<MultiSelect
label={t("Каналы")}
error={t("Выберите хотя бы один канал.")}
options={options}
bind:value={invalidChannels}
/>
</div>
Строка со счётчиком
overflow="count": чипы в одну строку, лишние скрыты, справа «+N» с полным списком в title — как в эталоне.
США Германия Франция Бразилия Япония Индия
<!--
🟢
Строчный режим эталона (select__multiple-wrapper): чипы в одну строку,
не поместившиеся скрыты, справа счётчик «+N» с полным списком в title.
-->
<script>
import { t } from "$lib/spec/demo-lang.svelte.js";
import MultiSelect from "$lib/ui/MultiSelect.svelte";
const options = [
{ value: "us", label: t("США") },
{ value: "de", label: t("Германия") },
{ value: "fr", label: t("Франция") },
{ value: "br", label: t("Бразилия") },
{ value: "jp", label: t("Япония") },
{ value: "in", label: t("Индия") },
{ value: "ca", label: t("Канада") },
];
let value = $state(["us", "de", "fr", "br", "jp", "in"]);
</script>
<div style="width: 320px;">
<MultiSelect label={t("Страны")} {options} bind:value overflow="count" />
</div>
Вставка списком
bulk: кнопка со значком списка открывает диалог, известные значения добавляются к выбору.
Кампания 1001
<!--
🟢
Вставка списком (эталон: Vue-легаси select `modalChangeable` +
modal/entitiesList): кнопка со значком списка открывает диалог, известные
значения добавляются к выбору, весь список уходит в onbulk.
-->
<script>
import { t } from "$lib/spec/demo-lang.svelte.js";
import MultiSelect from "$lib/ui/MultiSelect.svelte";
const options = Array.from({ length: 12 }, (_, i) => ({ value: String(1000 + i), label: `${t("Кампания")} ${1000 + i}` }));
let value = $state(["1001"]);
let unknown = $state([]);
</script>
<div style="display: flex; flex-direction: column; gap: var(--sp-2); width: 360px;">
<MultiSelect label={t("Кампании")} {options} bind:value bulk bulkTitle={t("ID кампаний")} onbulk={(list) => (unknown = list.filter((id) => !options.some((o) => o.value === String(id))))} />
{#if unknown.length}
<span aria-live="polite">{t("Не найдены:")} {unknown.join(", ")}</span>
{/if}
</div>