Installation
Complete project setup first. Your configuration
must select vue/reka and include your Design System ID.
pnpm exec frontfriend download multi-select -f vue --primitive rekaReview dependencies reported by the downloader and keep your source changes in version control.
Usage
<script setup lang="ts">
import { MultiSelect } from "@/components/ui/multi-select";
import { reactive } from "vue";
const args = reactive(
{
"placeholder": "Select...",
"maxCount": 3,
"selectAll": false,
"clearable": true,
"createNew": false,
"batchCreate": false,
"searchPlaceholder": "Search...",
"closeOnSelect": false,
"options": [
{ label: "Italy", value: "italy" },
{ label: "United Kingdom", value: "uk" },
],
"value": ["italy", "uk"],
} satisfies Partial<
InstanceType<typeof MultiSelect> extends { $props: infer Props }
? Props
: {}
> &
Record<string, unknown>,
);
</script>
<template>
<MultiSelect
v-model="args.value"
:options="args.options"
:placeholder="args.placeholder"
:max-count="args.maxCount"
:select-all="args.selectAll"
:clearable="args.clearable"
:create-new="args.createNew"
:batch-create="args.batchCreate"
:search-placeholder="args.searchPlaceholder"
:close-on-select="args.closeOnSelect"
/>
</template> Behavior
- createNew and batchCreate are opt-in; batchCreate is inactive when createNew is false.
- Batch creation uses comma, semicolon, tab, and newline separators. Paste with a separator is processed immediately; typed batches are processed on Enter or Create new.
- normalizeNewValue applies only to new candidates before empty checks, validation, deduplication, and insertion.
- validateNewValue is synchronous and runs per normalized candidate. onCreateNew runs only after a new option is accepted.
- Valid unique candidates are retained when other batch candidates are invalid or duplicated; value bindings update once per batch.
- Existing unselected options are selected rather than recreated; existing selected values and repeated normalized candidates count as duplicates.
- options remains authoritative. Created selected values render with label ?? value and do not warn or become persistent searchable options until the consumer adds them to options.
- Search is trimmed, case-insensitive, diacritic-insensitive substring matching over label, value, and keywords, with one target-native filtering pass and no public custom filter callback.
- Filtered Select all adds missing visible options while preserving hidden selections; when every visible option is selected it removes only visible options.
- Batch validation errors remain visible after partial success. Duplicate-only batches show duplicateErrorMessage; mixed successful batches report duplicates through announcements without a visual duplicate error.
- The list exposes listbox semantics and rows expose option plus aria-selected. Internal checkboxes and icons are decorative; ariaLabel falls back to label.
- Announcements have English defaults and accept partial semantic formatter overrides without hardcoded selected/not-selected text in option labels.
- closeOnSelect closes only after selecting a previously unselected option; deselection keeps the popover open.
Examples
Standard selection
<script setup lang="ts">
import { ref } from "vue";
import { MultiSelect } from "@/components/ui/multi-select";
const countryList = [
{ value: "italy", label: "Italy", keywords: ["roma", "it"] },
{ value: "uk", label: "United Kingdom", keywords: ["britain", "gb"] },
{ value: "france", label: "France", keywords: ["paris", "fr"] },
{ value: "germany", label: "Germany", keywords: ["berlin", "de"] },
{ value: "usa", label: "United States", keywords: ["america", "us"] },
];
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
const args = {
"placeholder": "Select...",
"maxCount": 3,
"selectAll": false,
"clearable": true,
"createNew": false,
"batchCreate": false,
"searchPlaceholder": "Search...",
"closeOnSelect": false,
"options": countryList,
"class": TRIGGER_WIDTH,
"modelValue": undefined,
} satisfies Partial<
InstanceType<typeof MultiSelect> extends { $props: infer Props } ? Props : {}
> &
Record<string, unknown>;
const selected = ref(args.modelValue ?? ["italy", "uk"]);
</script>
<template>
<MultiSelect v-bind="args" v-model="selected" />
</template> Controlled and uncontrolled values
<script setup lang="ts">
import { ref } from "vue";
import { MultiSelect } from "@/components/ui/multi-select";
const countryList = [
{ value: "italy", label: "Italy", keywords: ["roma", "it"] },
{ value: "uk", label: "United Kingdom", keywords: ["britain", "gb"] },
{ value: "france", label: "France", keywords: ["paris", "fr"] },
{ value: "germany", label: "Germany", keywords: ["berlin", "de"] },
{ value: "usa", label: "United States", keywords: ["america", "us"] },
];
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
const args = {
"placeholder": "Select...",
"maxCount": 3,
"selectAll": false,
"clearable": true,
"createNew": false,
"batchCreate": false,
"searchPlaceholder": "Search...",
"closeOnSelect": false,
"options": countryList,
"class": TRIGGER_WIDTH,
} satisfies Partial<
InstanceType<typeof MultiSelect> extends { $props: infer Props } ? Props : {}
> &
Record<string, unknown>;
const controlled = ref(["italy"]);
</script>
<template>
<div class="flex flex-col gap-4">
<div class="flex items-center gap-2">
<MultiSelect v-bind="args" v-model="controlled" />
<button class="text-sm underline" @click="controlled = []">Reset</button>
</div>
<MultiSelect v-bind="args" :default-value="['france']" />
</div>
</template> Single create
<script setup lang="ts">
import { ref } from "vue";
import { MultiSelect } from "@/components/ui/multi-select";
const countryList = [
{ value: "italy", label: "Italy", keywords: ["roma", "it"] },
{ value: "uk", label: "United Kingdom", keywords: ["britain", "gb"] },
{ value: "france", label: "France", keywords: ["paris", "fr"] },
{ value: "germany", label: "Germany", keywords: ["berlin", "de"] },
{ value: "usa", label: "United States", keywords: ["america", "us"] },
];
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
const args = {
"placeholder": "Select...",
"maxCount": 3,
"selectAll": false,
"clearable": true,
"createNew": true,
"batchCreate": false,
"searchPlaceholder": "Search...",
"closeOnSelect": false,
"options": countryList,
"class": TRIGGER_WIDTH,
} satisfies Partial<
InstanceType<typeof MultiSelect> extends { $props: infer Props } ? Props : {}
> &
Record<string, unknown>;
const selected = ref<string[]>([]);
const created = ref<string[]>([]);
</script>
<template>
<div class="flex flex-col gap-2">
<MultiSelect
v-bind="args"
v-model="selected"
@create-new="(option) => created.push(option.value)"
/>
<p class="text-sm">Created: {{ created.join(", ") || "—" }}</p>
</div>
</template> Normalized partial batch create
<script setup lang="ts">
import { ref } from "vue";
import { MultiSelect } from "@/components/ui/multi-select";
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
const args = {
"placeholder": "Add recipients...",
"maxCount": 3,
"selectAll": false,
"clearable": true,
"createNew": true,
"batchCreate": true,
"searchPlaceholder": "Search...",
"closeOnSelect": false,
"options": [],
"class": TRIGGER_WIDTH,
} satisfies Partial<
InstanceType<typeof MultiSelect> extends { $props: infer Props } ? Props : {}
> &
Record<string, unknown>;
const selected = ref<string[]>([]);
const normalize = (value: string) => value.trim().toLowerCase();
const validate = (value: string) => /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value);
</script>
<template>
<MultiSelect
v-bind="args"
v-model="selected"
:normalize-new-value="normalize"
:validate-new-value="validate"
validation-error-message="Enter a valid email address"
duplicate-error-message="Already added"
/>
</template> Label, value and keyword search
<script setup lang="ts">
import { ref } from "vue";
import { MultiSelect } from "@/components/ui/multi-select";
const countryList = [
{ value: "italy", label: "Italy", keywords: ["roma", "it"] },
{ value: "uk", label: "United Kingdom", keywords: ["britain", "gb"] },
{ value: "france", label: "France", keywords: ["paris", "fr"] },
{ value: "germany", label: "Germany", keywords: ["berlin", "de"] },
{ value: "usa", label: "United States", keywords: ["america", "us"] },
];
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
const args = {
"placeholder": "Select...",
"maxCount": 3,
"selectAll": false,
"clearable": true,
"createNew": false,
"batchCreate": false,
"searchPlaceholder": "Search...",
"closeOnSelect": false,
"options": countryList,
"class": TRIGGER_WIDTH,
} satisfies Partial<
InstanceType<typeof MultiSelect> extends { $props: infer Props } ? Props : {}
> &
Record<string, unknown>;
const selected = ref<string[]>([]);
</script>
<template>
<MultiSelect v-bind="args" v-model="selected" />
</template> Filtered Select all merge and removal
<script setup lang="ts">
import { ref } from "vue";
import { MultiSelect } from "@/components/ui/multi-select";
const countryList = [
{ value: "italy", label: "Italy", keywords: ["roma", "it"] },
{ value: "uk", label: "United Kingdom", keywords: ["britain", "gb"] },
{ value: "france", label: "France", keywords: ["paris", "fr"] },
{ value: "germany", label: "Germany", keywords: ["berlin", "de"] },
{ value: "usa", label: "United States", keywords: ["america", "us"] },
];
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
const args = {
"placeholder": "Select...",
"maxCount": 3,
"selectAll": true,
"clearable": true,
"createNew": false,
"batchCreate": false,
"searchPlaceholder": "Search...",
"closeOnSelect": false,
"options": countryList,
"class": TRIGGER_WIDTH,
} satisfies Partial<
InstanceType<typeof MultiSelect> extends { $props: infer Props } ? Props : {}
> &
Record<string, unknown>;
const selected = ref(["italy"]);
</script>
<template>
<MultiSelect v-bind="args" v-model="selected" />
</template> Accessible announcements and selected state
<script setup lang="ts">
import { ref } from "vue";
import { MultiSelect } from "@/components/ui/multi-select";
const countryList = [
{ value: "italy", label: "Italy", keywords: ["roma", "it"] },
{ value: "uk", label: "United Kingdom", keywords: ["britain", "gb"] },
{ value: "france", label: "France", keywords: ["paris", "fr"] },
{ value: "germany", label: "Germany", keywords: ["berlin", "de"] },
{ value: "usa", label: "United States", keywords: ["america", "us"] },
];
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
const args = {
"placeholder": "Select...",
"maxCount": 3,
"selectAll": true,
"clearable": true,
"createNew": false,
"batchCreate": false,
"searchPlaceholder": "Search...",
"closeOnSelect": false,
"options": countryList,
"class": TRIGGER_WIDTH,
} satisfies Partial<
InstanceType<typeof MultiSelect> extends { $props: infer Props } ? Props : {}
> &
Record<string, unknown>;
const selected = ref<string[]>([]);
const announcements = {
selected: (option: { label: string }) => `${option.label} selezionato`,
};
</script>
<template>
<MultiSelect
v-bind="args"
v-model="selected"
:announcements="announcements"
clear-aria-label="Rimuovi tutte le selezioni"
/>
</template> contentClassName customisation
<script setup lang="ts">
import { ref } from "vue";
import { MultiSelect } from "@/components/ui/multi-select";
const countryList = [
{ value: "italy", label: "Italy", keywords: ["roma", "it"] },
{ value: "uk", label: "United Kingdom", keywords: ["britain", "gb"] },
{ value: "france", label: "France", keywords: ["paris", "fr"] },
{ value: "germany", label: "Germany", keywords: ["berlin", "de"] },
{ value: "usa", label: "United States", keywords: ["america", "us"] },
];
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
const args = {
"placeholder": "Select...",
"maxCount": 3,
"selectAll": false,
"clearable": true,
"createNew": false,
"batchCreate": false,
"searchPlaceholder": "Search...",
"closeOnSelect": false,
"options": countryList,
"class": TRIGGER_WIDTH,
} satisfies Partial<
InstanceType<typeof MultiSelect> extends { $props: infer Props } ? Props : {}
> &
Record<string, unknown>;
const selected = ref<string[]>([]);
</script>
<template>
<MultiSelect
v-bind="args"
v-model="selected"
content-class="max-h-64 shadow-lg"
/>
</template> Custom search and close on select
<script setup lang="ts">
import { ref } from "vue";
import { MultiSelect } from "@/components/ui/multi-select";
const countryList = [
{ value: "italy", label: "Italy", keywords: ["roma", "it"] },
{ value: "uk", label: "United Kingdom", keywords: ["britain", "gb"] },
{ value: "france", label: "France", keywords: ["paris", "fr"] },
{ value: "germany", label: "Germany", keywords: ["berlin", "de"] },
{ value: "usa", label: "United States", keywords: ["america", "us"] },
];
const TRIGGER_WIDTH = "min-w-[260px] w-auto";
const args = {
"placeholder": "Select...",
"maxCount": 3,
"selectAll": false,
"clearable": true,
"createNew": false,
"batchCreate": false,
"searchPlaceholder": "Find a country",
"closeOnSelect": true,
"options": countryList,
"class": TRIGGER_WIDTH,
} satisfies Partial<
InstanceType<typeof MultiSelect> extends { $props: infer Props } ? Props : {}
> &
Record<string, unknown>;
const selected = ref<string[]>([]);
</script>
<template>
<MultiSelect v-bind="args" v-model="selected" />
</template> API reference
Frontfriend-owned props. Native HTML and inherited primitive props remain documented by Reka UI.
| Prop | Type | Default | Description |
|---|---|---|---|
animation | Number | — | |
announcements | Object as () => Partial<MultiSelectAnnouncements> | — | |
batchCreate | Boolean | — | Split one create into several values on comma, semicolon, tab or newline |
class | String | — | |
clearable | Boolean | — | |
clearAriaLabel | String | — | |
closeOnSelect | Boolean | — | |
contentClass | String | — | |
createNew | Boolean | — | Allow values absent from `options` to be created from the search box |
createNewText | String | — | |
defaultValue | Array as () => string[] | — | |
duplicateErrorMessage | String | — | |
emptySearchText | String | — | |
fullWidth | Boolean | — | |
maxCount | Number | — | |
modalPopover | Boolean | — | |
modelValue | Array as () => string[] | — | |
moreBadgeSubtle | Boolean | — | |
moreBadgeText | String | — | |
moreBadgeVariant | String as PropType<BadgeVariant> | — | |
normalizeNewValue | Function as unknown as () => (value: string) => string | — | |
options | Array as () => MultiSelectOption[] | — | |
placeholder | String | — | |
searchPlaceholder | String | — | |
selectAll | Boolean | — | |
selectAllText | String | — | |
validateNewValue | Function as unknown as () => (value: string) => boolean | — | |
validationErrorMessage | String | — |
Configuration determines the actual classes and available styling hooks. See Theming & component configuration before introducing a repeated local override.