Multi-select
A searchable multi-select that keeps what you picked as removable chips inside the field.
<script setup lang="ts">
import type { FieldOption } from '@core/schema/types'
import SaMultiSelect from '@shared/components/common/SaMultiSelect.vue'
import { ref } from 'vue'
const INTEGRATIONS: FieldOption[] = [
{ label: 'Slack', value: 'slack' },
{ label: 'GitHub', value: 'github' },
{ label: 'Linear', value: 'linear' },
{ label: 'Figma', value: 'figma' },
{ label: 'Notion', value: 'notion' },
{ label: 'Zendesk', value: 'zendesk' },
]
const connected = ref<Array<string | number>>(['slack', 'github'])
</script>
<template>
<div class="max-w-sm space-y-3">
<SaMultiSelect
v-model="connected"
:options="INTEGRATIONS"
placeholder="Choose integrations"
search-placeholder="Search integrations"
/>
<p class="text-sm text-muted-foreground">
{{ connected.length }} connected
</p>
</div>
</template>Usage
Bind an array with v-model and pass options as FieldOption[]. The array holds values, not options, and values are compared as strings.
The picked options live inside the control as chips. A row of selectable pills would make the form's height a function of how many options exist, and could not be searched.
The popover stays open while you pick, because choosing several things is the point. Click the trigger again, or press Escape, to close it.
Examples
Nothing selected
With an empty array the control shows the placeholder and keeps a single field's height.
<script setup lang="ts">
import type { FieldOption } from '@core/schema/types'
import SaMultiSelect from '@shared/components/common/SaMultiSelect.vue'
import { ref } from 'vue'
const TAGS: FieldOption[] = [
{ label: 'Billing', value: 'billing' },
{ label: 'Bug', value: 'bug' },
{ label: 'Feature request', value: 'feature' },
{ label: 'Onboarding', value: 'onboarding' },
]
const tags = ref<Array<string | number>>([])
</script>
<template>
<div class="max-w-sm">
<SaMultiSelect v-model="tags" :options="TAGS" placeholder="No tags yet" />
</div>
</template>Disabled options
An option carrying disabled stays in the list and cannot be picked.
<script setup lang="ts">
import type { FieldOption } from '@core/schema/types'
import SaMultiSelect from '@shared/components/common/SaMultiSelect.vue'
import { ref } from 'vue'
const PERMISSIONS: FieldOption[] = [
{ label: 'Read orders', value: 'orders.read' },
{ label: 'Write orders', value: 'orders.write' },
{ label: 'Refund orders', value: 'orders.refund' },
{ label: 'Manage billing (owner only)', value: 'billing.manage', disabled: true },
]
const granted = ref<Array<string | number>>(['orders.read'])
</script>
<template>
<div class="max-w-sm">
<SaMultiSelect v-model="granted" :options="PERMISSIONS" placeholder="Grant permissions" />
</div>
</template>Disabled
disabled blocks the trigger. The chips stay readable, and their remove buttons go with the trigger.
<script setup lang="ts">
import type { FieldOption } from '@core/schema/types'
import SaMultiSelect from '@shared/components/common/SaMultiSelect.vue'
import { ref } from 'vue'
const CHANNELS: FieldOption[] = [
{ label: 'Email', value: 'email' },
{ label: 'SMS', value: 'sms' },
{ label: 'Push', value: 'push' },
]
const channels = ref<Array<string | number>>(['email', 'push'])
</script>
<template>
<div class="max-w-sm">
<SaMultiSelect v-model="channels" :options="CHANNELS" disabled />
</div>
</template>API
Source: src/shared/components/common/SaMultiSelect.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | Array<string | number> | [] | The selected values. |
options | FieldOption[] | required | { label, value, disabled? }. A label that is a translation key is translated. |
placeholder | string | 'field.selectPrompt' | Shown while nothing is selected. The default renders as "Select". |
searchPlaceholder | string | 'field.searchPrompt' | The search box text. The default renders as "Search…". |
emptyText | string | 'field.nothingFound' | Shown when the search matches nothing. The default renders as "Nothing found". |
disabled | boolean | false | Blocks the trigger. |
id | string | — | Put on the trigger, so a <Label for> points at it. |
invalid | boolean | false | Sets aria-invalid on the trigger. |
required | boolean | — | Sets aria-required on the trigger. |
describedBy | string | — | Sets aria-describedby on the trigger. |
Emits
| Event | Payload | Description |
|---|---|---|
update:modelValue | value: Array<string | number> | Fires on select, on deselect and on removing a chip. |
Slots
None.
Accessibility
- The trigger is a button with
role="combobox"andaria-expanded, and takesaria-invalid,aria-requiredandaria-describedbyfrom the props of the same names. - Each chip's remove control is named "Remove <label>" and answers to
Enter. It stops the click from reaching the trigger, so removing a chip does not also open the list. - Selected options carry a check in the list, so the state does not rest on color.
- The list filters as you type, and
ArrowUp,ArrowDown,EnterandEscapework as they do in a command list.