États Interactifs
Vue d'ensemble
Les états interactifs (hover, focus, disabled, active) donnent du feedback visuel à l'utilisateur et améliorent l'accessibilité.
Règles
- Toujours inclure hover pour les éléments cliquables
- Focus obligatoire pour l'accessibilité (navigation clavier)
- Transitions pour rendre les changements fluides
- États disabled clairs (opacity-60 + cursor-not-allowed)
- TOUJOURS inclure variante dark mode
États Standard
Hover
/* Boutons */
hover:bg-primary-700 dark:hover:bg-primary-600
hover:shadow-lg
/* Liens */
hover:text-primary-600 dark:hover:text-primary-400
hover:underline
/* Cards */
hover:shadow-lg
/* Lignes de tableau */
hover:bg-gray-100 dark:hover:bg-gray-700
Focus
/* Focus standard */
focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2
/* Focus visible (accessibilité) */
focus-visible:ring-2 focus-visible:ring-primary-500
Disabled/Loading
/* Désactivé */
opacity-60 cursor-not-allowed
/* Loading */
opacity-75 cursor-wait
Active
/* Bouton pressé */
active:scale-95
/* État sélectionné */
bg-primary-50 dark:bg-primary-900/20
Code de référence
Bouton avec États
<button
className="
btn btn-primary
hover:bg-primary-700 dark:hover:bg-primary-600
hover:shadow-lg
focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2
active:scale-95
disabled:opacity-60 disabled:cursor-not-allowed
transition-all duration-200
"
disabled={isLoading}
>
{isLoading ? 'Chargement...' : 'Valider'}
</button>
Lien avec États
<Link
to="/page"
className="
text-primary-600 dark:text-primary-400
hover:text-primary-700 dark:hover:text-primary-500
hover:underline
focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2
transition-colors duration-200
"
>
Voir plus
</Link>
Card Cliquable
<div
className="
card
cursor-pointer
hover:shadow-lg
hover:border-primary-300 dark:hover:border-primary-600
focus:outline-none focus:ring-2 focus:ring-primary-500
active:scale-[0.98]
transition-all duration-200
"
role="button"
tabIndex={0}
onClick={handleClick}
>
{/* Contenu */}
</div>
Input avec États
<input
className="
input
focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent
disabled:opacity-60 disabled:cursor-not-allowed disabled:bg-gray-100 dark:disabled:bg-gray-900
transition-all duration-200
"
disabled={isDisabled}
/>
Header de Section Collapsible
<div
className={`
p-6 cursor-pointer
hover:bg-gray-50 dark:hover:bg-gray-700/50
transition-colors duration-200
${isLoading ? 'opacity-60 cursor-not-allowed' : ''}
`}
onClick={() => {
if (!isLoading) {
setIsExpanded(!isExpanded)
}
}}
>
{/* Contenu */}
</div>
Exemples d'utilisation
Tableau avec Hover
<table>
<tbody>
{items.map((item) => (
<tr
key={item.id}
className="
hover:bg-gray-100 dark:hover:bg-gray-700
transition-colors duration-150
"
>
<td>{item.name}</td>
<td>{item.value}</td>
</tr>
))}
</tbody>
</table>
Bouton Loading
<button
className="btn btn-primary"
disabled={isLoading}
onClick={handleSubmit}
>
{isLoading ? (
<>
<Loader2 className="animate-spin" size={18} />
Chargement...
</>
) : (
<>
<Download size={18} />
Télécharger
</>
)}
</button>
Liste Sélectionnable
{items.map((item) => (
<div
key={item.id}
className={`
p-4 rounded-lg cursor-pointer
${
selectedId === item.id
? 'bg-primary-50 dark:bg-primary-900/20 border-primary-500'
: 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700'
}
hover:shadow-md
focus:outline-none focus:ring-2 focus:ring-primary-500
transition-all duration-200
`}
role="button"
tabIndex={0}
onClick={() => setSelectedId(item.id)}
onKeyPress={(e) => e.key === 'Enter' && setSelectedId(item.id)}
>
{item.name}
</div>
))}
Icône Cliquable
<button
className="
p-2 rounded-lg
hover:bg-gray-100 dark:hover:bg-gray-700
focus:outline-none focus:ring-2 focus:ring-primary-500
transition-colors duration-200
"
aria-label="Paramètres"
>
<Settings
className="text-gray-600 dark:text-gray-400"
size={20}
/>
</button>
Toggle Switch
<button
className={`
relative w-12 h-6 rounded-full
${enabled ? 'bg-primary-600' : 'bg-gray-300 dark:bg-gray-600'}
hover:shadow-md
focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2
transition-colors duration-200
`}
onClick={() => setEnabled(!enabled)}
>
<span
className={`
absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full
${enabled ? 'translate-x-6' : 'translate-x-0'}
transition-transform duration-200
`}
/>
</button>
Transitions
Durées Standard
/* Rapide (couleurs, opacité) */
duration-150
/* Standard (la plupart des interactions) */
duration-200
/* Lent (animations complexes) */
duration-300
Types de Transition
/* Tout */
transition-all duration-200
/* Couleurs uniquement */
transition-colors duration-200
/* Transform uniquement */
transition-transform duration-200
/* Shadow uniquement */
transition-shadow duration-200
À ne pas faire
Hover sans transition
// ❌ INCORRECT - Changement brutal
<button className="bg-primary-600 hover:bg-primary-700">
// ✅ CORRECT
<button className="bg-primary-600 hover:bg-primary-700 transition-colors duration-200">
Focus sans ring
// ❌ INCORRECT - Pas de feedback visuel pour navigation clavier
<button className="btn btn-primary">
// ✅ CORRECT
<button className="btn btn-primary focus:outline-none focus:ring-2 focus:ring-primary-500">
Disabled sans visual feedback
// ❌ INCORRECT
<button disabled>Désactivé</button>
// ✅ CORRECT
<button className="opacity-60 cursor-not-allowed" disabled>
Désactivé
</button>
Hover sans dark mode
// ❌ INCORRECT
<button className="hover:bg-gray-100">
// ✅ CORRECT
<button className="hover:bg-gray-100 dark:hover:bg-gray-700">
Cliquable sans cursor-pointer
// ❌ INCORRECT
<div onClick={handleClick}>
// ✅ CORRECT
<div className="cursor-pointer" onClick={handleClick}>
Accessibilité
Navigation Clavier
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyPress={(e) => e.key === 'Enter' && handleClick()}
className="focus:outline-none focus:ring-2 focus:ring-primary-500"
>
{/* Contenu */}
</div>
Focus Visible
<button
className="
focus:outline-none
focus-visible:ring-2 focus-visible:ring-primary-500
"
>
Cliquer
</button>
États ARIA
<button
aria-disabled={isLoading}
aria-pressed={isActive}
aria-busy={isLoading}
disabled={isLoading}
>
{/* Contenu */}
</button>
Améliorations UX 2025
États Conditionnels : Disabled vs Enabled
Depuis janvier 2025, les sections et composants utilisent un système d'états visuels clairs pour indiquer la disponibilité des données.
Pattern : Déverrouillage Progressif
Les sections se "déverrouillent" progressivement au fur et à mesure que les données sont chargées.
Code de référence
// État conditionnel basé sur la disponibilité des données
<div
className={`flex items-center justify-between p-6 ${
allLoadingComplete ? "cursor-pointer" : "cursor-not-allowed opacity-60"
}`}
onClick={() => {
if (allLoadingComplete) {
setIsExpanded(!isExpanded);
}
}}
>
{/* Contenu du header */}
</div>
États Visuels
Sans Données (Disabled)
// Classes pour état disabled
className="opacity-60 cursor-not-allowed"
Caractéristiques :
- Opacité réduite à 60%
- Curseur
not-allowedau survol - Non cliquable (condition dans onClick)
- Section pliée par défaut
Avec Données (Enabled)
// Classes pour état enabled
className="opacity-100 cursor-pointer"
Caractéristiques :
- Opacité normale (100%)
- Curseur
pointerau survol - Cliquable et interactive
- Peut être dépliée/repliée
Logique de Déverrouillage
// Exemple : Logique de chargement
const [allLoadingComplete, setAllLoadingComplete] = useState(false)
// Mise à jour de l'état quand toutes les tâches sont complètes
useEffect(() => {
const isComplete =
!dailyLoading && dailyComplete &&
!powerLoading && powerComplete &&
!detailLoading && detailComplete &&
!hcHpLoading && hcHpComplete
setAllLoadingComplete(isComplete)
}, [
dailyLoading, dailyComplete,
powerLoading, powerComplete,
detailLoading, detailComplete,
hcHpLoading, hcHpComplete
])
Condition de Rendu du Contenu
Le contenu ne s'affiche que si DEUX conditions sont vraies :
{isExpanded && allLoadingComplete && (
<div className="px-6 pb-6 space-y-8">
{/* Contenu de la section */}
</div>
)}
- Section dépliée :
isExpanded = true - Données chargées :
allLoadingComplete = true
Bénéfices UX
- Feedback visuel clair : L'utilisateur sait immédiatement quelles sections sont disponibles
- Pas de frustration : Impossible de cliquer sur des sections vides
- Guidance naturelle : Les états grisés guident vers les sections prêtes
- Déverrouillage progressif : Sensation de progression pendant le chargement
- Cohérence : Pattern uniforme dans toute l'application
Pages Utilisant ce Pattern
/consumption- 4 sections avec déverrouillage progressif/simulator- Section "Comparaison des offres"- Toute page avec sections collapsibles dépendantes de données
Voir aussi
- 03 - Sections - Pour les sections always visible
- 13 - Loading - Pour la progression du chargement
Voir aussi
- 07 - Boutons - Pour les états de boutons
- 08 - Cards - Pour les états de cards
- 09 - Formulaires - Pour les états de formulaire
- 13 - Loading - Pour les états de chargement