Skip to content

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

PropTypeDefaultDescription
modelValuestring | number | nullnullSelected value (v-model)
optionsSelectOption[]Array of { label, value } objects
labelstringLabel above the select
placeholderstring'Select...'Shown when no value selected
disabledbooleanfalseDisables the select
requiredbooleanfalseShows asterisk, sets aria-required
errorbooleanfalseApplies error styling
errorMessagestringError text below select
hintstringHint text (hidden when errorMessage is set)

Events

EventPayloadDescription
update:modelValuestring | number | nullEmitted on selection
changestring | number | nullEmitted on selection (same value)

Slots

SlotPropsDescription
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)
}