Manipuler les attributs HTML class et style
Présentation
Cette page traite de la manipulation de l'apparence d'une interface à l'aide de JavaScript, en s'appuyant sur deux attributs HTML très utilisés class et style.
En JavaScript, ces attributs ne sont pas modifiés directement sous forme de texte. Le navigateur fournit des propriétés dédiées permettant de les manipuler de manière structurée, fiable et cohérente avec le fonctionnement du DOM.
La propriété JavaScript classList permet de gérer la valeur de l'attribut HTML class. Elle sert à ajouter, retirer ou basculer des classes CSS afin d'activer ou désactiver des états visuels déjà définis dans les feuilles de style. Cette approche favorise une séparation claire entre la logique JavaScript et la présentation CSS.
La propriété JavaScript style permet de modifier la valeur de l'attribut HTML style. Elle est utilisée lorsque certaines valeurs doivent être calculées ou ajustées dynamiquement par le script, par exemple en fonction d'une interaction, d'une taille d'écran ou d'un état interne de l'application.
Manipuler l'attribut HTML class
Comme n'importe quel autre attribut HTML, l'attribut class peut être lu ou modifié avec getAttribute(), setAttribute() ou removeAttribute().
Cependant, un élément peut posséder plusieurs classes en même temps, et ces classes sont souvent ajoutées, retirées ou testées au fil des interactions utilisateur. Manipuler cet attribut comme une simple chaîne de caractères devient vite source d'erreurs.
Pour cette raison, le DOM fournit une API dédiée classList, conçue pour travailler classe par classe, sans écraser les autres.
Ajouter une classe avec classList.add()
La méthode add() permet d'ajouter une ou plusieurs classes à un élément. Si la classe est déjà présente, rien ne change.
Code HTML :
<section id="panneau" class="panneau">
<!--Contenu du panneau-->
</section>
Code Javascript :
const panneau = document.querySelector('#panneau');
panneau.classList.add('ouvert');
Code HTML après l'exécution du script :
<section id="panneau" class="panneau ouvert">
<!--Contenu du panneau-->
</section>
Retirer une classe avec classList.remove()
La méthode remove() permet de retirer une classe existante. Si la classe n'est pas présente, l'appel est ignoré.
Code HTML :
<section id="panneau" class="panneau ouvert">
<!--Contenu du panneau-->
</section>
Code Javascript :
const panneau = document.querySelector('#panneau');
panneau.classList.remove('ouvert');
Code HTML après l'exécution du script :
<section id="panneau" class="panneau">
<!--Contenu du panneau-->
</section>
Vérifier la présence d'une classe avec classList.contains()
La méthode contains() permet de tester si une classe est présente sur un élément. Elle renvoie un booléen.
Code HTML :
<section id="panneau" class="panneau ouvert">
<!--Contenu du panneau-->
</section>
Code Javascript :
const panneau = document.querySelector('#panneau');
const estOuvert = panneau.classList.contains('ouvert');
console.log(estOuvert); // Affiche : true
Basculer une classe avec classList.toggle()
La méthode toggle() permet de basculer une classe CSS. Si la classe n'est pas présente sur l'élément, elle est ajoutée. Si elle est déjà présente, elle est retirée. La méthode renvoie true lorsque la classe est ajoutée, et false lorsqu'elle est retirée.
Code HTML :
<section id="panneau" class="panneau">
<!--Contenu du panneau-->
</section>
Code Javascript :
const panneau = document.querySelector('#panneau');
const estMaintenantOuvert = panneau.classList.toggle('ouvert');
console.log(estMaintenantOuvert); // Affiche : true
Code HTML après l'exécution du script :
<section id="panneau" class="panneau ouvert">
<!--Contenu du panneau-->
</section>
Deuxième paramètre optionnel
La méthode toggle() accepte un deuxième paramètre facultatif (un booléen), souvent appelé force. Ce paramètre ne "bascule" plus, il impose l'état de la classe. Si ce paramètre vaut true, la classe est ajoutée. S'il vaut false, la classe est retirée. La valeur de retour correspond aussi à l'état final true si la classe est présente, false sinon.Ce deuxième paramètre s'utilise quand on connaît déjà le résultat attendu. Exemple courant, synchroniser l'affichage avec une variable, une préférence, un état applicatif, ou un événement comme change sur une case à cocher.
Code HTML :
<section id="panneau" class="panneau">
<!--Contenu du panneau-->
</section>
<label>
<input type="checkbox" id="checkboxPanneau">
Ouvrir le panneau
</label>
Code Javascript :
const panneauElem = document.querySelector('#panneau');
const checkboxPanneauElem = document.querySelector('#checkboxPanneau');
// Imposer l'état de la classe CSS "ouvert" selon l'état du checkbox.
checkboxPanneauElem.addEventListener('change', () =>
{
// Récupérer l'état de la checkbox (true si activée, false si désactivée)
const doitEtreOuvert = checkboxPanneauElem.checked;
// Forcer l'ajout ou la suppression de la classe CSS "ouvert" en fonction de la valeur de doitEtreOuvert.
// Si la constante doitEtreOuvert vaut true alors la classe CSS "ouvert" sera ajoutée à panneauElem,
// si la constante doitEtreOuvert vaut false alors la classe CSS "ouvert" sera retirée de panneauElem.
const estOuvert = panneauElem.classList.toggle('ouvert', doitEtreOuvert);
});
Code HTML après l'exécution du script si la case est cochée :
<section id="panneau" class="panneau ouvert">
<!--Contenu du panneau-->
</section>
Code HTML après l'exécution du script si la case est décochée :
<section id="panneau" class="panneau">
<!--Contenu du panneau-->
</section>
Approche historique avec className
Avant l'apparition de classList, la manipulation des classes CSS se faisait via la propriété className. Cette propriété permet d'accéder à l'attribut HTML class sous forme d'une chaîne de caractères.
Dans un premier temps, cette approche est parfaitement lisible et fonctionne bien pour consulter les classes appliquées à un élément.
Code HTML :
<section id="panneau" class="panneau">
Contenu du panneau
</section>
Code Javascript :
const panneau = document.querySelector('#panneau');
// Lire toutes les classes CSS sous forme de texte.
console.log(panneau.className); // "panneau"
Le navigateur renvoie ici une simple chaîne de caractères. Tant que l'objectif consiste uniquement à lire cette information, il n'y a aucune difficulté particulière.
Les choses se compliquent dès que l'on souhaite modifier cette liste de classes. Imaginons que l'on veuille ajouter une classe CSS ouvert à une balise qui possède déjà des classes existantes.
Approche naïve avec className :
// Ajouter une classe en reconstruisant la chaîne.
panneau.className = panneau.className + ' ouvert';
Cette écriture fonctionne dans un cas simple, mais elle introduit plusieurs problèmes potentiels.
- Elle repose sur la gestion manuelle des espaces. Un oubli ou un double espace peut produire une classe invalide.
- Elle ne vérifie pas si la classe existe déjà, ce qui peut entraîner des doublons.
- Toute la logique repose sur une chaîne de caractères, ce qui rend le code plus fragile et plus difficile à faire évoluer.
Plus l'interface devient complexe, plus ce type de manipulation devient source d'erreurs et de comportements imprévisibles.
C'est pour résoudre ces limitations que le DOM fournit une API dédiée, classList. Elle permet de manipuler les classes une par une, sans avoir à analyser ou reconstruire une chaîne.
Même opération avec classList :
// Ajouter la classe sans se soucier des autres.
panneau.classList.add('ouvert');
Avec cette approche, les classes existantes sont préservées, les doublons sont évités, et l'intention du code est immédiatement lisible.
C'est pourquoi, en pratique, classList est systématiquement privilégiée dès qu'il s'agit d'ajouter, retirer ou tester une classe CSS.
Manipuler l'attribut HTML style
L'attribut style permet de définir des styles directement sur une balise. Cette approche est généralement déconseillée pour gérer des états, car elle mélange logique JavaScript et présentation visuelle.
Lorsqu'un changement correspond à un état de l'interface ouvert, actif, erreur, sélectionné il est préférable d'utiliser des classes CSS via classList.
Cependant, le style inline reste utile lorsque la valeur dépend d'un calcul dynamique, par exemple une taille mesurée ou une position calculée.
Modifier une propriété avec element.style
Code HTML :
<p id="message">Profil mis à jour</p>
Code Javascript :
const message = document.querySelector('#message');
// Modifier la propriété CSS "color".
message.style.color = 'green';
Correspondance CSS et JavaScript
Les propriétés CSS sont écrites en kebab-case. En JavaScript, elles sont converties en camelCase.
// Modifier la propriété CSS "background-color".
panneau.style.backgroundColor = 'lightblue';
// Modifier la propriété CSS "font-size".
panneau.style.fontSize = '18px';
Exercices
Exo-01-dom-class-style
Cet exercice porte sur l'attribut HTML class, la méthode classList.toggle(), et la création d'un petit module utilitaire réutilisable.
Attendu
Au clic sur le bouton !, les styles doivent alterner entre normal et "enfoncé rouge" (active et alerte). Au clic sur le bouton ¿, le style doit alterner uniquement entre normal et "enfoncé" (active). Chaque bouton ne doit modifier que son propre état.
Structure
Créer un dossier nommé Exo-01-dom-class-style, puis organiser les fichiers et dossiers en respectant l'arborescence suivante :
Exo-01-dom-class-style/
|-- index.html
|-- assets/
| └── css/
| └── style.css
└── js/
|-- app.js
└── modules/
└── alternerClasseCss.js
Fichiers fournis
Copier les fichiers suivants à l'identique.
index.html
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JS: DOM Attributs "class" et "style" - Exo 01</title>
<link rel="stylesheet" href="./assets/css/style.css">
</head>
<body>
<main>
<h1>JS: DOM Attributs "class" et "style" - Exo 01</h1>
<button id="btn-exclamation"><span>!</span></button>
<button id="btn-interrogation"><span>¿</span></button>
</main>
<script src="./js/app.js" type="module"></script>
</body>
</html>
assets/css/style.css
/* VARIABLES */
:root {
/* Couleurs Sémantiques */
--fond-principal: #0e1319;
--texte-principal: #e7e1ca;
--accent: orange;
--alerte: rgb(148, 16, 13);
/* Échelle d'espacement */
--espace-p: 1vw;
--espace-m: 2rem;
--espace-g: 8rem;
/* Typographie */
--police-corps: 'Arial', sans-serif;
/* Élévations (Neumorphisme) */
--ombre-bouton-relief:
0 10px 4px var(--fond-principal),
10px 0 4px var(--texte-principal),
0 -10px 4px #999a9d,
-10px 0 4px #7a7b81;
--ombre-bouton-enfonce:
0 -10px 4px var(--fond-principal),
10px 0 4px var(--texte-principal),
0 10px 4px #999a9d,
-10px 0 4px #7a7b81;
}
/* RESET */
html, body, span, h1, button {
margin: 0;
padding: 0;
border: 0;
box-sizing: border-box;
}
/* BASES */
html {
color: var(--texte-principal);
}
body {
min-height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
background-color: var(--fond-principal);
font-family: var(--police-corps);
}
main {
padding: var(--espace-m) 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
width: 100%;
}
h1 {
margin-bottom: var(--espace-m);
text-align: center;
}
/* BOUTONS */
button {
border-radius: 50%;
width: 9rem;
height: 9rem;
background-color: var(--fond-principal);
box-shadow: var(--ombre-bouton-relief);
cursor: pointer;
}
button > span {
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
font-size: 5rem;
font-weight: bold;
text-transform: uppercase;
background: radial-gradient(var(--texte-principal), var(--accent) 50%, var(--fond-principal) 80%);
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
/* Bouton 2 */
button:nth-of-type(2) {
transform: translate3d(var(--espace-g), -4rem, 0) scale3d(0.5, 0.5, 1);
}
/* Bouton activation */
button.active > span {
transform: scale3d(0.9, 0.9, 1);
box-shadow: var(--ombre-bouton-enfonce);
}
/* Bouton alerte */
button.alerte > span {
background: radial-gradient(var(--texte-principal), var(--alerte) 50%, var(--fond-principal) 80%);
}
Instructions
Le travail se fait dans js/app.js et js/modules/alternerClasseCss.js. Le résultat attendu doit être visible dans la page.
Étape 01
- Créer le fichier js/modules/alternerClasseCss.js. Y créer puis exporter une fonction nommée alternerClasseCss.
- La fonction doit recevoir un élément HTML en premier argument, stocké dans un paramètre nommé elem. Les classes CSS à basculer doivent être reçues dans un paramètre de reste nommé ...nomClasses. Les trois points ... indiquent que la fonction peut recevoir un nombre variable d'arguments supplémentaires. Tous les arguments placés après elem sont alors regroupés automatiquement dans un tableau.
- Concrètement, si la fonction est appelée comme ceci alternerClasseCss(bouton, 'active', 'alerte'), alors nomClasses vaut ['active', 'alerte']. Si la fonction est appelée avec une seule classe, par exemple alternerClasseCss(bouton, 'active'), alors nomClasses vaut ['active']. Ce mécanisme évite d'écrire plusieurs fonctions différentes selon qu'il y a une ou plusieurs classes à gérer.
- Si elem est absente, arrêter la fonction immédiatement avec return. Ce test évite une erreur qui bloquerait tout le JavaScript de la page.
- Parcourir le tableau nomClasses avec une boucle afin d'appliquer le même traitement à chaque classe. Utiliser une variable de boucle nommée nomClasse pour représenter la classe courante.
- Par exemple, si nomClasses vaut ['active', 'alerte'], la boucle doit traiter d'abord 'active', puis 'alerte'. À chaque itération, appeler elem.classList.toggle(nomClasse).
- La méthode classList.toggle() signifie "basculer" si la classe n'est pas présente sur l'élément, elle est ajoutée, si elle est déjà présente, elle est supprimée. Cela permet de gérer un effet d'activation au clic sans devoir écrire soi-même des conditions if pour ajouter ou retirer la classe.
Étape 02
- Dans js/app.js, importer la fonction alternerClasseCss depuis ./modules/alternerClasseCss.js.
- Sélectionner les deux boutons et stocker chaque référence dans une constante btnExclamationElem pour #btn-exclamation, et btnInterrogationElem pour #btn-interrogation.
- Créer deux fonctions gestionnaires de clic gererBtnExclamationClic et gererBtnInterrogationClic. Chaque fonction reçoit l'événement e en paramètre. Dans ces gestionnaires, utiliser e.currentTarget pour viser le bouton qui a déclenché l'écouteur. Cela garantit de manipuler le bouton, même si le clic se produit sur sa balise enfant span.
- Dans gererBtnExclamationClic, appeler alternerClasseCss sur e.currentTarget avec les classes 'active' et 'alerte'.
- Dans gererBtnInterrogationClic, appeler alternerClasseCss sur e.currentTarget avec la seule classe 'active'.
-
Poser les écouteurs click
- sur btnExclamationElem en utilisant gererBtnExclamationClic
- sur btnInterrogationElem en utilisant gererBtnInterrogationClic
Test
- Cliquer plusieurs fois sur ! et vérifier l'alternance entre l'état normal et l'état "enfoncé en alerte".
- Cliquer plusieurs fois sur ¿ et vérifier l'alternance entre l'état normal et l'état "enfoncé".
- Cliquer sur l'un puis sur l'autre et vérifier que chaque bouton conserve son propre état.
Exo-02-dom-class-style
Cet exercice porte sur la sélection unique dans un groupe de boutons, la délégation d'événement, et la manipulation d'une classe CSS via classList.add() et classList.remove().
Attendu
Quatre boutons sont affichés. Un seul bouton doit être en état "actif" à la fois grâce à la classe CSS active. Au clic sur un bouton, l'état actif doit quitter l'ancien bouton et passer sur le bouton cliqué. Si le bouton cliqué est déjà actif, rien ne doit changer.
Structure
Créer un dossier nommé Exo-02-dom-class-style, puis organiser les fichiers et dossiers en respectant l'arborescence suivante :
Exo-02-dom-class-style/
|-- index.html
|-- assets/
| └── css/
| └── style.css
└── js/
|-- app.js
└── modules/
└── basculerClasseCss.js
Fichiers fournis
Copier les fichiers suivants à l'identique.
index.html
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JS: DOM Attributs "class" et "style" - Exo 02</title>
<link rel="stylesheet" href="./assets/css/style.css">
</head>
<body>
<main>
<h1>JS: DOM Attributs "class" et "style" - Exo 02</h1>
<div id="btn-conteneur" class="btn-conteneur">
<button class="active"><span></span></button>
<button><span></span></button>
<button><span></span></button>
<button><span></span></button>
</div>
</main>
<script type="module" src="./js/app.js"></script>
</body>
</html>
assets/css/style.css
/* VARIABLES */
:root
{
/* Couleurs Sémantiques */
--fond-principal: #0e1319;
--texte-principal: #e7e1ca;
--accent: orange;
--etat-actif: red;
/* Échelle d'espacement */
--espace-p: 1vw;
--espace-g: 8rem;
--espace-m: 2rem;
/* Typographie */
--police-corps: 'Arial', sans-serif;
}
/* RESET */
html, body, div, span, h1, button
{
margin: 0;
padding: 0;
border: 0;
box-sizing: border-box;
}
/* BASES */
html
{
color: var(--texte-principal);
font-family: var(--police-corps);
}
body
{
min-height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
background-color: var(--fond-principal);
}
main
{
padding: var(--espace-m) 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
width: 100%;
}
h1
{
margin-bottom: var(--espace-m);
text-align: center;
}
/* Boutons */
.btn-conteneur
{
display: flex;
gap: 0 3rem;
}
button
{
border-radius: 50%;
width: 5rem;
height: 5rem;
background-color: var(--fond-principal);
box-shadow: 0 10px 4px #232325, 10px 0 4px #c8c9ce, 0 -10px 4px #999a9d, -10px 0 4px #7a7b81;
}
button:hover
{
cursor: pointer;
}
button > span
{
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
font-size: 5rem;
font-weight: bold;
text-transform: uppercase;
background: radial-gradient(var(--texte-principal), var(--accent) 50%, var(--fond-principal) 80%);
transition: transform ease-in-out 0.2s, box-shadow ease-in-out 0.2s;
}
button.active > span
{
transform: scale3d(0.9, 0.9, 1);
background: radial-gradient(var(--texte-principal), var(--etat-actif) 50%, var(--fond-principal) 80%);
box-shadow: 0 -10px 4px #232325, 10px 0 4px #c8c9ce, 0 10px 4px #999a9d, -10px 0 4px #7a7b81;
}
Instructions
Le travail se fait dans js/app.js et js/modules/basculerClasseCss.js. Le résultat attendu doit être visible dans la page.
Étape 01
- Créer le fichier js/modules/basculerClasseCss.js. Y créer puis exporter une fonction nommée basculerClasseCss.
-
La fonction doit recevoir trois arguments et utiliser exactement les noms suivants :
- conteneurElem pour la balise HTML qui contient tous les boutons
- cibleElem pour le bouton (balise button) qui vient d'être cliqué
- classeCss pour le nom de la classe CSS à déplacer sur le bon bouton (par exemple "active")
- Si conteneurElem ou cibleElem n'existent pas, arrêter la fonction immédiatement avec return. Ce test évite une erreur qui bloquerait tout le JavaScript de la page.
- Si cibleElem contient déjà la classe classeCss, arrêter la fonction avec return. Cela évite de retirer puis de remettre la même classe alors que rien ne doit changer.
- Récupérer l'ancien élément actif dans une constante nommée ancienElement, en utilisant la méthode prédéfinie querySelector sur conteneurElem, en ciblant la classe CSS classeCss. Cette recherche permet d'identifier le bouton actuellement actif afin de transférer l'état vers le nouveau bouton cliqué.
- Retirer la classe sur ancienElement avec ancienElement.classList.remove(classeCss), puis ajouter la classe sur cibleElem avec cibleElem.classList.add(classeCss). Le résultat attendu est une sélection unique, comparable à un groupe de boutons radio, mais réalisée avec des classes CSS.
Étape 02
- Dans js/app.js, importer la fonction basculerClasseCss depuis ./modules/basculerClasseCss.js.
- Sélectionner le conteneur #btn-conteneur et stocker la référence dans une constante nommée btnConteneurElem.
- Créer une fonction gestionnaire nommée gererBtnConteneurClic. Cette fonction doit recevoir l'événement e en paramètre.
- Poser un seul écouteur click sur btnConteneurElem en utilisant gererBtnConteneurClic. Cette technique s'appelle la délégation d'événement. Lors d'un clic, l'événement suit le flux des événements il est d'abord déclenché sur l'élément ciblé, puis il remonte progressivement vers ses parents. Ce mécanisme est appelé le bubbling. Grâce à ce comportement, un seul écouteur placé sur le conteneur peut intercepter les clics provenant de tous les boutons qu'il contient.
-
Dans gererBtnConteneurClic, récupérer le bouton réellement concerné et stocker le résultat dans une constante nommée
boutonCible, avec l'instruction
e.target.closest('button').
- e.target représente l'élément précis cliqué, ce peut être le span à l'intérieur du bouton
- closest('button') remonte jusqu'au bouton le plus proche, ce qui garantit de manipuler la balise button
-
Si boutonCible est absent, ne rien faire.
Sinon, appeler la fonction utilitaire basculerClasseCss avec ces trois arguments, dans cet ordre
- e.currentTarget pour le conteneur
- boutonCible pour le bouton à activer
- 'active' pour le nom de la classe CSS à déplacer
Test
- Cliquer sur chacun des boutons et vérifier qu'un seul bouton est actif à la fois.
- Cliquer sur le bouton déjà actif et vérifier que rien ne change.
- Cliquer sur le span à l'intérieur d'un bouton et vérifier que la sélection fonctionne quand même.
Exo-03-dom-class-style
Cet exercice porte sur le déclenchement d'un effet visuel au défilement, la mesure de position d'un élément, et l'ajout ou la suppression d'une classe CSS en fonction d'un point de référence dans la fenêtre.
Attendu
Lorsque la section Portfolio traverse le centre vertical de la fenêtre, elle doit devenir active grâce à la classe CSS active et prendre la couleur d'accent. Dès que le centre de la fenêtre n'est plus situé dans cette section, la classe doit être retirée. Le comportement doit aussi fonctionner après un rafraîchissement de la page, même si le scroll est déjà positionné sur la section.
Structure
Créer un dossier nommé Exo-03-dom-class-style, puis organiser les fichiers et dossiers en respectant l'arborescence suivante :
Exo-03-dom-class-style/
|-- index.html
|-- assets/
| └── css/
| └── style.css
└── js/
|-- app.js
└── modules/
└── basculerClasseCssRefCentreFenetre.js
Fichiers fournis
Copier les fichiers suivants à l'identique.
index.html
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JS: DOM Attributs "class" et "style" - Exo 03</title>
<link rel="stylesheet" href="./assets/css/style.css">
</head>
<body>
<div class="ligneHorizontale"></div>
<main>
<h1>JS: DOM Attributs "class" et "style" - Exo 03</h1>
<section id="section-introduction">
<h2>Section Introduction</h2>
</section>
<section id="section-portfolio">
<h2>Section Portfolio</h2>
</section>
<section id="section-atouts">
<h2>Section Atouts</h2>
</section>
</main>
<script type="module" src="./js/app.js"></script>
</body>
</html>
assets/css/style.css
/* VARIABLES */
:root
{
/* Couleurs Sémantiques */
--fond-principal: #0e1319;
--texte-principal: #e7e1ca;
--accent: orange;
/* Échelle d'espacement */
--espace-p: 1vw; /* p pour petit (ancien hor-s) */
--espace-g: 8rem; /* g pour grand (ancien ver-l) */
--espace-m: 2rem; /* m pour moyen (ancien ver-s) */
/* Typographie */
--police-corps: 'Arial', sans-serif;
}
/* RESET */
html, body, section, div, span, h1, p, b
{
margin: 0;
padding: 0;
border: 0;
box-sizing: border-box;
}
/* BASES */
html
{
color: var(--texte-principal);
font-family: var(--police-corps);
}
body
{
min-height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
background-color: var(--fond-principal);
}
main
{
padding: var(--espace-m) 0;
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
width: 100%;
}
section
{
padding: var(--espace-m) var(--espace-g);
min-height: 105vh;
width: 100%;
display: flex;
flex-direction: column;
}
/* Sections paires : inversion des couleurs */
section:nth-of-type(2n)
{
min-height: 50vh;
color: var(--fond-principal);
background-color: var(--texte-principal);
}
section > div
{
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
h1
{
margin-bottom: var(--espace-m);
text-align: center;
}
h2
{
text-align: center;
}
p
{
margin-bottom: var(--espace-m);
}
section.active
{
background-color: var(--accent);
}
.ligneHorizontale
{
border: 2px solid black;
position: fixed;
top: 50%;
left: 0;
width: 100vw;
height: 6px;
background: white;
transform: translate3d(0, -50%, 0);
}
Instructions
Le travail se fait dans js/app.js et js/modules/basculerClasseCssRefCentreFenetre.js. Le résultat attendu doit être visible dans la page.
Étape 01
-
Créer le fichier js/modules/basculerClasseCssRefCentreFenetre.js.
Y créer puis exporter deux fonctions :
- actualiserCachePosition
- basculerClasseCssRefCentreFenetre
- Créer une variable de module nommée cachePositionElement qui sert de cache pour stocker la position de l'élément. L'objectif est d'éviter de recalculer une géométrie complète à chaque défilement.
- Dans actualiserCachePosition(elem), mesurer l'élément avec getBoundingClientRect(). Cette méthode demande au navigateur de calculer le rectangle qui entoure l'élément à l'écran. Elle retourne un objet (souvent stocké dans une constante nommée rect) qui contient plusieurs informations utiles, par exemple top et bottom.
- Les valeurs rect.top et rect.bottom sont exprimées en pixels et représentent des distances par rapport au haut de la fenêtre visible. Cela signifie que ces valeurs changent quand la page défile, même si l'élément ne bouge pas dans le document.
-
Pour transformer ces positions "dans la fenêtre" en positions "dans toute la page", ajouter la quantité de pixels déjà défilée (scroll)
avec la propriété window.scrollY.
On obtient alors des coordonnées plus stables, adaptées à une comparaison pendant le scroll.
Stocker ces valeurs dans la variable de cache cachePositionElement sous la forme
cachePositionElement = { top: rect.top + window.scrollY, bottom: rect.bottom + window.scrollY } - Cette mesure n'est pas réalisée à chaque défilement (scroll) de la page, car getBoundingClientRect() peut être coûteuse, le navigateur pouvant être amené à recalculer la mise en page avant de fournir des valeurs exactes. L'objectif est donc de limiter ces calculs au chargement de la page et lors d'un redimensionnement de la fenêtre de navigation (resize), puis d'utiliser uniquement les valeurs mises en cache pendant le défilement.
-
Calculer la position du centre vertical de la fenêtre en pixels, afin d'obtenir un point de référence unique dans toute la page,
représenté à l'écran par la ligne horizontale.
L'objectif est de transformer "le centre de la fenêtre" en une valeur numérique simple,
comparable aux coordonnées mises en cache (top et bottom)
et de stocker ce point de référence dans une constante nommée centreFenetreY.
- Commencer par déterminer où se situe le centre dans la zone visible. La hauteur de cette zone est disponible via window.innerHeight. En divisant cette valeur par 2, on obtient la position du centre, mais uniquement dans la fenêtre.
- Ensuite, convertir cette valeur relative à la fenêtre en une valeur tenant compte du défilement de la page. Pour cela, prendre en compte le défilement déjà effectué, fourni par window.scrollY. Quand la page est tout en haut, cette valeur vaut 0, puis elle augmente à mesure que l'on descend.
- Additionner ces deux informations pour obtenir la position absolue du centre dans la page. Stocker le résultat dans centreFenetreY avec la formule window.scrollY + (window.innerHeight / 2).
- Vérifier si la valeur centreFenetreY se situe entre cache.top et cache.bottom. Stocker le résultat de cette comparaison dans une constante nommée estAuCentre. Si cette constante vaut true, l'élément est considéré comme étant "au centre" de la fenêtre.
- Basculer la classe avec la forme suivante elem.classList.toggle(classeCss, estAuCentre). Avec un deuxième argument, toggle ne sert plus à inverser aveuglément la classe est ajoutée quand la condition vaut true, et supprimée quand elle vaut false.
Étape 02
- Dans js/app.js, importer les deux fonctions actualiserCachePosition et basculerClasseCssRefCentreFenetre depuis ./modules/basculerClasseCssRefCentreFenetre.js.
- Sélectionner la section Portfolio via l'id section-portfolio et stocker la référence dans une constante nommée sectionPortfolioElem.
- Appeler actualiserCachePosition dès le chargement en passant sectionPortfolioElem, afin de remplir le cache. Appeler ensuite immédiatement basculerClasseCssRefCentreFenetre avec sectionPortfolioElem et la classe 'active', afin de gérer le cas où la page est rafraîchie alors que le scroll est déjà positionné sur la section.
- Poser un écouteur resize sur window. Dans cet écouteur, rappeler actualiserCachePosition en passant sectionPortfolioElem. Sans ce recalcul, les coordonnées stockées ne correspondraient plus à la réalité après un redimensionnement.
- Poser un écouteur scroll sur window. Lors du scroll, ne pas appeler getBoundingClientRect(). Utiliser uniquement les valeurs en cache et un calcul mathématique simple, afin de garder un défilement fluide.
- Lors d'un scroll, l'événement peut se déclencher très souvent, parfois des dizaines de fois par seconde. Pour éviter d'exécuter la vérification "n'importe quand" au milieu d'un rafraîchissement graphique, déclencher la vérification à l'intérieur de requestAnimationFrame.
- requestAnimationFrame demande au navigateur d'exécuter une fonction juste avant le prochain affichage à l'écran. Cela permet de regrouper les mises à jour et de les faire au bon moment, ce qui réduit les saccades lorsque le code est exécuté pendant le défilement.
- Concrètement, dans l'écouteur de scroll, appeler window.requestAnimationFrame(() => { ... }). À l'intérieur, appeler basculerClasseCssRefCentreFenetre en passant sectionPortfolioElem et la classe 'active'.
- Ajouter l'option { passive: true } à l'écouteur de scroll. Un écouteur "passif" indique au navigateur que le gestionnaire ne tentera pas d'empêcher le scroll avec preventDefault(). Le navigateur peut alors démarrer le défilement immédiatement, sans attendre l'exécution du JavaScript.
- Cette option est surtout utile pour des événements qui peuvent bloquer le défilement, comme scroll, wheel, touchstart et touchmove. Ici, l'objectif est simplement d'informer le navigateur que le code ne cherche pas à bloquer le scroll, afin d'obtenir un défilement plus fluide, notamment sur mobile.
Test
- Faire défiler la page et vérifier que la section Portfolio devient orange quand elle traverse la ligne blanche.
- Continuer à défiler et vérifier que la section perd la classe active dès que le centre sort de la section.
- Rafraîchir la page alors que la section Portfolio est déjà au centre et vérifier que la couleur est correcte dès le chargement.
- Redimensionner la fenêtre, puis refaire un scroll et vérifier que la détection reste correcte.
Exo-04-dom-class-style
Cet exercice porte sur le positionnement dynamique d'un élément via JavaScript, l'événement mousemove, et la modification de styles avec style.transform pour faire suivre un élément au curseur.
Attendu
Un petit cercle orange doit suivre le curseur de la souris et rester centré dessus. Le déplacement doit rester fluide.
Structure
Créer un dossier nommé Exo-04-dom-class-style, puis organiser les fichiers et dossiers en respectant l'arborescence suivante :
Exo-04-dom-class-style/
|-- index.html
|-- assets/
| └── css/
| └── style.css
└── js/
|-- app.js
└── modules/
└── suivreCurseur.js
Fichiers fournis
Copier les fichiers suivants à l'identique.
index.html
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JS: DOM Attributs "class" et "style" - Exo 04</title>
<link rel="stylesheet" href="./assets/css/style.css">
</head>
<body>
<main>
<h1>JS: DOM Attributs "class" et "style" - Exo 04</h1>
<div id="harceleur-curseur" class="harceleur-curseur"></div>
</main>
<script type="module" src="./js/app.js"></script>
</body>
</html>
assets/css/style.css
/* VARIABLES */
:root {
/* Couleurs sémantiques */
--fond-principal: #0e1319;
--texte-principal: #e7e1ca;
--accent-visuel: orange;
/* Typographie */
--police-corps: 'Arial', sans-serif;
/* Espacements */
--espace-m: 2rem;
}
/* RESET */
html, body, h1 {
margin: 0;
padding: 0;
border: 0;
box-sizing: border-box;
}
/* BASES */
html {
color: var(--texte-principal);
font-family: var(--police-corps);
background-color: var(--fond-principal);
}
body {
min-height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
}
main {
padding: var(--espace-m) 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
width: 100%;
}
h1 {
margin-bottom: var(--espace-m);
text-align: center;
}
/* Indicateur de Curseur (Accessibilité) */
.harceleur-curseur {
position: fixed;
top: 0;
left: 0;
width: 2rem;
height: 2rem;
border-radius: 50%;
background-color: var(--accent-visuel);
/* IMPORTANT : empêche le cercle de bloquer les clics sur les boutons en dessous */
pointer-events: none;
/* Assure que l'indicateur est au-dessus de tout le reste */
z-index: 9999;
/* Transition pour lisser le mouvement */
transition: transform 0.1s ease-out;
/* Optimisation GPU : indique au navigateur que cet élément va bouger */
will-change: transform;
}
/* Accessibilité : désactivation si l'utilisateur refuse les animations système */
@media (prefers-reduced-motion: reduce) {
.harceleur-curseur {
display: none;
}
}
Instructions
Le travail se fait dans js/app.js et js/modules/suivreCurseur.js. Le résultat attendu doit être visible dans la page.
Étape 01
- Créer le fichier js/modules/suivreCurseur.js. Y créer puis exporter une fonction nommée suivreCurseur.
-
La fonction doit recevoir deux arguments et utiliser exactement les noms suivants :
- harceleurCurseurElem pour l'élément HTML qui doit suivre la souris
- e pour l'objet événement du mousemove
- Si harceleurCurseurElem est absent, arrêter la fonction immédiatement avec return. Ce test évite une erreur qui bloquerait tout le JavaScript de la page.
- Mettre à jour la position de l'élément via style.transform. Pour obtenir un mouvement fluide, réaliser cette mise à jour dans un requestAnimationFrame. Cette méthode demande au navigateur d'exécuter une fonction juste avant le prochain affichage à l'écran, ce qui permet d'aligner les mises à jour sur le rythme de rafraîchissement de l'écran.
- Récupérer la position de la souris dans deux constantes nommées x et y en lisant e.clientX et e.clientY. Ces coordonnées représentent la position de la souris en pixels dans la zone visible de la fenêtre.
-
Déplacer l'élément pour qu'il suive précisément le curseur en modifiant
harceleurCurseurElem.style.transform,
en utilisant une transformation CSS composée de deux
translate3d chaînés.
Le premier positionne l'élément aux coordonnées de la souris, le second sert à le centrer sur lui-même.
- Les transformations CSS peuvent être chaînées en les écrivant dans une seule valeur de transform, séparées par un espace.
- Le premier translate3d place le cercle aux coordonnées de la souris, par exemple translate3d(${x}px, ${y}px, 0). Sans centrage, ces coordonnées positionnent le coin supérieur gauche de l'élément sur le curseur.
- Le second translate3d sert à recentrer l'élément sur lui-même, en appliquant un décalage de -50% sur l'axe X et sur l'axe Y, par exemple translate3d(-50%, -50%, 0). Ce décalage permet d'aligner le centre du cercle sur la position du curseur.
- Au final, la propriété transform doit contenir les deux transformations, écrites dans cet ordre et séparées par un espace, par exemple translate3d(${x}px, ${y}px, 0) translate3d(-50%, -50%, 0).
Étape 02
- Dans js/app.js, importer la fonction suivreCurseur depuis ./modules/suivreCurseur.js.
- Sélectionner l'élément qui suit la souris (classe CSS harceleur-curseur) et stocker la référence dans une constante nommée harceleurCurseurElem.
- Créer une fonction gestionnaire nommée gererMouvementSourisDocument. Cette fonction doit recevoir l'événement e en paramètre.
- Dans gererMouvementSourisDocument, appeler suivreCurseur(harceleurCurseurElem, e).
- Poser un écouteur mousemove sur document, en utilisant gererMouvementSourisDocument. L'objectif est de recevoir un événement à chaque mouvement de souris, afin de mettre à jour la position du cercle.
Test
- Déplacer la souris et vérifier que le cercle orange suit le curseur.
- Vérifier que le cercle reste centré sur la souris, pas décalé sur un coin.
Exo-05-dom-class-style
Cet exercice porte sur un effet de parallaxe au scroll, l'utilisation de querySelectorAll(), la mise à jour de styles via style.transform, et l'optimisation d'un écouteur de scroll avec requestAnimationFrame et l'option { passive: true }.
Un effet de parallaxe consiste à donner une impression de profondeur en déplaçant plusieurs éléments à des vitesses différentes. On rencontre ce principe dans les dessins animés et dans les jeux 2D un décor "loin" bouge lentement, un décor "proche" bouge plus vite.
Sur une page web, on applique le même principe pendant le défilement. La page possède plusieurs calques, et chaque calque est déplacé en fonction du scroll avec un coefficient. Un coefficient faible produit un déplacement faible donc un calque qui semble éloigné. Un coefficient élevé produit un déplacement plus grand donc un calque qui semble plus proche.
Attendu
Trois calques verticaux sont affichés. Lors du défilement, ils doivent se déplacer verticalement à des vitesses différentes, comme un effet de profondeur. Le premier calque doit bouger très lentement, le deuxième plus vite, et le troisième encore plus vite.
Structure
Créer un dossier nommé Exo-05-dom-class-style, puis organiser les fichiers et dossiers en respectant l'arborescence suivante :
Exo-05-dom-class-style/
|-- index.html
|-- assets/
| └── css/
| └── style.css
└── js/
|-- app.js
└── modules/
└── parallaxe.js
Fichiers fournis
Copier les fichiers suivants à l'identique.
index.html
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JS: DOM Attributs "class" et "style" - Exo 05</title>
<link rel="stylesheet" href="./assets/css/style.css">
</head>
<body>
<main>
<h1>JS: DOM Attributs "class" et "style" - Exo 05</h1>
<div id="parallaxe-conteneur" class="parallaxe-conteneur">
<div class="parallaxe-calque js-parallaxe-calque"></div>
<div class="parallaxe-calque js-parallaxe-calque"></div>
<div class="parallaxe-calque js-parallaxe-calque"></div>
</div>
</main>
<script type="module" src="./js/app.js"></script>
</body>
</html>
assets/css/style.css
/* VARIABLES */
:root {
/* Couleurs sémantiques */
--fond-principal: #0e1319;
--texte-principal: #e7e1ca;
--accent-visuel: orange;
/* Espaces */
--espace-m: 2rem;
--espace-l: 8rem;
/* Typo */
--police-corps: 'Arial', sans-serif;
}
/* RESET */
main, div, h1 {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* BASES */
html {
color: var(--texte-principal);
font-family: var(--police-corps);
background-color: var(--fond-principal);
}
body {
/* Garder une hauteur minimale pour permettre le scroll du parallaxe */
min-height: 200vh;
width: 100%;
display: flex;
flex-direction: column;
}
main {
/* overflow: hidden permet d'éviter les barres de scroll horizontales
si les éléments sortent un peu du cadre */
overflow: hidden;
padding: var(--espace-m) 0;
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
width: 100%;
}
h1 {
margin-bottom: var(--espace-m);
text-align: center;
padding: 0 1rem;
}
/* CONTENEUR PARALLAXE */
.parallaxe-conteneur {
display: flex;
gap: 0 var(--espace-m);
/* S'assurer que le conteneur est le référentiel pour ses enfants */
position: relative;
padding: var(--espace-l) 0;
}
.parallaxe-conteneur > .parallaxe-calque {
width: 20vw;
height: 40vh;
background-color: var(--accent-visuel);
/* OPTIMISATION GPU :
Indiquer au navigateur que ces éléments vont bouger pour
qu'il utilise la carte graphique (évite les saccades). */
will-change: transform;
transition: transform 0.1s ease-out;
}
Instructions
Le travail se fait dans js/app.js et js/modules/parallaxe.js. Le résultat attendu doit être visible dans la page.
Étape 01
- Créer le fichier js/modules/parallaxe.js. Y créer puis exporter une fonction nommée animerParallaxe.
-
La fonction doit recevoir deux arguments :
- calquesElements pour la liste des éléments HTML à animer
- vitesses pour le tableau de coefficients de vitesse
- Vérifier que la liste calquesElements contient au moins un élément. Si elle est vide, arrêter la fonction immédiatement avec return. Ce test évite une erreur et évite d'exécuter des calculs inutiles.
- Pour rendre l'animation fluide au scroll, déclencher la mise à jour dans un requestAnimationFrame. L'objectif est de regrouper les mises à jour et de les exécuter juste avant le prochain affichage à l'écran.
- Dans la fonction appelée par requestAnimationFrame, stocker la valeur du scroll dans une constante nommée scrollActuel en lisant window.scrollY. Cette valeur représente le nombre de pixels déjà défilés depuis le haut de la page.
-
Parcourir tous les calques avec une boucle for et un index nommé
i.
À chaque itération, récupérer le coefficient de vitesse dans une constante nommée
vitesse.
Si le tableau vitesses ne contient pas de valeur à cet index,
utiliser 0.5 comme valeur par défaut.
- Un coefficient est un nombre qui sert de multiplicateur. Ici, on multiplie le scroll par vitesse pour obtenir un déplacement plus petit ou plus grand.
-
Utiliser un nombre à virgule inférieur à 1 permet de créer un mouvement plus lent que le scroll.
Par exemple, si scrollActuel vaut 300
- avec vitesse = 0.1, le déplacement vaut environ 30
- avec vitesse = 0.4, le déplacement vaut environ 120
- avec vitesse = 0.7, le déplacement vaut environ 210
- Une valeur plus grande que 1 ferait bouger un calque plus vite que le scroll. Ici, l'objectif est plutôt de simuler des calques "lointains" et "proches" sans que le premier plan parte trop vite.
- Le choix de 0.5 par défaut permet d'avoir un comportement "moyen" si une vitesse manque. Cela évite un undefined et garantit que tous les calques peuvent quand même bouger.
- Calculer le déplacement vertical dans une constante nommée deplacement. Utiliser la formule suivante Math.round(scrollActuel * vitesse). Plus vitesse est grand, plus le calque se déplace rapidement.
- Appliquer le déplacement au calque courant en modifiant le style du calque courant avec style.transform. Utiliser une transformation de ce type translate3d(0, ${deplacement}px, 0). L'objectif est de déplacer uniquement sur l'axe vertical.
Étape 02
- Dans js/app.js, importer la fonction animerParallaxe depuis ./modules/parallaxe.js.
- Sélectionner les trois calques (classe .js-parallaxe-calque dans #parallaxe-conteneur) et stocker la liste dans une constante nommée calquesElements. Utiliser querySelectorAll(), qui retourne une liste de résultats.
-
Créer un tableau de vitesses dans une constante nommée coefficientsVitesse
(ex.: [0.1, 0.4, 0.7]).
Chaque valeur correspond à la vitesse d'un calque
- une petite valeur représente un calque lent, souvent utilisé comme arrière-plan
- une grande valeur représente un calque rapide, souvent utilisé comme premier plan
- Créer une fonction gestionnaire nommée gererScrollWindow. Dans cette fonction, appeler animerParallaxe(calquesElements, coefficientsVitesse).
- Poser un écouteur scroll sur window en utilisant gererScrollWindow. Ajouter l'option { passive: true }. Cette option indique que le gestionnaire ne tentera pas de bloquer le scroll avec preventDefault(), ce qui permet au navigateur d'optimiser le défilement, en particulier sur mobile.
- Appeler une première fois gererScrollWindow() après avoir posé l'écouteur, afin d'appliquer un état initial correct au chargement de la page. Cela évite d'attendre le premier scroll pour voir l'effet.
Test
- Faire défiler la page et vérifier que les trois calques se déplacent verticalement.
- Vérifier que le premier calque se déplace plus lentement que les suivants, et que le troisième calque présente le déplacement le plus rapide, ce qui renforce l'effet de profondeur entre les différents plans.
- Modifier les valeurs dans coefficientsVitesse et observer l'impact sur la vitesse des calques.