Skip to main content

Chapitre 64 : Pseudo-classes de structure et d'état : :target et :empty

Utilisation de l'ancre URL et du contenu vide pour modifier le style

Dans l'arsenal du développeur CSS Senior, la capacité à créer des interfaces interactives sans dépendre systématiquement de JavaScript est un atout majeur pour la performance et l'accessibilité. Les pseudo-classes :target et :empty permettent de réagir à l'état de l'URL et à la structure même du DOM, offrant des solutions élégantes pour des patterns de navigation et de gestion de contenu dynamique.

La pseudo-classe :target

1. Quoi

La pseudo-classe :target représente un élément dont l'identifiant (id) correspond à l'identifiant de fragment (la partie après le #) présent dans l'URL actuelle.

C'est un sélecteur d'état basé sur l'historique de navigation. Lorsque l'URL change pour inclure un fragment (ex: index.html#section-1), le navigateur recherche l'élément possédant id="section-1". Si cet élément est trouvé, il devient la "cible" (target) et le sélecteur :target s'applique à lui.

2. Pourquoi

Dans un contexte professionnel, :target est utilisé pour créer des composants d'interface "sans JS" (Zero-JS) qui nécessitent un état mémorisable dans l'URL. Cela permet notamment :

  • Le Deep Linking : Envoyer un utilisateur vers une section précise d'une page avec un style mis en avant.
  • Les Modales et Onglets : Afficher un contenu spécifique en changeant simplement le hash de l'URL.
  • L'Accessibilité : Permettre un saut direct vers un contenu tout en signalant visuellement à l'utilisateur où il se trouve.

3. Comment

A. Syntaxe de base

/* L'élément avec l'ID correspondant au hash de l'URL sera mis en évidence */
.section:target {
background-color: yellow;
border: 2px solid orange;
}

B. Cas concret : Système de Modales sans JavaScript

Voici une implémentation robuste utilisant :target pour gérer l'ouverture et la fermeture de modales.


<nav>
<a href="#modal-login">Connexion</a>
<a href="#modal-signup">Inscription</a>
</nav>


<div id="modal-login" class="modal">
<div class="modal-content">
<h2>Connexion</h2>
<p>Veuillez entrer vos identifiants.</p>
<a href="#" class="close-btn">Fermer</a>
</div>
</div>

<div id="modal-signup" class="modal">
<div class="modal-content">
<h2>Inscription</h2>
<p>Créez votre compte dès maintenant.</p>
<a href="#" class="close-btn">Fermer</a>
</div>
</div>
/* Style de base de la modale : cachée par défaut */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;

/* On utilise opacity et visibility pour permettre des transitions fluides */
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
z-index: 1000;
}

/* État actif : quand l'ID de la modale est dans l'URL */
.modal:target {
opacity: 1;
visibility: visible;
}

.modal-content {
background: white;
padding: 2rem;
border-radius: 8px;
max-width: 500px;
position: relative;
}

.close-btn {
display: inline-block;
margin-top: 1rem;
color: red;
text-decoration: none;
font-weight: bold;
}

C. Limitations

  • Historique de navigation : Chaque clic sur un lien #id ajoute une entrée dans l'historique du navigateur. L'utilisateur devra cliquer plusieurs fois sur "Précédent" pour quitter la page s'il a ouvert plusieurs modales.
  • Sauts brusques : Par défaut, le navigateur "saute" vers l'élément cible. Pour éviter cela, on utilise scroll-behavior: smooth sur le html ou on gère le positionnement de la cible.
  • Unicité : Seul un élément peut être :target à la fois (puisqu'un ID doit être unique et l'URL n'a qu'un seul fragment).

4. Zone de Danger

Erreur commune : Utiliser :target pour des états complexes de formulaire ou des interactions qui ne doivent pas être partagées via URL.

Bonne pratique : Réserver :target pour des éléments de navigation ou des vues "feuilles" (modales, onglets) où le partage du lien direct apporte une valeur ajoutée à l'utilisateur.

Cycle de fonctionnement de la pseudo-classe :target


La pseudo-classe :empty

1. Quoi

La pseudo-classe :empty cible un élément qui ne contient absolument rien.

Pour être considéré comme "vide", l'élément ne doit avoir :

  • Aucun élément enfant.
  • Aucun nœud de texte (même un espace blanc ou un saut de ligne).
  • Aucun commentaire HTML.

2. Pourquoi

L'utilité de :empty réside dans le nettoyage automatique de l'interface utilisateur. Dans des applications modernes où le contenu est injecté dynamiquement (via des API ou des frameworks JS), il arrive souvent que des conteneurs soient rendus alors que les données sont absentes.

Plutôt que de gérer l'affichage via des conditions if dans le JavaScript (ce qui peut provoquer des "flashs" de contenu ou des problèmes de layout), on peut laisser le CSS masquer les éléments vides ou afficher un état "fallback".

3. Comment

A. Syntaxe de base

/* Masque tout élément div qui n'a aucun contenu */
div:empty {
display: none;
}

B. Cas concret : Gestion de notifications et placeholders

Imaginons un système de badges de notification où le badge ne doit apparaître que s'il y a un nombre à l'intérieur.

<div class="nav-item">
Messages

<span class="badge"></span>
</div>

<div class="nav-item">
Alertes

<span class="badge">3</span>
</div>
.badge {
background-color: red;
color: white;
border-radius: 50%;
padding: 2px 6px;
font-size: 0.8rem;
margin-left: 8px;
}

/* On masque le badge s'il n'y a pas de notification */
.badge:empty {
display: none;
}

/* Variante Senior : Utilisation avec ::before pour un placeholder */
.content-area:empty::before {
content: "Aucun contenu disponible pour le moment.";
color: #666;
font-style: italic;
}

C. Limitations

La limitation principale de :empty est sa stricte définition.


<div class="box"></div>


<div class="box"> </div>
<div class="box">
</div>
<div class="box"></div>

Pour pallier cela, le standard CSS a introduit :blank (encore peu supporté) ou l'utilisation de :not(:has(*)) dans les navigateurs modernes.

4. Zone de Danger

Erreur commune : Penser que :empty détecte un élément dont le texte est une chaîne vide "" injectée par JS. Si le JS insère un espace, :empty ne fonctionnera plus.

Bonne pratique : S'assurer que le moteur de rendu (ou le template engine) ne génère pas d'espaces blancs accidentels entre les balises ouvrantes et fermantes des éléments destinés à être ciblés par :empty.

Logique d'évaluation de :empty


Questions clés

1. Quelle est la différence fondamentale entre :target et un sélecteur d'ID classique (#id) ?

Découvrir la réponse

L'identifiant #id cible l'élément de manière statique, peu importe l'état de la page. :target est une pseudo-classe d'état : elle ne s'applique à l'élément que si son ID est présent dans l'URL actuelle. Si l'URL change, l'état :target est perdu.

2. Pourquoi un élément contenant uniquement un espace blanc n'est-il pas considéré comme :empty ?

Découvrir la réponse

Parce que :empty vérifie la présence de tout ChildNode. Un espace blanc est considéré comme un TextNode par le DOM. Pour le navigateur, un nœud de texte, même invisible, est un contenu.

3. Comment peut-on fermer un élément :target sans utiliser de JavaScript ?

Découvrir la réponse

En créant un lien dont la destination est simplement # (ou un autre ID). Cela modifie le fragment de l'URL, faisant perdre à l'élément actuel son état de cible.

4. Peut-on utiliser :empty pour masquer des éléments dont le contenu est généré par un pseudo-élément ::before ou ::after ?

Découvrir la réponse

Oui. Les pseudo-éléments ne sont pas considérés comme des enfants du DOM. Un élément peut être :empty tout en affichant du contenu via ::before ou ::after. C'est d'ailleurs une technique courante pour afficher des messages de "fallback".

5. Quel est l'impact de :target sur l'expérience utilisateur (UX) concernant le bouton "Précédent" du navigateur ?

Découvrir la réponse

Chaque changement de fragment (#) est enregistré dans l'historique. Si un utilisateur ouvre 5 modales via :target, il devra cliquer 5 fois sur "Précédent" pour revenir à la page précédente, ce qui peut être frustrant.


Mise en pratique

Exercice 1 : Reproduction guidée (Niveau Junior)

Objectif : Créer un système de navigation simple où cliquer sur un lien met en évidence la section correspondante.

  • Créez trois sections avec les IDs section1, section2, section3.
  • Créez trois liens pointant vers ces IDs.
  • Appliquez un style (bordure bleue et fond léger) aux sections uniquement lorsqu'elles sont la cible de l'URL.
Découvrir la solution commentée
<nav>
<a href="#section1">Aller à 1</a>
<a href="#section2">Aller à 2</a>
<a href="#section3">Aller à 3</a>
</nav>

<section id="section1" class="content">Contenu 1</section>
<section id="section2" class="content">Contenu 2</section>
<section id="section3" class="content">Contenu 3</section>
.content {
padding: 20px;
margin: 10px 0;
border: 1px solid #ccc;
transition: all 0.3s ease;
}

/* L'élément devient bleu quand il est ciblé par l'URL */
.content:target {
border: 2px solid blue;
background-color: #eef;
}

Exercice 2 : Adaptation (Niveau Intermédiaire)

Objectif : Refactorer un accordéon JavaScript vers une version 100% CSS utilisant :target.

Le code de départ ci-dessous fonctionne mais repose sur JavaScript. Votre mission est de le transformer pour qu'il n'utilise plus de JS du tout.

Code de départ (version JS à refactorer) :

<div class="accordion">
<div class="item">
<button class="trigger">Qu'est-ce que le CSS ?</button>
<div class="panel">
<p>Le CSS est un langage de style utilisé pour décrire la présentation d'un document HTML.</p>
</div>
</div>
<div class="item">
<button class="trigger">C'est quoi :target ?</button>
<div class="panel">
<p>C'est une pseudo-classe qui cible l'élément dont l'ID est dans l'URL.</p>
</div>
</div>
</div>
.trigger {
display: block;
width: 100%;
padding: 10px;
background: #eee;
border: none;
cursor: pointer;
text-align: left;
font-weight: bold;
}

.panel {
display: none;
padding: 10px;
background: #f9f9f9;
border: 1px solid #ddd;
}

/* L'ouverture dépend d'une classe ajoutée par JS */
.panel.active {
display: block;
}
document.querySelectorAll('.trigger').forEach(btn => {
btn.addEventListener('click', () => {
const panel = btn.nextElementSibling;
panel.classList.toggle('active');
});
});

Étapes de refactoring :

  1. Remplacez chaque <button class="trigger"> par un <a href="#faq1"> (et #faq2).
  2. Ajoutez un id sur chaque .panel correspondant.
  3. Remplacez .panel.active { display: block } par .panel:target { display: block }.
  4. Ajoutez un lien <a href="#">Fermer</a> à l'intérieur de chaque panel.
  5. Supprimez totalement le bloc <script>.
Découvrir la solution commentée
<div class="accordion">
<div class="item">
<a href="#faq1">Qu'est-ce que le CSS ?</a>
<div id="faq1" class="panel">
<p>Le CSS est un langage de style utilisé pour décrire la présentation d'un document HTML.</p>
<a href="#" class="close">Fermer</a>
</div>
</div>

<div class="item">
<a href="#faq2">C'est quoi :target ?</a>
<div id="faq2" class="panel">
<p>C'est une pseudo-classe qui cible l'élément dont l'ID est dans l'URL.</p>
<a href="#" class="close">Fermer</a>
</div>
</div>
</div>
.panel {
display: none; /* Masqué par défaut */
padding: 10px;
background: #f9f9f9;
border: 1px solid #ddd;
}

/* Affiche le panel quand il devient la cible */
.panel:target {
display: block;
}

.close {
font-size: 0.8rem;
color: grey;
text-decoration: underline;
}

Exercice 3 : Conception (Niveau Senior)

Objectif : Créer un composant de "Profil Utilisateur" dynamique.

  • Le composant doit afficher un avatar et un nom.
  • Il doit y avoir un champ "Bio" (un div).
  • Si la bio est vide (aucun contenu), le div doit disparaître complètement du layout.
  • Si la bio est vide, utilisez un pseudo-élément pour afficher un message discret "L'utilisateur n'a pas encore renseigné sa biographie" uniquement au survol du composant.
  • Ajoutez un bouton "Modifier" qui, via :target, affiche un formulaire de modification en superposition (overlay) sans JS.
Découvrir la solution commentée
<div class="profile-card">
<img src="avatar.jpg" alt="Avatar" class="avatar" />
<h3>Jean Dupont</h3>


<div class="bio"></div>




<a href="#edit-profile" class="edit-btn">Modifier le profil</a>
</div>


<div id="edit-profile" class="overlay">
<div class="form-container">
<h2>Modifier Profil</h2>
<input type="text" placeholder="Nouvelle bio..." />
<div class="actions">
<a href="#" class="save-btn">Sauvegarder</a>
<a href="#" class="cancel-btn">Annuler</a>
</div>
</div>
</div>
.profile-card {
position: relative;
width: 300px;
padding: 20px;
border: 1px solid #ddd;
text-align: center;
}

/* Gestion de la bio vide : on la supprime du layout par défaut */
.bio:empty {
display: none;
}

/*
Correction du bug : display:none empêche ::before de s'afficher.
On réactive l'élément lors du survol de la carte parente
pour que le message fallback ::before puisse apparaître.
*/
.profile-card:hover .bio:empty {
display: block;
}

/* Message de fallback visible uniquement au survol de la carte */
.profile-card:hover .bio:empty::before {
content: "L'utilisateur n'a pas encore renseigné sa biographie";
display: block;
font-size: 0.8rem;
color: #999;
font-style: italic;
}

/* Overlay de modification */
.overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
visibility: hidden;
pointer-events: none; /* Empêche les clics sur l'overlay invisible */
transition: opacity 0.3s ease, visibility 0.3s ease;
}

.overlay:target {
opacity: 1;
visibility: visible;
pointer-events: auto;
}

.form-container {
background: white;
padding: 2rem;
border-radius: 10px;
}