Chapitre 67 : Ciblage des ancres et navigation interne avec :target
Utilisation de la pseudo-classe :target pour le routage CSS pur
Le développement moderne tend à déléguer toute la gestion d'état de l'interface utilisateur (UI) à des frameworks JavaScript. Cependant, pour un ingénieur CSS senior, savoir exploiter les capacités natives du navigateur est un atout majeur en termes de performance, de résilience et de SEO. La pseudo-classe :target permet d'implémenter des mécanismes de navigation interne et de gestion d'état (comme des modales ou des onglets) sans écrire une seule ligne de script.
Le Routage CSS avec :target
1. Quoi
La pseudo-classe :target représente un élément dont l'identifiant (id) correspond exactement à la partie fragment (l'ancre) de l'URL actuelle.
Par exemple, si l'URL du navigateur est https://monsite.com/page#section-contact, l'élément HTML possédant l'attribut id="section-contact" sera considéré comme étant dans l'état :target.
C'est un sélecteur d'état dynamique : dès que l'URL change (via un lien <a> ou une manipulation de l'historique), le navigateur recalcule quel élément est la cible et met à jour le rendu CSS instantanément.
2. Pourquoi
L'intérêt d'utiliser :target dans un projet professionnel réside dans plusieurs piliers :
- Performance Critique : Le moteur de rendu CSS traite le changement d'état
:targetbeaucoup plus rapidement qu'un cycle de rendu JavaScript (Virtual DOM, réconciliation, etc.). - Deep Linking (Lien Profond) : Contrairement à un état géré en mémoire JS, l'état
:targetest inscrit dans l'URL. Un utilisateur peut envoyer un lien spécifique à un collègue, et celui-ci arrivera directement sur la modale ouverte ou l'onglet actif. - Résilience (Progressive Enhancement) : L'interface reste fonctionnelle même si le JavaScript est désactivé ou échoue à charger.
- Réduction de la Complexité : Pour des composants simples (FAQ, Modales de confirmation, Tabs), supprimer la logique JS réduit la surface d'attaque pour les bugs et allège le bundle final.
3. Comment
A. Syntaxe de base
Le principe repose sur le couple Lien (href="#id") → Cible (id="id").
/* L'élément est invisible par défaut */
.content-box {
display: none;
padding: 20px;
background: #f0f0f0;
}
/* L'élément devient visible uniquement quand il est la cible de l'URL */
.content-box:target {
display: block;
border: 2px solid blue;
}
B. Cas concret : Système de Modales et Navigation
Dans une architecture senior, on ne se contente pas de display: none. On utilise des transitions pour l'expérience utilisateur et on gère l'overlay.
<nav>
<a href="#home">Accueil</a>
<a href="#settings">Paramètres</a>
<a href="#profile">Profil</a>
</nav>
<div id="settings" class="modal">
<div class="modal-content">
<h2>Paramètres</h2>
<p>Gérez vos préférences ici.</p>
<a href="#" class="close-btn">Fermer</a>
</div>
</div>
<div id="profile" class="modal">
<div class="modal-content">
<h2>Profil</h2>
<p>Informations utilisateur.</p>
<a href="#" class="close-btn">Fermer</a>
</div>
</div>
/* Architecture de la modale */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
/* État initial : invisible et non interactif */
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
z-index: 1000;
}
/* Activation via :target */
.modal:target {
opacity: 1;
pointer-events: auto;
}
.modal-content {
background: white;
padding: 2rem;
border-radius: 8px;
max-width: 500px;
width: 90%;
transform: translateY(-20px);
transition: transform 0.3s ease;
}
.modal:target .modal-content {
transform: translateY(0);
}
.close-btn {
display: inline-block;
margin-top: 1rem;
color: red;
text-decoration: none;
font-weight: bold;
}
C. Limitations
Bien que puissant, le routage CSS pur présente des limites structurelles :
- Pollution de l'historique : Chaque clic sur un lien d'ancre ajoute une entrée dans l'historique du navigateur. Si l'utilisateur ouvre 10 modales, il devra cliquer 10 fois sur "Précédent" pour quitter la page.
- Un seul
:targetà la fois : L'URL ne peut contenir qu'un seul fragment. Il est donc impossible d'avoir deux modales ouvertes simultanément ou d'activer plusieurs états indépendants via:target. - Accessibilité (a11y) : Le focus clavier ne se déplace pas automatiquement à l'intérieur de la modale lors de l'activation de
:target. Un utilisateur de lecteur d'écran pourrait rester bloqué sur le lien déclencheur. - Sauts de page : Par défaut, le navigateur tente de scroller l'élément
:targeten haut de la vue. Pour des modales, cela peut provoquer un saut visuel désagréable si la modale est placée bas dans le DOM.
4. Zone de Danger
❌ Erreur commune : Utiliser :target pour des états complexes de formulaire.
Tenter de gérer un formulaire multi-étapes uniquement avec :target rend la validation et la soumission extrêmement complexes car l'état n'est pas persisté ailleurs que dans l'URL.
✅ Bonne pratique : Combiner :target avec scroll-behavior: smooth.
Pour une navigation interne (ancres de page), ajoutez html { scroll-behavior: smooth; } pour éviter le saut brusque et améliorer l'UX.
❌ Erreur commune : Oublier le lien de fermeture.
Un utilisateur peut se sentir piégé dans une modale :target s'il n'y a pas de lien href="#" ou href="/page" pour réinitialiser l'URL.
✅ Bonne pratique : Utiliser visibility: hidden plutôt que display: none pour les animations.
display: none ne peut pas être animé. Pour des transitions fluides d'opacité, utilisez le couple opacity / visibility ou pointer-events.
Cycle de vie de la pseudo-classe :target
Questions clés
1. Quelle est la différence fondamentale entre :target et :checked (utilisé avec des checkboxes invisibles) pour créer des onglets ?
Découvrir la réponse
:target utilise l'URL du navigateur, ce qui permet le partage de liens directs vers un contenu spécifique (Deep Linking) et l'utilisation du bouton "Précédent" du navigateur. :checked gère un état local au DOM qui disparaît au rafraîchissement de la page et ne modifie pas l'URL.
2. Comment empêcher le navigateur de scroller brusquement vers l'élément :target ?
Découvrir la réponse
Il existe plusieurs approches. La plus propre consiste à utiliser scroll-behavior: smooth sur l'élément html. Pour un blocage total (cas des modales), on peut placer la modale en position: fixed ou absolute avec un top: 0, ce qui neutralise l'effet de scroll puisque l'élément est sorti du flux normal.
3. Peut-on styliser le lien qui a déclenché l'état :target ?
Découvrir la réponse
Non, pas directement. :target s'applique à l'élément qui est la cible, pas à l'élément qui pointe vers la cible. Pour styliser le lien actif, il faudra soit utiliser du JavaScript, soit s'appuyer sur des structures de navigation où le lien est un frère/parent de la cible et utiliser des sélecteurs combinateurs (bien que cela soit très limité avec :target).
4. Quel est l'impact de :target sur le SEO ?
Découvrir la réponse
L'impact est positif. Les moteurs de recherche indexent les ancres. En structurant votre page avec des IDs clairs et des liens internes, vous aidez Google à générer des "Sitelinks" (liens directs vers des sections de votre page dans les résultats de recherche).
Mise en pratique
Exercice 1 : Reproduction guidée - La Modale de Bienvenue
Objectif : Créer une modale simple qui s'affiche à l'ouverture d'un lien et se ferme via un bouton.
- Créez un lien "Ouvrir les conditions".
- Créez une div avec l'id
conditionsqui est cachée par défaut. - Utilisez
:targetpour l'afficher avec un fond semi-transparent. - Ajoutez un lien "Fermer" qui réinitialise l'URL.
Découvrir la solution commentée
<a href="#conditions">Lire les conditions d'utilisation</a>
<div id="conditions" class="modal-overlay">
<div class="modal-card">
<h2>Conditions d'utilisation</h2>
<p>L'utilisation de ce site implique l'acceptation de...</p>
<a href="#" class="close">Fermer</a>
</div>
</div>
/* CSS */
.modal-overlay {
position: fixed;
inset: 0; /* Raccourci pour top/right/bottom/left: 0 */
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
/* Masqué par défaut */
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
/* Activation quand l'ID est dans l'URL */
.modal-overlay:target {
opacity: 1;
pointer-events: auto;
}
.modal-card {
background: white;
padding: 2rem;
border-radius: 10px;
max-width: 400px;
}
.close {
color: blue;
text-decoration: underline;
cursor: pointer;
}
Exercice 2 : Adaptation - Système d'Onglets (Tabs)
Objectif : Transformer le concept de modale en un système d'onglets où seul le contenu ciblé est visible.
- Créez une liste de 3 liens (Onglet 1, 2, 3).
- Créez 3 sections de contenu correspondantes.
- Faites en sorte que seul l'onglet actif soit visible.
- Bonus : Stylisez le lien actif (Astuce : utilisez une structure où les liens sont dans un conteneur et le contenu dans un autre, et jouez sur la visibilité).
Code de départ (tous les panneaux sont visibles en même temps) :
<div class="tabs-container">
<nav class="tabs-nav">
<a href="#tab1">Description</a>
<a href="#tab2">Spécifications</a>
<a href="#tab3">Avis</a>
</nav>
<div class="tabs-content">
<section id="tab1" class="tab-pane">
<h3>Description</h3>
<p>Ceci est le produit X, révolutionnaire...</p>
</section>
<section id="tab2" class="tab-pane">
<h3>Spécifications</h3>
<p>Poids : 1kg, Taille : 20cm...</p>
</section>
<section id="tab3" class="tab-pane">
<h3>Avis</h3>
<p>⭐⭐⭐⭐⭐ - Super produit !</p>
</section>
</div>
</div>
/* CSS de départ : tous les panneaux sont affichés en même temps */
.tabs-nav {
display: flex;
gap: 1rem;
border-bottom: 2px solid #ccc;
padding-bottom: 0.5rem;
}
.tabs-nav a {
text-decoration: none;
padding: 0.5rem 1rem;
color: #333;
border-radius: 4px 4px 0 0;
}
.tab-pane {
/* ❌ Tous les panneaux sont toujours visibles */
display: block;
padding: 1.5rem;
border: 1px solid #ccc;
border-top: none;
}
Étapes de correction :
- Remplacez
display: blockpardisplay: nonesur.tab-pane. - Ajoutez
.tab-pane:target { display: block; }pour n'afficher que le panneau ciblé. - (Bonus) Stylisez le lien actif via
.tabs-nav a:target— mais attention ::targets'applique à la cible, pas au lien. Cherchez comment utiliser la structure du DOM pour contourner cette limitation.
Découvrir la solution commentée
<div class="tabs-container">
<nav class="tabs-nav">
<a href="#tab1">Description</a>
<a href="#tab2">Spécifications</a>
<a href="#tab3">Avis</a>
</nav>
<div class="tabs-content">
<section id="tab1" class="tab-pane">
<h3>Description</h3>
<p>Ceci est le produit X, révolutionnaire...</p>
</section>
<section id="tab2" class="tab-pane">
<h3>Spécifications</h3>
<p>Poids : 1kg, Taille : 20cm...</p>
</section>
<section id="tab3" class="tab-pane">
<h3>Avis</h3>
<p>⭐⭐⭐⭐⭐ - Super produit !</p>
</section>
</div>
</div>
.tab-pane {
display: none; /* Cache tous les panneaux */
padding: 20px;
border: 1px solid #ccc;
}
/* Affiche uniquement le panneau qui est la cible de l'URL */
.tab-pane:target {
display: block;
}
/* Pour éviter que la page soit vide au chargement initial */
/* On peut utiliser un ID par défaut ou un peu de JS,
mais en CSS pur, le premier élément reste caché
tant qu'on ne clique pas. */
Exercice 3 : Conception - Le "Mini-SPA" sans JS
Objectif : Concevoir une interface de type application (Sidebar + Contenu principal) où la navigation se fait entièrement via :target.
Contraintes :
- La sidebar doit être fixe à gauche.
- Le contenu principal doit être à droite.
- L'utilisateur doit pouvoir naviguer entre "Dashboard", "Messages", et "Paramètres".
- Implémentez une animation de glissement (slide) pour le contenu qui arrive.
- Gérez l'accessibilité minimale : assurez-vous que le contenu est lisible et que les liens sont explicites.
Propriétés CSS attendues :
| Sélecteur | Propriétés clés | Rôle |
|---|---|---|
.app-shell | display: grid, grid-template-columns: 250px 1fr, height: 100vh | Structure en deux colonnes |
.sidebar | background, color, padding | Colonne de navigation |
.sidebar nav a | display: block, color, text-decoration: none, transition | Liens de navigation |
.main-viewport | position: relative, overflow: hidden, background | Zone de contenu principale |
.page, .default-view | position: absolute, inset: 0, padding, opacity: 0, transform: translateX(50px), transition, pointer-events: none, background-color | Toutes les vues (cachées par défaut) |
.default-view | opacity: 1, transform: translateX(0), pointer-events: auto | Vue affichée au chargement initial |
.page:target | opacity: 1, transform: translateX(0), pointer-events: auto, z-index: 2 | Vue active |
.main-viewport:has(.page:target) .default-view | opacity: 0, pointer-events: none | Masque la vue par défaut quand une page est active |
Découvrir la solution commentée
<div class="app-shell">
<aside class="sidebar">
<h2>My App</h2>
<nav>
<a href="#dash">Dashboard</a>
<a href="#msg">Messages</a>
<a href="#set">Settings</a>
</nav>
</aside>
<main class="main-viewport">
<div class="default-view">
<h1>Bienvenue</h1>
<p>Veuillez sélectionner une option dans le menu.</p>
</div>
<div id="dash" class="page">
<h1>Dashboard</h1>
<p>Statistiques en temps réel...</p>
</div>
<div id="msg" class="page">
<h1>Messages</h1>
<p>Vous avez 3 nouveaux messages.</p>
</div>
<div id="set" class="page">
<h1>Settings</h1>
<p>Configurez votre compte.</p>
</div>
</main>
</div>
.app-shell {
display: grid;
grid-template-columns: 250px 1fr;
height: 100vh;
font-family: sans-serif;
}
.sidebar {
background: #2c3e50;
color: white;
padding: 1rem;
}
.sidebar nav a {
display: block;
color: #bdc3c7;
padding: 10px 0;
text-decoration: none;
transition: color 0.2s;
}
.sidebar nav a:hover {
color: white;
}
.main-viewport {
position: relative;
overflow: hidden;
background: #ecf0f1;
}
.page, .default-view {
position: absolute;
inset: 0;
padding: 2rem;
background-color: #ecf0f1; /* Fond explicite pour éviter la transparence */
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s;
opacity: 0;
transform: translateX(50px);
pointer-events: none;
}
/* Vue visible au chargement : aucune page n'est encore ciblée */
.default-view {
opacity: 1;
transform: translateX(0);
pointer-events: auto;
}
/* Quand une page est ciblée, on l'affiche par-dessus */
.page:target {
opacity: 1;
transform: translateX(0);
pointer-events: auto;
z-index: 2;
}
/*
Correction du bug de superposition :
On masque la vue par défaut dès qu'une page est ciblée.
Sans cette règle, "Bienvenue" reste visible en dessous des pages.
*/
.main-viewport:has(.page:target) .default-view {
opacity: 0;
pointer-events: none;
}