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é

  • Rôles ARIA : role="button" pour le trigger, role="menu" pour le menu, role="menuitem" pour les options
  • Navigation au clavier : Support des touches fléchées, Entrée, Espace, Échap, Home/End
  • Focus trap : Gestion du focus dans le menu ouvert
  • Annonces vocales : Messages pour les lecteurs d'écran lors des sélections
  • É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
TabNaviguer vers/depuis le dropdown
Entrée / EspaceOuvrir/fermer le menu, sélectionner l'option focalisée
Flèche basOuvrir le menu ou naviguer vers l'option suivante
Flèche hautNaviguer vers l'option précédente
HomeAller à la première option
EndAller à la dernière option
ÉchapFermer le menu

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.