Skip to content

SelectBox ​

Composant SelectBox personnalisé avec support de la sélection multiple, recherche intégrée, groupes d'options et accessibilité complète selon les normes W3C.

Description ​

SuSelectBox est un composant de sélection enrichi qui étend les capacités d'un élément <select> natif. Il offre des fonctionnalités avancées telles que la recherche, la sélection multiple avec tags, les options avec icônes et descriptions, et une accessibilité complète au clavier.

Exemples d'utilisation ​

Utilisation simple (sans SuFormField) ​

SelectBox de base

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="Choisissez une option..."
    v-model="selectedValue"
  />
</template>

Sélection multiple ​

Sélection multiple avec 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="Sélectionnez vos technologies..."
    v-model="selectedTechnologies"
  />
</template>

Recherche intégrée ​

SelectBox avec recherche

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

const selectedCountry = ref('')
const countries = [
  { value: 'fr', label: 'France' },
  { value: 'us', label: 'États-Unis' },
  { value: 'de', label: 'Allemagne' },
  { value: 'es', label: 'Espagne' },
  { value: 'it', label: 'Italie' },
  { value: 'uk', label: 'Royaume-Uni' },
  { value: 'ca', label: 'Canada' },
  { value: 'jp', label: 'Japon' }
]
</script>

<template>
  <SuSelectBox 
    :options="countries"
    :searchable="true"
    :clearable="true"
    placeholder="Rechercher un pays..."
    searchPlaceholder="Tapez pour rechercher..."
    v-model="selectedCountry"
  />
</template>

Tailles ​

Tailles disponibles

vue
<template>
  <SuSelectBox 
    :options="options"
    size="sm" 
    placeholder="Petit SelectBox" 
  />
  
  <SuSelectBox 
    :options="options"
    size="md" 
    placeholder="SelectBox moyen" 
  />
  
  <SuSelectBox 
    :options="options"
    size="lg" 
    placeholder="Grand SelectBox" 
  />
</template>

États ​

États visuels

vue
<template>
  <SuSelectBox 
    :options="options"
    placeholder="État par défaut"
  />
  
  <SuSelectBox 
    :options="options"
    state="error"
    value="invalid"
  />
  
  <SuSelectBox 
    :options="options"
    state="success"
    value="valid"
  />
  
  <SuSelectBox 
    :options="options"
    state="warning"
    value="warning"
  />
</template>

Utilisation avec SuFormField ​

Le composant SuSelectBox peut être utilisé avec SuFormField pour bénéficier d'une structure de formulaire complète avec label, message et gestion des états.

Usage basique avec SuFormField ​

vue
<template>
  <SuFormField 
    label="Pays" 
    message="Sélectionnez votre pays de résidence"
  >
    <template #default="{ fieldId, labelId, messageId }">
      <SuSelectBox 
        :id="fieldId"
        :aria-labelledby="labelId"
        :aria-describedby="messageId"
        :options="countries"
        placeholder="Choisissez un pays..."
        v-model="selectedCountry"
      />
    </template>
  </SuFormField>
</template>

Nom accessible

Le déclencheur est un combobox (et non un <input>) : <label for> ne lui donne pas de nom. Reliez le label avec aria-labelledby="labelId", ou utilisez directement SuSelectBoxField, qui le fait pour vous.

Avec destructuration des slot props ​

vue
<template>
  <SuFormField 
    label="Catégorie"
    :required="true"
    message="Champ obligatoire"
  >
    <template #default="{ fieldId, labelId, messageId, state, disabled }">
      <SuSelectBox 
        :options="categories"
        :id="fieldId"
        :aria-labelledby="labelId"
        :aria-describedby="messageId"
        :state="state"
        :disabled="disabled"
        placeholder="Sélectionnez une catégorie..."
        v-model="category"
      />
    </template>
  </SuFormField>
</template>

Validation avec états ​

SelectBox avec validation

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

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

const validateUserType = () => {
  if (!userType.value) {
    userTypeError.value = 'Veuillez sélectionner un type d\'utilisateur'
  } else {
    userTypeError.value = ''
  }
}

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

<template>
  <SuFormField 
    label="Type d'utilisateur"
    :required="true"
    :state="userTypeState"
    :message="userTypeError || 'Sélectionnez le type qui vous correspond'"
  >
    <template #default="slotProps">
      <SuSelectBox 
        :options="userTypes"
        placeholder="Sélectionnez votre type..."
        v-bind="slotProps"
        v-model="userType"
        @blur="validateUserType"
      />
    </template>
  </SuFormField>
</template>

Sélection multiple avec FormField ​

vue
<template>
  <SuFormField 
    label="Compétences"
    message="Sélectionnez jusqu'à 5 compétences"
    :required="true"
  >
    <template #default="slotProps">
      <SuSelectBox 
        :options="skillOptions"
        :multiple="true"
        :searchable="true"
        :clearable="true"
        :maxSelectedItems="5"
        placeholder="Sélectionnez vos compétences..."
        v-bind="slotProps"
        v-model="skills"
      />
    </template>
  </SuFormField>
</template>

Label et message personnalisés ​

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

<template>
  <SuFormField 
    label="Rôle" 
    :required="true"
    message="Sélectionnez votre rôle dans l'organisation"
  >
    <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="Choisissez un rôle..."
        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'">⚠️ Veuillez sélectionner un rôle</span>
        <span v-else>Sélectionnez le rôle qui correspond à votre fonction</span>
      </div>
    </template>
  </SuFormField>
</template>

Options avec icônes et descriptions ​

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

const plans = [
  { 
    value: 'basic', 
    label: 'Plan Basic', 
    description: 'Fonctionnalités de base pour débuter',
    icon: StarIcon 
  },
  { 
    value: 'pro', 
    label: 'Plan Pro', 
    description: 'Fonctionnalités avancées pour les professionnels',
    icon: BuildingOfficeIcon 
  },
  { 
    value: 'enterprise', 
    label: 'Plan Enterprise', 
    description: 'Solution complète pour les grandes entreprises',
    icon: GlobeAltIcon 
  }
]
</script>

<template>
  <SuFormField 
    label="Plan d'abonnement"
    message="Choisissez le plan adapté à vos besoins"
  >
    <template #default="slotProps">
      <SuSelectBox 
        :options="plans"
        :searchable="true"
        placeholder="Choisissez votre plan..."
        v-bind="slotProps"
        v-model="selectedPlan"
      />
    </template>
  </SuFormField>
</template>

Options groupées ​

vue
<template>
  <SuFormField 
    label="Produits alimentaires"
    message="Sélectionnez vos produits préférés"
  >
    <template #default="slotProps">
      <SuSelectBox 
        :groups="[
          {
            label: 'Fruits',
            options: [
              { value: 'apple', label: 'Pomme' },
              { value: 'banana', label: 'Banane' },
              { value: 'orange', label: 'Orange' }
            ]
          },
          {
            label: 'Légumes',
            options: [
              { value: 'carrot', label: 'Carotte' },
              { value: 'broccoli', label: 'Brocoli' },
              { value: 'spinach', label: 'Épinard' }
            ]
          }
        ]"
        :searchable="true"
        :multiple="true"
        placeholder="Sélectionnez des produits..."
        v-bind="slotProps"
        v-model="selectedProducts"
      />
    </template>
  </SuFormField>
</template>

Champ désactivé ​

vue
<template>
  <SuFormField 
    label="Option désactivée"
    :disabled="true"
    message="Ce champ n'est pas modifiable actuellement"
  >
    <template #default="slotProps">
      <SuSelectBox 
        :options="options"
        value="selected"
        v-bind="slotProps"
      />
    </template>
  </SuFormField>
</template>

API ​

Props ​

PropTypeDefaultDescription
optionsSelectOption[][]Liste des options disponibles
groupsSelectGroup[][]Options organisées en groupes
modelValuestring | number | (string | number)[]undefinedValeur sélectionnée (v-model)
multiplebooleanfalseSélection multiple
searchablebooleanfalseRecherche intégrée
clearablebooleanfalseBouton d'effacement
size'sm' | 'md' | 'lg''md'Taille du SelectBox
state'default' | 'error' | 'success' | 'warning''default'État visuel
disabledbooleanfalseDésactive le SelectBox
readonlybooleanfalseSelectBox en lecture seule
requiredbooleanfalseChamp requis
placeholderstring'Sélectionnez...'Texte de placeholder
searchPlaceholderstring'Rechercher...'Placeholder de recherche
noOptionsTextstring'Aucune option disponible'Texte sans options
noResultsTextstring'Aucun résultat trouvé'Texte sans résultats
maxHeightstring'200px'Hauteur max du dropdown
textAlign'left' | 'center' | 'right''left'Alignement du texte
loadingbooleanfalseÉtat de chargement
closeOnSelectbooleantrueFermer après sélection
maxSelectedItemsnumberundefinedLimite de sélection multiple

Props d'accessibilité (héritées de SuFormField) ​

Lorsque utilisé avec SuFormField, le composant reçoit automatiquement :

PropTypeDescription
idstringID unique du champ (fieldId)
aria-labelledbystringID du label (labelId) : nomme le combobox et la liste
aria-describedbystringID du message associé (messageId)
statestringÉtat de validation
disabledbooleanÉtat désactivé

Types d'options ​

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)[]) => voidÉmis lors du changement de valeur (v-model)
@change(value: string | number | (string | number)[]) => voidÉmis lors du changement
@open() => voidÉmis à l'ouverture du dropdown
@close() => voidÉmis à la fermeture du dropdown
@search(query: string) => voidÉmis lors de la recherche
@focus(event: FocusEvent) => voidÉmis lors du focus
@blur(event: FocusEvent) => voidÉmis lors de la perte de focus

Méthodes exposées ​

MéthodeTypeDescription
focus()() => voidDonne le focus au SelectBox
open()() => voidOuvre le dropdown
close()() => voidFerme le dropdown
clear()() => voidEfface la sélection
selectRefRef<HTMLDivElement>Référence au conteneur

Accessibilité ​

Le composant SelectBox respecte les normes WCAG 2.1 AA et les bonnes pratiques W3C :

✅ Fonctionnalités d'accessibilité ​

  • Motif combobox + listbox (WAI-ARIA) : le déclencheur (role="combobox", aria-expanded, aria-controls) pilote une liste role="listbox" (aria-multiselectable en sélection multiple), dont les options portent aria-selected et aria-disabled
  • Focus conservé : le focus reste sur le déclencheur (ou sur le champ de recherche) ; l'option mise en avant est annoncée via aria-activedescendant et défile automatiquement dans la vue
  • Groupes nommés : chaque groupe d'options est un role="group" relié à son titre
  • Saisie rapide : taper une lettre met en avant l'option suivante qui commence ainsi ; avec searchable, la lettre ouvre la liste et amorce la recherche
  • Annonces vocales : sélection multiple, retrait d'une étiquette, limite atteinte
  • Labels associés : via aria-labelledby (labelId de SuFormField, automatique avec SuSelectBoxField)
  • Positionnement : la liste est en position: fixed, retournée au-dessus s'il manque de place et jamais coupée par un parent en overflow: hidden
  • Messages d'état : Via aria-describedby (messageId)
  • Contraste des couleurs : Ratios conformes WCAG AA (4.5:1)
  • Focus visible : Indicateurs clairs et contrastés
  • Support RTL : Langues de droite à gauche
  • Mode sombre : Contraste adapté
  • Réduction d'animation : Respect de prefers-reduced-motion

🎯 Bonnes pratiques d'utilisation ​

vue
<!-- ✅ BON : Utilisation avec SuFormField pour l'accessibilité complète -->
<SuFormField 
  label="Pays"
  :required="true"
  :state="countryError ? 'error' : 'default'"
  :message="countryError || 'Sélectionnez votre pays de résidence'"
>
  <template #default="{ fieldId, labelId, messageId }">
    <SuSelectBox 
      :id="fieldId"
      :aria-labelledby="labelId"
      :aria-describedby="messageId"
      :options="countries"
      :searchable="true"
      placeholder="Rechercher un pays..."
      v-model="country"
    />
  </template>
</SuFormField>

<!-- ✅ BON : Utilisation standalone avec attributs ARIA manuels -->
<SuSelectBox 
  :options="options"
  id="country-select"
  aria-label="Sélection du pays"
  aria-describedby="country-help"
  v-model="country"
/>
<span id="country-help">Sélectionnez votre pays de résidence</span>

<!-- ❌ MAUVAIS : Sans label ni description -->
<SuSelectBox 
  :options="options"
  v-model="value"
/>
ToucheAction
TabNaviguer vers/depuis le SelectBox (ferme la liste)
Entrée / EspaceOuvrir la liste, sélectionner l'option mise en avant
Flèche basOuvrir la liste ou option suivante (les options désactivées sont sautées)
Flèche hautOuvrir la liste ou option précédente
Alt + Flèche hautSélectionner l'option mise en avant
Début / FinPremière / dernière option (dans le champ de recherche : déplace le curseur)
ÉchapFermer la liste sans changer la valeur (le focus revient au déclencheur)
A-ZSaisie rapide : met en avant l'option suivante qui commence par ces lettres
Retour arrièreSélection multiple : retire la dernière étiquette (déclencheur ou recherche vide)

Exemples d'usage avancés ​

Formulaire complet avec 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 = 'Type d\'utilisateur requis'
  }
  
  if (formData.value.skills.length === 0) {
    errors.value.skills = 'Sélectionnez au moins une compétence'
  }
  
  if (!formData.value.country) {
    errors.value.country = 'Pays requis'
  }
  
  return Object.keys(errors.value).length === 0
}
</script>

<template>
  <form @submit.prevent="validateForm">
    <SuFormField 
      label="Type d'utilisateur"
      :required="true"
      :state="errors.userType ? 'error' : 'default'"
      :message="errors.userType || 'Sélectionnez votre type'"
    >
      <template #default="slotProps">
        <SuSelectBox 
          :options="userTypes"
          placeholder="Choisissez un type..."
          v-bind="slotProps"
          v-model="formData.userType"
        />
      </template>
    </SuFormField>
    
    <SuFormField 
      label="Compétences"
      :required="true"
      :state="errors.skills ? 'error' : 'default'"
      :message="errors.skills || 'Maximum 5 compétences'"
    >
      <template #default="slotProps">
        <SuSelectBox 
          :options="skillOptions"
          :multiple="true"
          :searchable="true"
          :maxSelectedItems="5"
          placeholder="Sélectionnez vos compétences..."
          v-bind="slotProps"
          v-model="formData.skills"
        />
      </template>
    </SuFormField>
    
    <SuFormField 
      label="Pays"
      :required="true"
      :state="errors.country ? 'error' : 'default'"
      :message="errors.country || 'Pays de résidence'"
    >
      <template #default="slotProps">
        <SuSelectBox 
          :options="countries"
          :searchable="true"
          placeholder="Rechercher un pays..."
          v-bind="slotProps"
          v-model="formData.country"
        />
      </template>
    </SuFormField>
    
    <button type="submit">Soumettre</button>
  </form>
</template>

Chargement dynamique avec recherche ​

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="Recherche dynamique"
    message="Tapez au moins 3 caractères pour rechercher"
  >
    <template #default="slotProps">
      <SuSelectBox 
        :options="options"
        :searchable="true"
        :loading="loading"
        placeholder="Commencez à taper..."
        searchPlaceholder="Minimum 3 caractères..."
        v-bind="slotProps"
        @search="searchQuery = $event"
        v-model="selectedValue"
      />
    </template>
  </SuFormField>
</template>

Composant SuSelectBoxField ​

Pour une utilisation encore plus simple, vous pouvez utiliser le composant SuSelectBoxField qui combine automatiquement SuFormField et SuSelectBox :

vue
<template>
  <!-- Au lieu de -->
  <SuFormField label="Pays" message="Sélectionnez votre pays">
    <template #default="slotProps">
      <SuSelectBox 
        :options="countries"
        v-bind="slotProps"
        v-model="country"
      />
    </template>
  </SuFormField>
  
  <!-- Vous pouvez simplement écrire -->
  <SuSelectBoxField 
    :options="countries"
    label="Pays"
    message="Sélectionnez votre pays"
    v-model="country"
  />
</template>

Le composant SelectBoxField accepte toutes les props de SuFormField et SuSelectBox combinées, offrant une syntaxe plus concise tout en conservant toutes les fonctionnalités.

Voir la documentation complète de SelectBoxField pour plus de détails.

Publié sous licence MIT.