Skip to content

Dropdown ​

Composant Dropdown qui ressemble à un bouton mais affiche une liste d'options au clic. Chaque option peut être un lien (si elle contient un href) ou déclencher une action. Conforme aux normes W3C d'accessibilité avec support complet du clavier et des lecteurs d'écran.

Exemples d'utilisation ​

Menu utilisateur simple

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

const userOptions = [
  { value: 'profile', label: 'Mon profil', icon: UserIcon },
  { value: 'settings', label: 'Paramètres', icon: CogIcon },
  { value: 'separator1', separator: true },
  { value: 'logout', label: 'Déconnexion', icon: ArrowUturnLeftIcon }
]

const handleSelect = (option) => {
  console.log('Option sélectionnée:', option)
  // Gérer l'action selon l'option
}
</script>

<template>
  <SuDropdown 
    :options="userOptions"
    label="Menu utilisateur"
    :icon="UserIcon"
    @select="handleSelect"
  />
</template>

Avec liens et actions mixtes ​

Options mixtes (liens + actions)

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

const mixedOptions = [
  { value: 'profile', label: 'Mon profil', icon: UserIcon },
  { value: 'docs', label: 'Documentation', href: '/docs', icon: DocumentIcon },
  { value: 'separator1', separator: true },
  { value: 'settings', label: 'Paramètres', icon: CogIcon },
  { value: 'help', label: 'Aide', href: 'https://help.example.com', target: '_blank', icon: QuestionMarkCircleIcon }
]

const handleSelect = (option) => {
  // Les options avec href naviguent automatiquement
  // Les options sans href déclenchent cette fonction
  if (!option.href) {
    console.log('Action:', option.value)
  }
}
</script>

<template>
  <SuDropdown 
    :options="mixedOptions"
    label="Menu mixte"
    :icon="CogIcon"
    variant="secondary"
    @select="handleSelect"
  />
</template>

Icône seule ​

Dropdown avec icône uniquement

vue
<script setup>
import { EllipsisVerticalIcon, PencilIcon, DocumentDuplicateIcon, TrashIcon } from '@heroicons/vue/24/outline'

const actionOptions = [
  { value: 'edit', label: 'Modifier', icon: PencilIcon },
  { value: 'duplicate', label: 'Dupliquer', icon: DocumentDuplicateIcon },
  { value: 'separator1', separator: true },
  { value: 'delete', label: 'Supprimer', icon: TrashIcon }
]
</script>

<template>
  <SuDropdown 
    :options="actionOptions"
    :icon="EllipsisVerticalIcon"
    iconDisplay="only"
    aria-label="Menu des actions"
    variant="ghost"
    @select="handleSelect"
  />
</template>

Déclenchement au survol ​

Ouverture au survol

vue
<template>
  <SuDropdown 
    :options="productOptions"
    label="Produits"
    trigger="hover"
    variant="ghost"
    @select="handleSelect"
  />
</template>

Tailles ​

Tailles disponibles

vue
<template>
  <SuDropdown size="sm" label="Small" :options="options" />
  <SuDropdown size="md" label="Medium" :options="options" />
  <SuDropdown size="lg" label="Large" :options="options" />
</template>

Variantes ​

Variantes disponibles

vue
<template>
  <SuDropdown variant="primary" label="Primary" :options="options" />
  <SuDropdown variant="secondary" label="Secondary" :options="options" />
  <SuDropdown variant="outline" label="Outline" :options="options" />
  <SuDropdown variant="ghost" label="Ghost" :options="options" />
</template>

Avec descriptions ​

Options avec descriptions

vue
<script setup>
import { PlusIcon, DocumentIcon, ArrowUpTrayIcon } from '@heroicons/vue/24/outline'

const createOptions = [
  { 
    value: 'create', 
    label: 'Créer un document', 
    description: 'Nouveau document vierge',
    icon: PlusIcon
  },
  { 
    value: 'template', 
    label: 'Utiliser un modèle', 
    description: 'Partir d\'un modèle existant',
    icon: DocumentIcon
  },
  { value: 'separator1', separator: true },
  { 
    value: 'import', 
    label: 'Importer', 
    description: 'Importer depuis un fichier',
    icon: ArrowUpTrayIcon
  }
]
</script>

<template>
  <SuDropdown 
    :options="createOptions"
    label="Nouveau"
    :icon="PlusIcon"
    variant="primary"
    @select="handleSelect"
  />
</template>

États ​

États du dropdown

vue
<template>
  <SuDropdown label="Normal" :options="options" />
  <SuDropdown label="Chargement..." :options="options" :loading="true" />
  <SuDropdown label="Désactivé" :options="options" :disabled="true" />
</template>

API ​

Props ​

PropTypeDefaultDescription
optionsDropdownOption[][]Liste des options du dropdown
trigger'click' | 'hover''click'Déclencheur d'ouverture
placement'bottom-start' | 'bottom-end' | 'top-start' | 'top-end' | 'left-start' | 'left-end' | 'right-start' | 'right-end''bottom-start'Position du menu
size'sm' | 'md' | 'lg''md'Taille du bouton
variant'primary' | 'secondary' | 'outline' | 'ghost''outline'Variante du bouton
disabledbooleanfalseDésactive le dropdown
loadingbooleanfalseÉtat de chargement
iconComponentundefinedIcône du bouton
iconDisplay'left' | 'right' | 'only''left'Position de l'icône
labelstringundefinedLabel du bouton
closeOnSelectbooleantrueFermer après sélection
maxHeightstring'300px'Hauteur maximale du menu

Types d'options ​

typescript
interface DropdownOption {
  value: string | number
  label: string
  href?: string                    // Si présent, l'option devient un lien
  target?: '_blank' | '_self' | '_parent' | '_top'
  rel?: string
  disabled?: boolean
  icon?: Component
  description?: string
  separator?: boolean              // Si true, affiche un séparateur
}

Attributs d'accessibilité ​

PropTypeDefaultDescription
ariaLabelstringundefinedLabel accessible
ariaDescribedBystringundefinedID de l'élément de description
rolestringundefinedRôle ARIA personnalisé
tabIndexnumber0Ordre de tabulation

Events ​

EventTypeDescription
@select(option: DropdownOption) => voidÉmis lors de la sélection d'une option
@open() => voidÉmis à l'ouverture du menu
@close() => voidÉmis à la fermeture du menu
@focus(event: FocusEvent) => voidÉmis lors du focus sur le bouton
@blur(event: FocusEvent) => voidÉmis lors de la perte de focus

Méthodes exposées ​

MéthodeTypeDescription
focus()() => voidDonne le focus au bouton
open()() => voidOuvre le menu
close()() => voidFerme le menu
dropdownRefRef<HTMLDivElement>Référence au conteneur
triggerRefRef<HTMLButtonElement>Référence au bouton

Différence entre liens et actions ​

Le composant Dropdown gère automatiquement deux types d'options :

🔗 Options avec liens (href) ​

Les options avec une propriété href deviennent des liens :

  • Navigation automatique vers l'URL
  • Support des liens externes avec target="_blank"
  • Attributs de sécurité automatiques (rel="noopener noreferrer")
  • Icône externe automatique pour les liens externes
vue
<!-- Option lien interne -->
{ value: 'docs', label: 'Documentation', href: '/docs' }

<!-- Option lien externe -->
{ value: 'github', label: 'GitHub', href: 'https://github.com', target: '_blank' }

⚡ Options avec actions ​

Les options sans href déclenchent l'événement @select :

  • Émission de l'événement avec l'option complète
  • Fermeture automatique du menu (si closeOnSelect est true)
  • Gestion personnalisée dans le composant parent
vue
<!-- Option action -->
{ value: 'delete', label: 'Supprimer', icon: TrashIcon }

<SuDropdown @select="handleAction" />

<script setup>
const handleAction = (option) => {
  if (option.value === 'delete') {
    // Logique de suppression
  }
}
</script>

Séparateurs ​

Les séparateurs permettent de grouper visuellement les options :

vue
const options = [
  { value: 'edit', label: 'Modifier' },
  { value: 'duplicate', label: 'Dupliquer' },
  { value: 'separator1', separator: true }, // Séparateur
  { value: 'delete', label: 'Supprimer' }
]

Positionnement ​

Le dropdown supporte 8 positions différentes :

📍 Positions disponibles ​

  • Bottom : bottom-start, bottom-end
  • Top : top-start, top-end
  • Left : left-start, left-end
  • Right : right-start, right-end
vue
<!-- Menu qui s'ouvre en bas à gauche -->
<SuDropdown placement="bottom-start" />

<!-- Menu qui s'ouvre en haut à droite -->
<SuDropdown placement="top-end" />

Accessibilité ​

Le composant Dropdown respecte les normes WCAG 2.1 AA :

✅ Fonctionnalités d'accessibilité ​

  • Motif « bouton de menu » (WAI-ARIA) : bouton avec aria-haspopup="menu", menu role="menu" nommé par le bouton, items role="menuitem"
  • Focus mobile (roving) : un seul item à la fois reçoit le focus ; les flèches le déplacent en boucle, les items désactivés sont sautés
  • Saisie rapide : taper une lettre amène le focus sur l'item suivant qui commence par elle
  • Fermeture : Échap (focus rendu au bouton), appui à l'extérieur ou focus qui quitte le menu ; Tab sort du menu vers l'élément suivant de la page
  • États ARIA : aria-expanded, aria-haspopup, aria-controls
  • Labels accessibles : Support d'aria-label et aria-describedby
  • Focus visible : Indicateurs de focus clairs et contrastés
  • Contraste des couleurs : Ratios conformes WCAG AA
  • Tailles minimales : Respecte les tailles minimales de cible tactile
  • Mode sombre : Contraste adapté automatiquement
  • Contraste élevé : Support de prefers-contrast: high
  • Réduction d'animation : Respect de prefers-reduced-motion

🎯 Bonnes pratiques ​

vue
<!-- Dropdown avec label accessible -->
<SuDropdown 
  :options="options"
  label="Menu des actions"
  aria-label="Menu des actions disponibles"
  @select="handleSelect"
/>

<!-- Dropdown avec icône seule (OBLIGATOIRE: aria-label) -->
<SuDropdown 
  :options="options"
  :icon="EllipsisVerticalIcon"
  iconDisplay="only"
  aria-label="Menu des options"
  @select="handleSelect"
/>

<!-- Dropdown avec description -->
<SuDropdown 
  :options="options"
  label="Actions"
  aria-describedby="actions-help"
  @select="handleSelect"
/>
<div id="actions-help">Menu des actions disponibles pour cet élément</div>
ToucheAction
Entrée / EspaceSur le bouton : ouvrir le menu sur le premier item. Sur un item : l'activer
Flèche basSur le bouton : ouvrir le menu sur le premier item. Dans le menu : item suivant (en boucle)
Flèche hautSur le bouton : ouvrir le menu sur le dernier item. Dans le menu : item précédent (en boucle)
Début / FinPremier / dernier item
A-ZSaisie rapide : item suivant qui commence par ces lettres
ÉchapFermer le menu, focus rendu au bouton
TabFermer le menu et passer à l'élément suivant de la page

Exemples d'usage avancés ​

vue
<script setup>
const navigationOptions = [
  { value: 'all', label: 'Tous les produits', href: '/products' },
  { value: 'laptops', label: 'Ordinateurs portables', href: '/products/laptops' },
  { value: 'phones', label: 'Téléphones', href: '/products/phones' },
  { value: 'tablets', label: 'Tablettes', href: '/products/tablets' },
  { value: 'separator1', separator: true },
  { value: 'accessories', label: 'Accessoires', href: '/products/accessories' },
  { value: 'support', label: 'Support', href: '/support' }
]
</script>

<template>
  <nav>
    <div class="navbar">
      <h1>surgeUI</h1>
      
      <div class="nav-links">
        <a href="/">Accueil</a>
        
        <SuDropdown 
          :options="navigationOptions"
          label="Produits"
          variant="ghost"
          trigger="hover"
        />
        
        <a href="/about">À propos</a>
      </div>
    </div>
  </nav>
</template>
vue
<script setup>
import { ref } from 'vue'
import { UserIcon, CogIcon, DocumentIcon, ArrowUturnLeftIcon } from '@heroicons/vue/24/outline'

const user = ref({
  name: 'John Doe',
  email: 'john@example.com',
  avatar: '/avatar.jpg'
})

const userMenuOptions = [
  { value: 'profile', label: 'Mon profil', href: '/profile', icon: UserIcon },
  { value: 'settings', label: 'Paramètres', href: '/settings', icon: CogIcon },
  { value: 'separator1', separator: true },
  { value: 'docs', label: 'Documentation', href: '/docs', icon: DocumentIcon },
  { value: 'help', label: 'Centre d\'aide', href: 'https://help.example.com', target: '_blank', icon: DocumentIcon },
  { value: 'separator2', separator: true },
  { value: 'logout', label: 'Déconnexion', icon: ArrowUturnLeftIcon }
]

const handleUserAction = (option) => {
  if (option.value === 'logout') {
    // Logique de déconnexion
    console.log('Déconnexion...')
    // Redirection ou nettoyage de session
  }
  // Les autres options avec href naviguent automatiquement
}
</script>

<template>
  <div class="user-menu">
    <div class="user-info">
      <img :src="user.avatar" :alt="user.name" class="user-avatar" />
      <div class="user-details">
        <div class="user-name">{{ user.name }}</div>
        <div class="user-email">{{ user.email }}</div>
      </div>
    </div>
    
    <SuDropdown 
      :options="userMenuOptions"
      :icon="UserIcon"
      iconDisplay="only"
      aria-label="Menu utilisateur"
      variant="ghost"
      placement="bottom-end"
      @select="handleUserAction"
    />
  </div>
</template>
vue
<script setup>
import { ref } from 'vue'
import { PencilIcon, DocumentDuplicateIcon, ShareIcon, TrashIcon } from '@heroicons/vue/24/outline'

const selectedItem = ref(null)

const contextMenuOptions = [
  { value: 'edit', label: 'Modifier', icon: PencilIcon },
  { value: 'duplicate', label: 'Dupliquer', icon: DocumentDuplicateIcon },
  { value: 'separator1', separator: true },
  { value: 'share', label: 'Partager', icon: ShareIcon },
  { value: 'separator2', separator: true },
  { value: 'delete', label: 'Supprimer', icon: TrashIcon }
]

const handleContextAction = (option) => {
  switch (option.value) {
    case 'edit':
      console.log('Modifier l\'élément')
      break
    case 'duplicate':
      console.log('Dupliquer l\'élément')
      break
    case 'share':
      console.log('Partager l\'élément')
      break
    case 'delete':
      if (confirm('Êtes-vous sûr de vouloir supprimer cet élément ?')) {
        console.log('Supprimer l\'élément')
      }
      break
  }
}
</script>

<template>
  <div class="item-card">
    <div class="item-content">
      <h3>Document important</h3>
      <p>Description du document...</p>
    </div>
    
    <SuDropdown 
      :options="contextMenuOptions"
      :icon="EllipsisVerticalIcon"
      iconDisplay="only"
      aria-label="Actions pour ce document"
      variant="ghost"
      placement="bottom-end"
      @select="handleContextAction"
    />
  </div>
</template>
vue
<script setup>
const fileMenuOptions = [
  { value: 'new', label: 'Nouveau', icon: PlusIcon },
  { value: 'open', label: 'Ouvrir', icon: DocumentIcon },
  { value: 'separator1', separator: true },
  { value: 'save', label: 'Sauvegarder', icon: DocumentIcon, disabled: true },
  { value: 'save-as', label: 'Sauvegarder sous...', icon: DocumentIcon },
  { value: 'separator2', separator: true },
  { value: 'print', label: 'Imprimer', icon: DocumentIcon, disabled: true }
]
</script>

<template>
  <SuDropdown 
    :options="fileMenuOptions"
    label="Fichier"
    variant="outline"
    @select="handleFileAction"
  />
</template>

Cas d'usage courants ​

🎯 Menus d'actions ​

  • CRUD : Créer, modifier, supprimer
  • Fichiers : Nouveau, ouvrir, sauvegarder
  • Partage : Copier le lien, partager, exporter

👤 Menus utilisateur ​

  • Profil : Mon profil, paramètres
  • Navigation : Tableau de bord, projets
  • Système : Aide, déconnexion

📱 Navigation mobile ​

  • Menu hamburger : Navigation principale
  • Actions contextuelles : Actions spécifiques à un élément
  • Filtres : Options de tri et filtrage

🔧 Menus d'outils ​

  • Éditeur : Formatage, insertion, outils
  • Tableau de bord : Vues, exports, paramètres
  • Administration : Gestion, configuration

Bonnes pratiques ​

✅ À faire ​

  • Utiliser des icônes cohérentes pour les actions similaires
  • Grouper les options logiquement avec des séparateurs
  • Fournir un aria-label pour les dropdowns avec icône seule
  • Utiliser trigger="hover" pour la navigation principale
  • Limiter le nombre d'options (max 10-12 pour l'UX)

❌ À éviter ​

  • Imbriquer des dropdowns (utiliser des sous-menus si nécessaire)
  • Mélanger trop d'actions et de liens dans le même menu
  • Oublier les séparateurs pour les groupes logiques
  • Utiliser des descriptions trop longues
  • Placer des actions destructives sans confirmation

Performance ​

🚀 Optimisations intégrées ​

  • Lazy rendering : Le menu n'est rendu que quand ouvert
  • Focus trap : Gestion efficace du focus
  • Event listeners : Ajout/suppression automatique
  • Transitions CSS : Animations hardware-accelerated

📊 Impact minimal ​

  • CSS généré : ~3KB gzippé
  • JavaScript : Logique légère et optimisée
  • Accessibilité : Aucun compromis sur les performances
  • Responsive : Adaptation automatique sur mobile

Publié sous licence MIT.