DOM JavaScript : manipuler la page correctement
Le DOM est la représentation de votre page que JavaScript peut lire et modifier. Sélection, création d’éléments, événements, délégation et pièges à éviter, avec exemples.

Le DOM est la représentation de votre page que JavaScript peut lire et modifier. Sélection, création d’éléments, événements, délégation et pièges à éviter, avec exemples.

Le DOM (Document Object Model) est la représentation en objets de votre page HTML, que JavaScript peut lire et modifier. Chaque balise devient un nœud ; chaque nœud expose des propriétés et des méthodes. Savoir le manipuler proprement, c’est la base de toute interface dynamique, avec ou sans framework.
Cet article couvre les gestes essentiels : sélectionner, modifier, créer, écouter des événements, puis éviter les pièges de performance et d’accessibilité. La référence complète est l’introduction au DOM sur MDN.
Le navigateur lit votre HTML, construit un arbre d’objets, puis l’affiche. Cet arbre peut ensuite être modifié par JavaScript : le HTML affiché à l’écran n’est alors plus identique au fichier d’origine. L’onglet « Éléments » des outils de développement montre le DOM courant, pas le source.
Conséquences pratiques :
Le contexte d’exécution est détaillé dans Comment fonctionne JavaScript dans le navigateur.
Un script qui cherche un élément avant qu’il existe obtient null. Trois approches simples :
<!-- Le script se charge sans bloquer et s'exécute après l'analyse du HTML -->
<script src="/js/app.js" defer></script>
<!-- Ou en module, différé par défaut -->
<script type="module" src="/js/app.js"></script>
// Alternative : attendre l'événement de fin d'analyse
document.addEventListener('DOMContentLoaded', () => {
console.log('Le DOM est prêt');
});
Placer le script en fin de body fonctionne aussi, mais defer est plus lisible et permet un téléchargement en parallèle. Pour la syntaxe des modules, voyez Modules JavaScript : import, export et ESM.
// Premier élément correspondant (ou null)
const titre = document.querySelector('h1');
const bouton = document.querySelector('#envoyer');
const liens = document.querySelectorAll('nav a');
querySelectorAll renvoie une NodeList statique : vous pouvez la parcourir avec forEach ou for...of, et la convertir avec Array.from() pour utiliser map et filter.
const zone = document.querySelector('[data-resultats]');
if (!zone) {
// Rien à faire sur cette page (dans une fonction d'initialisation)
return;
}
Un code qui suppose que l’élément existe plante dès qu’une page change. Un test explicite coûte une ligne.
data-*Évitez d’accrocher votre JavaScript à des classes de style (.btn-rouge) qui vont changer au prochain redesign. Préférez des attributs dédiés :
<button data-action="ajouter-panier">Ajouter</button>
const boutons = document.querySelectorAll('[data-action="ajouter-panier"]');
Le style et le comportement évoluent ainsi séparément.
const message = document.querySelector('#message');
// Texte brut : sûr
message.textContent = 'Enregistré';
// Attributs et classes
message.setAttribute('role', 'status');
message.classList.add('visible');
message.classList.toggle('erreur', false);
// Style ponctuel via une variable CSS plutôt que des styles en dur
message.style.setProperty('--decalage', '8px');
textContent insère du texte : aucune balise n’est interprétée ;innerHTML interprète du HTML. Avec une donnée venant d’un utilisateur ou d’une API non maîtrisée, c’est une porte ouverte aux failles XSS.// À éviter avec une donnée non fiable
liste.innerHTML = `<li>${commentaire}</li>`;
// Préférable
const li = document.createElement('li');
li.textContent = commentaire;
liste.append(li);
Règle simple : tant que vous n’avez pas besoin de HTML, utilisez textContent.
function creerCarte(produit) {
const carte = document.createElement('article');
carte.className = 'carte';
const titre = document.createElement('h3');
titre.textContent = produit.nom;
const prix = document.createElement('p');
prix.textContent = `${produit.prix} €`;
carte.append(titre, prix);
return carte;
}
const conteneur = document.querySelector('#produits');
conteneur.append(creerCarte({ nom: 'Casque', prix: 49 }));
Quelques méthodes utiles : append, prepend, before, after, replaceWith et remove. Pour insérer beaucoup d’éléments, construisez-les d’abord dans un fragment puis ajoutez-le en une fois :
const fragment = document.createDocumentFragment();
produits.forEach((p) => fragment.append(creerCarte(p)));
conteneur.append(fragment);
Vous limitez ainsi les recalculs de mise en page.
const bouton = document.querySelector('#envoyer');
bouton.addEventListener('click', (evenement) => {
evenement.preventDefault();
console.log('Clic sur', evenement.currentTarget);
});
Points clés :
addEventListener permet plusieurs écouteurs sur le même élément ;evenement.target est l’élément réellement cliqué, currentTarget celui qui porte l’écouteur ;preventDefault() annule le comportement par défaut (envoi d’un formulaire, suivi d’un lien) ;removeEventListener si l’élément a une durée de vie longue.Avec une liste dynamique, inutile d’ajouter un écouteur à chaque ligne : un seul sur le parent suffit.
const liste = document.querySelector('#taches');
liste.addEventListener('click', (e) => {
const bouton = e.target.closest('[data-action="supprimer"]');
if (!bouton || !liste.contains(bouton)) return;
bouton.closest('li').remove();
});
Les éléments ajoutés plus tard sont couverts sans code supplémentaire, et vous évitez d’accumuler des écouteurs.
const form = document.querySelector('#contact');
form.addEventListener('submit', (e) => {
e.preventDefault();
const donnees = new FormData(form);
console.log(Object.fromEntries(donnees));
});
FormData lit les champs sans que vous deviez les récupérer un par un. Pensez à conserver la validation native (required, type="email") et à ne jamais remplacer la validation côté serveur par celle du navigateur.
offsetHeight) et de styles dans une boucle : chaque lecture peut forcer un recalcul de mise en page. Regroupez d’abord les lectures, puis les écritures.setInterval.requestAnimationFrame pour synchroniser un changement visuel avec le rafraîchissement de l’écran.scroll, resize, input) avec un anti-rebond ou un IntersectionObserver quand c’est adapté.Si une page vous paraît lente sans cause évidente, un audit technique peut relier symptômes et sources.
Manipuler le DOM modifie ce que les lecteurs d’écran et le clavier perçoivent. Quelques réflexes :
<button>) plutôt qu’une div cliquable : focus clavier, activation avec Entrée et Espace, rôle exposé ;aria-live (par exemple role="status") pour les messages d’erreur ou de confirmation ;Pour situer ces exigences, consultez la page accessibilité de Yunt Audit et celle sur les obligations d’accessibilité.
querySelector renvoie null. Utilisez defer ou un module.innerHTML avec une donnée non maîtrisée : risque XSS.div > div > span:nth-child(3) casse au moindre changement de structure. Préférez des data-*.preventDefault() sur un formulaire envoyé en JavaScript : la page se recharge.NodeList et tableau : convertissez avec Array.from() pour map et filter.Maîtriser le DOM, c’est savoir sélectionner avec des sélecteurs robustes, modifier en sécurité (textContent plutôt que innerHTML), créer des éléments proprement, écouter avec la délégation et penser accessibilité dès la conception. Ces gestes sous-tendent aussi les frameworks : comprendre le DOM, c’est mieux comprendre ce qu’ils font à votre place.
Prochaine étape naturelle : charger des données depuis une API et les afficher, ce qui demande de maîtriser l’asynchrone, expliqué dans Promesses et async/await en JavaScript. Pour progresser avec un parcours structuré, découvrez Academy Yunt et son blog.
C’est fortement recommandé. Les frameworks automatisent la mise à jour du DOM, mais les concepts (événements, focus, accessibilité) restent les mêmes.
querySelector et getElementById ?getElementById ne cherche que par identifiant ; querySelector accepte n’importe quel sélecteur CSS. Les deux sont corrects, querySelector est plus polyvalent.
addEventListener ne se déclenche-t-il pas ?Causes fréquentes : l’élément était null à ce moment-là, le sélecteur ne correspond à rien, ou l’élément a été remplacé après l’ajout de l’écouteur. Vérifiez avec console.log.
textContent ou innerText ?textContent renvoie tout le texte du nœud, sans tenir compte de la mise en forme. innerText dépend du rendu (éléments masqués ignorés), ce qui peut déclencher un calcul de mise en page. Pour modifier un texte, textContent convient presque toujours.
Cela dépend de l’usage. Un DOM virtuel simplifie la logique de mise à jour d’interfaces complexes ; il ne rend pas magiquement le DOM natif plus lent. Mesurez avant de conclure.