Skip to content

SuSelectBox

Custom SelectBox component with multiple selection support, integrated search, option groups and complete accessibility according to W3C standards.

Description

SuSelectBox is an enhanced selection component that extends the capabilities of a native <select> element. It offers advanced features such as search, multiple selection with tags, options with icons and descriptions, and complete keyboard accessibility.

Usage examples

Simple usage (without SuFormField)

Basic SelectBox

vue
<script setup>
import { ref } from 'vue'

const selectedValue = ref('')
const options = [
  { value: 'option1', label: 'Option 1' },
  { value: 'option2', label: 'Option 2' },
  { value: 'option3', label: 'Option 3' },
  { value: 'option4', label: 'Option 4', disabled: true }
]
</script>

<template>
  <SuSelectBox 
    :options="options"
    placeholder="Choose an option..."
    v-model="selectedValue"
  />
</template>

Multiple selection

Multiple selection with tags

vue
<script setup>
import { ref } from 'vue'

const selectedTechnologies = ref([])
const technologies = [
  { value: 'js', label: 'JavaScript' },
  { value: 'ts', label: 'TypeScript' },
  { value: 'vue', label: 'Vue.js' },
  { value: 'react', label: 'React' },
  { value: 'angular', label: 'Angular' }
]
</script>

<template>
  <SuSelectBox 
    :options="technologies"
    :multiple="true"
    :clearable="true"
    placeholder="Select your technologies..."
    v-model="selectedTechnologies"
  />
</template>

SelectBox with search

vue
<script setup>
import { ref } from 'vue'

const selectedCountry = ref('')
const countries = [
  { value: 'us', label: 'United States' },
  { value: 'uk', label: 'United Kingdom' },
  { value: 'fr', label: 'France' },
  { value: 'de', label: 'Germany' },
  { value: 'es', label: 'Spain' },
  { value: 'it', label: 'Italy' },
  { value: 'ca', label: 'Canada' },
  { value: 'jp', label: 'Japan' }
]
</script>

<template>
  <SuSelectBox 
    :options="countries"
    :searchable="true"
    :clearable="true"
    placeholder="Search a country..."
    searchPlaceholder="Type to search..."
    v-model="selectedCountry"
  />
</template>

Sizes

Available sizes

vue
<template>
  <SuSelectBox 
    :options="options"
    size="sm" 
    placeholder="Small SelectBox" 
  />
  
  <SuSelectBox 
    :options="options"
    size="md" 
    placeholder="Medium SelectBox" 
  />
  
  <SuSelectBox 
    :options="options"
    size="lg" 
    placeholder="Large SelectBox" 
  />
</template>

States

Visual states

vue
<template>
  <SuSelectBox 
    :options="options"
    placeholder="Default state"
  />
  
  <SuSelectBox 
    :options="options"
    state="error"
    value="invalid"
  />
  
  <SuSelectBox 
    :options="options"
    state="success"
    value="valid"
  />
  
  <SuSelectBox 
    :options="options"
    state="warning"
    value="warning"
  />
</template>

Usage with SuFormField

The SuSelectBox component can be used with SuFormField to benefit from a complete form structure with label, message and state management.

Basic usage with SuFormField

vue
<template>
  <SuFormField 
    label="Country" 
    message="Select your country of residence"
  >
    <template #default="slotProps">
      <SuSelectBox 
        :options="countries"
        placeholder="Choose a country..."
        v-bind="slotProps"
        v-model="selectedCountry"
      />
    </template>
  </SuFormField>
</template>

With slot props destructuring

vue
<template>
  <SuFormField 
    label="Category"
    :required="true"
    message="Required field"
  >
    <template #default="{ fieldId, messageId, state, disabled }">
      <SuSelectBox 
        :options="categories"
        :id="fieldId"
        :aria-describedby="messageId"
        :state="state"
        :disabled="disabled"
        placeholder="Select a category..."
        v-model="category"
      />
    </template>
  </SuFormField>
</template>

Validation with states

SelectBox with validation

vue
<script setup>
import { ref, computed } from 'vue'

const userType = ref('')
const userTypeError = ref('')

const validateUserType = () => {
  if (!userType.value) {
    userTypeError.value = 'Please select a user type'
  } else {
    userTypeError.value = ''
  }
}

const userTypeState = computed(() => {
  if (!userType.value) return 'default'
  return userTypeError.value ? 'error' : 'success'
})
</script>

<template>
  <SuFormField 
    label="User type"
    :required="true"
    :state="userTypeState"
    :message="userTypeError || 'Select the type that matches you'"
  >
    <template #default="slotProps">
      <SuSelectBox 
        :options="userTypes"
        placeholder="Select your type..."
        v-bind="slotProps"
        v-model="userType"
        @blur="validateUserType"
      />
    </template>
  </SuFormField>
</template>

Multiple selection with FormField

vue
<template>
  <SuFormField 
    label="Skills"
    message="Select up to 5 skills"
    :required="true"
  >
    <template #default="slotProps">
      <SuSelectBox 
        :options="skillOptions"
        :multiple="true"
        :searchable="true"
        :clearable="true"
        :maxSelectedItems="5"
        placeholder="Select your skills..."
        v-bind="slotProps"
        v-model="skills"
      />
    </template>
  </SuFormField>
</template>

Custom label and message

vue
<script setup>
import { UserIcon, InformationCircleIcon } from '@heroicons/vue/24/outline'
</script>

<template>
  <SuFormField 
    label="Role" 
    :required="true"
    message="Select your role in the organization"
  >
    <template #label="{ label, required, htmlFor }">
      <label 
        :for="htmlFor" 
        class="flex items-center gap-2 font-bold text-gray-900"
      >
        <UserIcon class="w-4 h-4" />
        {{ label }}
        <span v-if="required" class="text-red-500">*</span>
      </label>
    </template>
    
    <template #default="slotProps">
      <SuSelectBox 
        :options="roles"
        placeholder="Choose a role..."
        v-bind="slotProps"
        v-model="role"
      />
    </template>
    
    <template #message="{ state }">
      <div class="flex items-center gap-2 text-sm text-gray-600">
        <InformationCircleIcon class="w-4 h-4" />
        <span v-if="state === 'error'">⚠️ Please select a role</span>
        <span v-else>Select the role that matches your function</span>
      </div>
    </template>
  </SuFormField>
</template>

Options with icons and descriptions

vue
<script setup>
import { StarIcon, BuildingOfficeIcon, GlobeAltIcon } from '@heroicons/vue/24/outline'

const plans = [
  { 
    value: 'basic', 
    label: 'Basic Plan', 
    description: 'Basic features to get started',
    icon: StarIcon 
  },
  { 
    value: 'pro', 
    label: 'Pro Plan', 
    description: 'Advanced features for professionals',
    icon: BuildingOfficeIcon 
  },
  { 
    value: 'enterprise', 
    label: 'Enterprise Plan', 
    description: 'Complete solution for large companies',
    icon: GlobeAltIcon 
  }
]
</script>

<template>
  <SuFormField 
    label="Subscription plan"
    message="Choose the plan that fits your needs"
  >
    <template #default="slotProps">
      <SuSelectBox 
        :options="plans"
        :searchable="true"
        placeholder="Choose your plan..."
        v-bind="slotProps"
        v-model="selectedPlan"
      />
    </template>
  </SuFormField>
</template>

Grouped options

vue
<template>
  <SuFormField 
    label="Food products"
    message="Select your favorite products"
  >
    <template #default="slotProps">
      <SuSelectBox 
        :groups="[
          {
            label: 'Fruits',
            options: [
              { value: 'apple', label: 'Apple' },
              { value: 'banana', label: 'Banana' },
              { value: 'orange', label: 'Orange' }
            ]
          },
          {
            label: 'Vegetables',
            options: [
              { value: 'carrot', label: 'Carrot' },
              { value: 'broccoli', label: 'Broccoli' },
              { value: 'spinach', label: 'Spinach' }
            ]
          }
        ]"
        :searchable="true"
        :multiple="true"
        placeholder="Select products..."
        v-bind="slotProps"
        v-model="selectedProducts"
      />
    </template>
  </SuFormField>
</template>

Disabled field

vue
<template>
  <SuFormField 
    label="Disabled option"
    :disabled="true"
    message="This field is not editable currently"
  >
    <template #default="slotProps">
      <SuSelectBox 
        :options="options"
        value="selected"
        v-bind="slotProps"
      />
    </template>
  </SuFormField>
</template>

API

Props

PropTypeDefaultDescription
optionsSelectOption[][]List of available options
groupsSelectGroup[][]Options organized in groups
modelValuestring | number | (string | number)[]undefinedSelected value (v-model)
multiplebooleanfalseMultiple selection
searchablebooleanfalseIntegrated search
clearablebooleanfalseClear button
size'sm' | 'md' | 'lg''md'SelectBox size
state'default' | 'error' | 'success' | 'warning''default'Visual state
disabledbooleanfalseDisable the SelectBox
readonlybooleanfalseRead-only SelectBox
requiredbooleanfalseRequired field
placeholderstring'Select...'Placeholder text
searchPlaceholderstring'Search...'Search placeholder
noOptionsTextstring'No options available'No options text
noResultsTextstring'No results found'No results text
maxHeightstring'200px'Max dropdown height
textAlign'left' | 'center' | 'right''left'Text alignment
loadingbooleanfalseLoading state
closeOnSelectbooleantrueClose after selection
maxSelectedItemsnumberundefinedMultiple selection limit

Accessibility props (inherited from SuFormField)

When used with SuFormField, the component automatically receives:

PropTypeDescription
idstringUnique field ID (fieldId)
aria-describedbystringAssociated message ID (messageId)
statestringValidation state
disabledbooleanDisabled state

Option types

SelectOption

typescript
interface SelectOption {
  value: string | number
  label: string
  disabled?: boolean
  group?: string
  icon?: Component
  description?: string
}

SelectGroup

typescript
interface SelectGroup {
  label: string
  options: SelectOption[]
}

Events

EventTypeDescription
@update:modelValue(value: string | number | (string | number)[]) => voidEmitted when value changes (v-model)
@change(value: string | number | (string | number)[]) => voidEmitted on change
@open() => voidEmitted when dropdown opens
@close() => voidEmitted when dropdown closes
@search(query: string) => voidEmitted on search
@focus(event: FocusEvent) => voidEmitted on focus
@blur(event: FocusEvent) => voidEmitted on blur

Exposed methods

MethodTypeDescription
focus()() => voidFocuses the SelectBox
open()() => voidOpens the dropdown
close()() => voidCloses the dropdown
clear()() => voidClears the selection
selectRefRef<HTMLDivElement>Container reference

Accessibility

The SelectBox component follows WCAG 2.1 AA standards and W3C best practices:

✅ Accessibility features

  • Keyboard navigation: Complete support (arrows, Enter, Space, Escape, Home/End)
  • ARIA attributes: role="combobox", aria-expanded, aria-controls, aria-multiselectable
  • Focus trap: Focus management in dropdown
  • Voice announcements: Messages for screen readers
  • Associated labels: Via SuFormField props (fieldId)
  • State messages: Via aria-describedby (messageId)
  • Color contrast: WCAG AA compliant ratios (4.5:1)
  • Visible focus: Clear and contrasted indicators
  • RTL support: Right-to-left languages
  • Dark mode: Adapted contrast
  • Reduced motion: Respects prefers-reduced-motion

🎯 Usage best practices

vue
<!-- ✅ GOOD: Usage with SuFormField for complete accessibility -->
<SuFormField 
  label="Country"
  :required="true"
  :state="countryError ? 'error' : 'default'"
  :message="countryError || 'Select your country of residence'"
>
  <template #default="slotProps">
    <SuSelectBox 
      :options="countries"
      :searchable="true"
      placeholder="Search a country..."
      v-bind="slotProps"
      v-model="country"
    />
  </template>
</SuFormField>

<!-- ✅ GOOD: Standalone usage with manual ARIA attributes -->
<SuSelectBox 
  :options="options"
  id="country-select"
  aria-label="Country selection"
  aria-describedby="country-help"
  v-model="country"
/>
<span id="country-help">Select your country of residence</span>

<!-- ❌ BAD: Without label or description -->
<SuSelectBox 
  :options="options"
  v-model="value"
/>

Keyboard navigation

KeyAction
TabNavigate to/from SelectBox
Enter / SpaceOpen/close dropdown, select option
Arrow downOpen dropdown or next option
Arrow upPrevious option
HomeFirst option
EndLast option
EscapeClose dropdown
A-ZQuick search by first letter

Advanced usage examples

Complete form with validation

vue
<script setup>
import { ref, computed } from 'vue'

const formData = ref({
  userType: '',
  skills: [],
  country: ''
})

const errors = ref({})

const validateForm = () => {
  errors.value = {}
  
  if (!formData.value.userType) {
    errors.value.userType = 'User type required'
  }
  
  if (formData.value.skills.length === 0) {
    errors.value.skills = 'Select at least one skill'
  }
  
  if (!formData.value.country) {
    errors.value.country = 'Country required'
  }
  
  return Object.keys(errors.value).length === 0
}
</script>

<template>
  <form @submit.prevent="validateForm">
    <SuFormField 
      label="User type"
      :required="true"
      :state="errors.userType ? 'error' : 'default'"
      :message="errors.userType || 'Select your type'"
    >
      <template #default="slotProps">
        <SuSelectBox 
          :options="userTypes"
          placeholder="Choose a type..."
          v-bind="slotProps"
          v-model="formData.userType"
        />
      </template>
    </SuFormField>
    
    <SuFormField 
      label="Skills"
      :required="true"
      :state="errors.skills ? 'error' : 'default'"
      :message="errors.skills || 'Maximum 5 skills'"
    >
      <template #default="slotProps">
        <SuSelectBox 
          :options="skillOptions"
          :multiple="true"
          :searchable="true"
          :maxSelectedItems="5"
          placeholder="Select your skills..."
          v-bind="slotProps"
          v-model="formData.skills"
        />
      </template>
    </SuFormField>
    
    <SuFormField 
      label="Country"
      :required="true"
      :state="errors.country ? 'error' : 'default'"
      :message="errors.country || 'Country of residence'"
    >
      <template #default="slotProps">
        <SuSelectBox 
          :options="countries"
          :searchable="true"
          placeholder="Search a country..."
          v-bind="slotProps"
          v-model="formData.country"
        />
      </template>
    </SuFormField>
    
    <button type="submit">Submit</button>
  </form>
</template>
vue
<script setup>
import { ref, watch } from 'vue'

const searchQuery = ref('')
const options = ref([])
const loading = ref(false)
const selectedValue = ref('')

const searchOptions = async (query) => {
  if (query.length < 3) return
  
  loading.value = true
  try {
    const response = await fetch(`/api/search?q=${query}`)
    options.value = await response.json()
  } finally {
    loading.value = false
  }
}

watch(searchQuery, (newQuery) => {
  searchOptions(newQuery)
})
</script>

<template>
  <SuFormField 
    label="Dynamic search"
    message="Type at least 3 characters to search"
  >
    <template #default="slotProps">
      <SuSelectBox 
        :options="options"
        :searchable="true"
        :loading="loading"
        placeholder="Start typing..."
        searchPlaceholder="Minimum 3 characters..."
        v-bind="slotProps"
        @search="searchQuery = $event"
        v-model="selectedValue"
      />
    </template>
  </SuFormField>
</template>

SuSelectBoxField Component

For even simpler usage, you can use the SuSelectBoxField component which automatically combines SuFormField and SuSelectBox:

vue
<template>
  <!-- Instead of -->
  <SuFormField label="Country" message="Select your country">
    <template #default="slotProps">
      <SuSelectBox 
        :options="countries"
        v-bind="slotProps"
        v-model="country"
      />
    </template>
  </SuFormField>
  
  <!-- You can simply write -->
  <SuSelectBoxField 
    :options="countries"
    label="Country"
    message="Select your country"
    v-model="country"
  />
</template>

The SuSelectBoxField component accepts all props from both SuFormField and SuSelectBox combined, offering a more concise syntax while retaining all features.

See the complete SelectBoxField documentation for more details.

Publié sous licence MIT.