Les pseudo-éléments ::before et ::after avec la propriété content sont lus par défaut par les lecteurs d'écran.
Parfois c'est voulu, parfois ça pollue l'expérience utilisateur. CSS permet de différencier ce qui est visible de ce qui est annoncé grâce à une syntaxe avec barre oblique.
Peu connue, rarement utilisée, et pourtant indispensable.
Quand on écrit ça :
label::after {
content: " (optionnel)";
}
Le lecteur d'écran annonce : "Nom, parenthèse ouvrante optionnel parenthèse fermante"
Les parenthèses sont lues. C'est verbeux, inutile, et ça casse le rythme de navigation.
Autre exemple classique :
.badge::before {
content: "★";
}
Le lecteur d'écran va, au choix, ignorer le symbole ou lire « étoile » ou « astérisque ». Alors que ce visuel véhicule une information (favori, important, note), l'utilisateur de lecteur d'écran ne le saura jamais. Il perd complètement le sens.
La solution : le fallback content
CSS Generated Content Module Level 3 introduit une syntaxe avec barre oblique / pour séparer le contenu visuel du contenu pour les technologies d'assistance :
Syntaxe complète :
content: <visual-content> / <alt-content>;
<visual-content>: ce qui est affiché à l'écran/: séparateur<alt-content>: ce qui est annoncé par les lecteurs d'écran
La valeur alternative peut être une chaîne vide "" pour masquer complètement le contenu aux technologies d'assistance.
Cas d'usage
Les formulaires accessibles
Système complet avec i18n
Voici un système réel utilisé en production sur Kortic, gérant les champs optionnels, obligatoires, et la légende du formulaire en multilingue.
1. Champs optionnels (silencieux pour les AT)
/* Champs facultatifs
Structure : label[data-optional] ou label span[data-optional]
La valeur de l'attribut est utilisée comme texte
*/
[data-optional]:after {
content: " \20(" attr(data-optional) ")" / "";
text-transform: lowercase;
font-size: smaller;
}
HTML :
<label for="phone" data-optional="optionnel">
Téléphone
</label>
<input type="tel" id="phone" name="phone">
Résultat :
- Visuellement : Téléphone (optionnel)
- Lecteur d'écran : "Téléphone"
Logique : Si les champs obligatoires sont annoncés explicitement, l'absence d'annonce implique que le champ est optionnel. Pas besoin de le répéter.
2. Champs obligatoires avec symbole et multilingues
Note : pour les champs obligatoires, qu'ils soient précisés avec required ou aria-required="true", le fallback CSS peut rester vide puisque les AT annoncent déjà l'aspect obligatoire.
/* Champs obligatoires
Structure : label[data-marker] ~ input[required] ou [aria-required="true"]
data-marker="" : symbole par défaut \2731
data-marker="x" : symbole custom via attr()
*/
label[data-marker]:has(~ :is([required], [aria-required="true"])):after {
text-transform: lowercase;
font-size: smaller;
}
/* data-marker vide = \2731 par défaut */
label[data-marker=""]:has(~ :is([required], [aria-required="true"])) {
:root[lang="fr"] &:after { content: " \2731" / " élément obligatoire"; }
:root[lang="en"] &:after { content: " \2731" / " required"; }
:root[lang="es"] &:after { content: " \2731" / " elemento obligatorio"; }
:root[lang="de"] &:after { content: " \2731" / " Pflichtfeld"; }
:root[lang="it"] &:after { content: " \2731" / " elemento obbligatorio"; }
:root[lang="pt"] &:after { content: " \2731" / " elemento obrigatório"; }
}
/* data-marker avec valeur custom */
label[data-marker]:not([data-marker=""]):has(~ :is([required], [aria-required="true"])) {
:root[lang="fr"] &:after { content: " " attr(data-marker) / " élément obligatoire"; }
:root[lang="en"] &:after { content: " " attr(data-marker) / " required"; }
:root[lang="es"] &:after { content: " " attr(data-marker) / " elemento obligatorio"; }
:root[lang="de"] &:after { content: " " attr(data-marker) / " Pflichtfeld"; }
:root[lang="it"] &:after { content: " " attr(data-marker) / " elemento obbligatorio"; }
:root[lang="pt"] &:after { content: " " attr(data-marker) / " elemento obrigatório"; }
}
HTML :
<label for="email" data-marker="">
Adresse courriel
</label>
<input type="email" id="email" required>
Résultat :
- Visuellement : Adresse courriel ✱
- Lecteur d'écran (fr) : "Adresse courriel élément obligatoire"
Points techniques :
\2731: Astérisque lourd (Heavy Asterisk U+2731):has(~ :is([required], [aria-required="true"])): Sélecteur de fratrie pour détecter les champs obligatoires:root[lang="fr"]: Adaptation automatique selon la langue du document\20: Espace insécable échappé (évite le trim des minifieurs CSS)
3. Légende du formulaire (purement visuelle)
Il est d'usage de prévenir les utilisateurs que les champs marqués par ✱ sont nécessaires. Question accessibilité, c'est encore un souci pour la compréhension. On va donc en faire une structure totalement ignorée par une AT.
/* Légende du formulaire
Structure : <div aria-hidden="true" data-marker-legend></div>
data-marker-legend="" : symbole \2731 par défaut
data-marker-legend="x" : symbole custom via attr()
aria-hidden="true" sur l'élément HTML — pas de / "" nécessaire
*/
/* data-marker-legend vide = \2731 par défaut */
[data-marker-legend=""] {
:root[lang="fr"] &:after { content: "Les champs marqués \20\2731\20sont nécessaires."; }
:root[lang="en"] &:after { content: "Fields marked \20\2731\20are required."; }
:root[lang="es"] &:after { content: "Los campos marcados \20\2731\20son obligatorios."; }
:root[lang="de"] &:after { content: "Mit \20\2731\20markierte Felder sind Pflichtfelder."; }
:root[lang="it"] &:after { content: "I campi contrassegnati \20\2731\20sono obbligatori."; }
:root[lang="pt"] &:after { content: "Os campos marcados \20\2731\20são obrigatórios."; }
}
/* data-marker-legend avec valeur custom */
[data-marker-legend]:not([data-marker-legend=""]) {
:root[lang="fr"] &:after { content: "Les champs marqués " attr(data-marker-legend) "\20sont nécessaires."; }
:root[lang="en"] &:after { content: "Fields marked " attr(data-marker-legend) "\20are required."; }
:root[lang="es"] &:after { content: "Los campos marcados " attr(data-marker-legend) "\20son obligatorios."; }
:root[lang="de"] &:after { content: "Mit " attr(data-marker-legend) "\20markierte Felder sind Pflichtfelder."; }
:root[lang="it"] &:after { content: "I campi contrassegnati " attr(data-marker-legend) "\20sono obbligatori."; }
:root[lang="pt"] &:after { content: "Os campos marcados " attr(data-marker-legend) "\20são obrigatórios."; }
}
HTML :
<form>
<div aria-hidden="true" data-marker-legend=""></div>
<!-- Champs du formulaire -->
</form>
Résultat :
- Visuellement : Les champs marqués ✱ sont nécessaires.
- Lecteur d'écran : Ignoré (grâce à
aria-hidden="true"sur une balise<div>non sémantique)
Logique : Pas besoin de / car aria-hidden="true" masque déjà l'élément entier aux technologies d'assistance. L'utilisation d'un <div> au lieu d'un <p> évite de créer une structure sémantique inutile. Évite la redondance puisque chaque champ obligatoire est déjà annoncé individuellement.
Autres cas d'usage
Séparateurs visuels
.breadcrumb li:not(:last-child)::after {
content: " > " / "";
padding: 0 0.5rem;
color: rgb(153,153,153);
}
Résultat :
- Visuellement : Accueil > Blog > Article
- Lecteur d'écran : "Accueil Blog Article"
Support
Navigateurs
| Navigateur | Version minimale |
|---|---|
| Chrome | 119+ |
| Edge | 119+ |
| Safari | 17.4+ |
| Firefox | 128+ |
| Opera | 105+ |
État actuel : Supporté par tous les navigateurs modernes depuis juillet 2024.
Lecteurs d'écran
La restitution du fallback content varie selon les combinaisons navigateur/lecteur d'écran.
Compatibilité content: "x" / "alt" — groupé par lecteur
| Lecteur | Navigateurs |
|---|---|
| VoiceOver macOS | Safari, Chrome, Firefox |
| VoiceOver iOS | Safari, Chrome |
| JAWS 2025 | Chrome 140+, Edge |
| NVDA 2025 | Edge, Firefox 128+ — non restitué sur Chrome |
| Narrator | Edge |
| TalkBack | Chrome |
Points d'attention :
- NVDA + Chrome : Le fallback n'est pas restitué (bug connu). Utilisez Firefox ou Edge avec NVDA.
- JAWS : Support complet sur Chrome 140+ et Edge.
Recommandation : Toujours tester avec plusieurs combinaisons navigateur/lecteur d'écran, en particulier NVDA + Chrome qui peut poser problème.
Tester
Avec les DevTools
Chrome DevTools :
- Inspecter l'élément
- Panneau "Accessibility"
- Vérifier "Computed Properties" → "Name"
Firefox DevTools :
- Inspecter l'élément
- Onglet "Accessibility"
- Vérifier "Name" dans l'arbre d'accessibilité
Important : Les DevTools ne montrent pas toujours la restitution réelle par les lecteurs d'écran. Tester avec de vrais AT reste indispensable.
Exemple concret
Le système complet de gestion des formulaires décrit dans cet article est déployé sur la page de contact
Ressources
Spécifications :
Can I Use :
Articles de référence :
Conclusion
Le fallback content CSS est désormais universellement supporté par tous les navigateurs modernes depuis juillet 2024. C'est devenu un outil standard pour améliorer l'accessibilité des interfaces, à condition de connaître ses limites de compatibilité avec les lecteurs d'écran.
En 2026, vous pouvez utiliser cette syntaxe en production en connaissant les combinaisons navigateur/AT qui fonctionnent. Pour les applications critiques, doublez avec aria-label ou testez systématiquement vos principales combinaisons cibles.
Et surtout : testez avec de vrais utilisateurs et de vrais lecteurs d'écran. Les specs sont une chose, la réalité du terrain en est une autre.