BaseSelect
Accessible select component with keyboard navigation, v-model, and custom option slot.
Keyboard support: ↑ ↓ navigate, Enter/Space select, Escape close, Home/End jump to first/last.
Basic
Selected: —
<template>
<div style="max-width:320px">
<BaseSelect
v-model="value"
label="Framework"
placeholder="Select framework..."
:options="options"
/>
<p style="margin-top:8px;font-size:13px;color:#6b7280">Selected: {{ value ?? '—' }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const value = ref<string | null>(null)
const options = [
{ label: 'Vue 3', value: 'vue' },
{ label: 'React', value: 'react' },
{ label: 'Angular', value: 'angular' },
{ label: 'Svelte', value: 'svelte' },
]
</script>Validation
<template>
<div style="max-width:320px;display:flex;flex-direction:column;gap:12px">
<BaseSelect
v-model="value"
label="Role"
placeholder="Select role..."
:options="options"
:required="true"
:error="hasError"
:error-message="hasError ? 'Please select a role' : undefined"
/>
<BaseButton size="sm" @click="validate">Submit</BaseButton>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
const value = ref<string | null>(null)
const touched = ref(false)
const hasError = computed(() => touched.value && !value.value)
function validate() { touched.value = true }
const options = [
{ label: 'Developer', value: 'dev' },
{ label: 'Designer', value: 'design' },
{ label: 'Manager', value: 'pm' },
]
</script>Custom option slot
<template>
<div style="max-width:320px">
<BaseSelect
v-model="value"
label="Language"
placeholder="Select language..."
:options="options"
>
<template #option="{ option }">
<span style="display:flex;align-items:center;gap:8px">
<span>{{ option.icon }}</span>
<span>{{ option.label }}</span>
</span>
</template>
</BaseSelect>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const value = ref<string | null>(null)
const options = [
{ label: 'TypeScript', value: 'ts', icon: '🔷' },
{ label: 'JavaScript', value: 'js', icon: '🟨' },
{ label: 'Python', value: 'py', icon: '🐍' },
{ label: 'Rust', value: 'rs', icon: '🦀' },
]
</script>Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | string | number | null | null | Selected value (v-model) |
options | SelectOption[] | — | Array of { label, value } objects |
label | string | — | Label above the select |
placeholder | string | 'Select...' | Shown when no value selected |
disabled | boolean | false | Disables the select |
required | boolean | false | Shows asterisk, sets aria-required |
error | boolean | false | Applies error styling |
errorMessage | string | — | Error text below select |
hint | string | — | Hint text (hidden when errorMessage is set) |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | string | number | null | Emitted on selection |
change | string | number | null | Emitted on selection (same value) |
Slots
| Slot | Props | Description |
|---|---|---|
option | { option: SelectOption } | Custom option rendering |
Types
typescript
interface SelectOption<T = string | number> {
label: string
value: T
disabled?: boolean // option is visible but not selectable
group?: string // group label (rendered as a divider header)
}