Prérequis
- Niveau : bases JavaScript et manipulation du DOM (cf. manipuler le DOM).
- Outils : VS Code + navigateur moderne (les DevTools → onglet « Event Listeners »).
- Temps estimé : 45 min à 1 h.
Pourquoi les événements ?
Les événements sont la boucle de réaction du web : l’utilisateur agit, votre code répond. Sans eux, une page reste figée. Bien maîtrisés, ils permettent des interfaces fluides, performantes et accessibles.
Les événements : rendre vos pages interactives
Les événements JavaScript détectent les actions de l’utilisateur (clic, survol, saisie, scroll) et exécutent du code en réponse. C’est ce qui rend vos pages vivantes et interactives.
Les 3 façons d’écouter un événement
// ✅ Méthode recommandée : addEventListener
const bouton = document.getElementById('monBouton');
bouton.addEventListener('click', function() {
alert('Bouton cliqué !');
});
// ⚠️ Ancienne méthode (1 seul handler possible)
bouton.onclick = function() {
alert('Cliqué !');
};
// ❌ À éviter : inline dans le HTML
// <button onclick="alert('Cliqué')">Clic</button>
Les événements les plus utilisés
Événements de souris
const carte = document.querySelector('.carte');
// Clic simple
carte.addEventListener('click', () => console.log('Clic'));
// Double clic
carte.addEventListener('dblclick', () => console.log('Double clic'));
// Survol : entrée
carte.addEventListener('mouseenter', () => {
carte.style.transform = 'scale(1.05)';
});
// Survol : sortie
carte.addEventListener('mouseleave', () => {
carte.style.transform = 'scale(1)';
});
// Position de la souris
document.addEventListener('mousemove', (e) => {
console.log(`X: ${e.clientX}, Y: ${e.clientY}`);
});
Événements de clavier
// Détecter les touches
document.addEventListener('keydown', (e) => {
console.log('Touche pressée :', e.key);
if (e.key === 'Escape') {
fermerModal();
}
if (e.key === 'Enter') {
soumettreFormulaire();
}
// Raccourcis clavier (Ctrl+S)
if (e.ctrlKey && e.key === 's') {
e.preventDefault(); // Empêcher la sauvegarde du navigateur
sauvegarder();
}
});
// Champ de recherche en temps réel
const recherche = document.getElementById('recherche');
recherche.addEventListener('keyup', (e) => {
const terme = e.target.value;
filtrerResultats(terme);
});
Événements de formulaire
const form = document.getElementById('formulaire');
// Soumission
form.addEventListener('submit', (e) => {
e.preventDefault(); // Empêcher le rechargement de la page
const données = new FormData(form);
console.log('Nom :', données.get('nom'));
console.log('Email :', données.get('email'));
});
// Changement de valeur (select, checkbox)
const select = document.getElementById('pays');
select.addEventListener('change', (e) => {
console.log('Pays sélectionné :', e.target.value);
});
// Focus et blur (entrée/sortie du champ)
const input = document.getElementById('email');
input.addEventListener('focus', () => {
input.style.borderColor = '#667eea';
});
input.addEventListener('blur', () => {
input.style.borderColor = '#ddd';
});
// Saisie en temps réel
input.addEventListener('input', (e) => {
console.log('Valeur actuelle :', e.target.value);
});
Événements de scroll et fenêtre
// Scroll
window.addEventListener('scroll', () => {
const scrollY = window.scrollY;
// Barre de progression de lecture
const hauteurPage = document.body.scrollHeight - window.innerHeight;
const progression = (scrollY / hauteurPage) * 100;
document.getElementById('progress').style.width = progression + '%';
});
// Redimensionnement
window.addEventListener('resize', () => {
console.log(`Fenêtre : ${window.innerWidth}x${window.innerHeight}`);
});
// Chargement complet de la page
window.addEventListener('load', () => {
console.log('Page entièrement chargée');
document.getElementById('loader').style.display = 'none';
});
// DOM prêt (plus rapide que load)
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM prêt');
});
L’objet événement (e)
bouton.addEventListener('click', function(e) {
// L'élément cliqué
console.log(e.target);
// Position du clic
console.log(e.clientX, e.clientY);
// Empêcher le comportement par défaut
e.preventDefault();
// Arrêter la propagation
e.stopPropagation();
// Touche modificatrice
console.log(e.ctrlKey, e.shiftKey, e.altKey);
});
Délégation d’événements
Au lieu d’attacher un événement à chaque élément, attachez-le au parent :
// ❌ Inefficace : 100 listeners pour 100 boutons
document.querySelectorAll('.btn-supprimer').forEach(btn => {
btn.addEventListener('click', supprimer);
});
// ✅ Efficace : 1 seul listener sur le parent
document.getElementById('liste').addEventListener('click', (e) => {
if (e.target.classList.contains('btn-supprimer')) {
const id = e.target.dataset.id;
supprimerElement(id);
}
});
// Fonctionne même pour les éléments ajoutés dynamiquement !
💡 Pourquoi la délégation ?
- Performance : 1 listener au lieu de 100
- Dynamique : fonctionne sur les éléments ajoutés après le chargement
- Mémoire : moins de listeners = moins de mémoire utilisée
Erreurs fréquentes
Listener qui ne se déclenche pas
Cause : on attache addEventListener avant que l’élément n’existe (script chargé trop tôt).
Solution : ajoutez defer au <script> ou enveloppez dans DOMContentLoaded.
this qui n’est pas ce que vous croyez
Cause : dans une fonction fléchée passée à addEventListener, this ne pointe pas sur l’élément.
Solution : utilisez function() { this... } classique, ou e.currentTarget.
Formulaire qui recharge la page
Cause : oubli de e.preventDefault() dans le handler submit.
Solution : appelez systématiquement e.preventDefault() en début de handler.
Listener scroll ou resize qui rame
Cause : le handler s’exécute des centaines de fois par seconde.
Solution : appliquez du throttling ou du debouncing (par exemple via requestAnimationFrame pour le scroll).
Exercice pratique
🎯 Défi : Todo List interactive
- Champ de saisie + bouton « Ajouter » (événement
submitoukeydown Enter) - Chaque tâche a un bouton « Supprimer » (délégation d’événements)
- Clic sur une tâche = barré (événement
click) - Raccourci clavier :
Ctrl+Npour focus sur le champ de saisie - Compteur de tâches qui se met à jour automatiquement
Event delegation : optimiser la gestion d’événements sur des listes dynamiques
Attacher un addEventListener à chaque élément d’une liste de 1000 lignes est coûteux en mémoire et complique la gestion des éléments ajoutés dynamiquement. La solution standard est l’event delegation : un seul listener attaché au parent commun, qui inspecte l’élément cliqué via event.target et délègue le traitement. Cette approche scale à n’importe quelle taille de liste, fonctionne sur les éléments ajoutés après le chargement initial, et économise des ressources mémoire.
document.querySelector('#liste-clients').addEventListener('click', (e) => {
const ligne = e.target.closest('tr.client');
if (!ligne) return;
const id = ligne.dataset.clientId;
if (e.target.matches('.btn-modifier')) ouvrirModale(id);
if (e.target.matches('.btn-supprimer')) confirmerSuppression(id);
});
La méthode closest() remonte dans le DOM jusqu’au sélecteur cible, ce qui gère élégamment les clics sur les enfants des boutons (icône SVG dans un button, par exemple). C’est le pattern standard 2026 pour les tableaux interactifs, les listes filtrables et tout composant qui contient des actions par ligne.
Événements personnalisés (CustomEvent) pour découpler les composants
Au-delà des événements DOM standards (click, submit, change), JavaScript permet de créer ses propres événements personnalisés via CustomEvent. Cette technique permet de découpler des composants qui n’ont pas besoin de se connaître directement : un module émet un événement métier, et n’importe quel autre module peut s’y abonner sans dépendance circulaire.
// Émission d'un événement métier
const event = new CustomEvent('commande-validee', {
detail: { commandeId: 1234, montant: 25000, currency: 'XOF' },
bubbles: true
});
document.dispatchEvent(event);
// Écoute dans un autre module
document.addEventListener('commande-validee', (e) => {
envoyerNotification(e.detail.commandeId);
trackerAnalytics(e.detail);
});
Pour une application moderne (Vue, React, Svelte), les frameworks proposent leur propre système d’événements via les props et les stores. Les CustomEvent restent utiles pour la communication entre composants vanilla, entre web components, ou pour exposer des hooks à des scripts tiers (analytics, marketing tags) sans coupler le code applicatif à ces outils.
Performance et passive listeners pour le scroll
Les événements scroll, touchstart, touchmove et wheel sont déclenchés très fréquemment pendant l’interaction utilisateur. Un listener mal optimisé peut bloquer le rendu et créer un lag visible sur les appareils modestes. Deux techniques d’optimisation cumulables. Première technique : marquer le listener comme passif avec { passive: true } en troisième argument de addEventListener. Cela informe le navigateur que le handler n’appellera pas preventDefault(), lui permettant de continuer le scroll fluide en parallèle de l’exécution du handler.
Deuxième technique : throttler ou debouncer les handlers coûteux. Pour un scroll qui déclenche un calcul coûteux à chaque pixel, throttler à 16 ms (60 fps) ou 33 ms (30 fps) suffit largement. Une fonction throttle de 5 lignes, ou la version de Lodash _.throttle(), fait le travail. Pour un input keyup qui déclenche une recherche serveur, debouncer à 300 ms évite des requêtes inutiles à chaque frappe.
Erreurs fréquentes en gestion d’événements
| Erreur | Cause | Correction |
|---|---|---|
| Listener attaché plusieurs fois | addEventListener dans une fonction réappelée |
Garder une référence à la fonction et utiliser removeEventListener avant ré-attache |
| Memory leak sur composants destroy | Listeners non détachés au démontage | Toujours retirer les listeners dans le hook cleanup |
this incorrect dans le handler |
Fonction classique perd le contexte | Utiliser fonctions fléchées ou .bind(this) |
e.preventDefault() dans listener passif |
Conflit avec optimisation navigateur | Retirer passive: true ou ne pas appeler preventDefault |
| Performance scroll dégradée | Calculs coûteux à chaque frame | Throttle + passive listener + RequestAnimationFrame |
Adaptation au contexte ouest-africain
Pour un développeur basé à Dakar, Abidjan, Bamako ou Cotonou qui livre des sites e-commerce ou applications PME, la gestion d’événements impacte directement la qualité perçue par les utilisateurs finaux. Sur un Android d’entrée de gamme avec 2 Go de RAM, un listener mal optimisé peut faire ramer toute l’interface — le visiteur quitte le site avant d’avoir cliqué sur « Ajouter au panier ».
Trois conseils pratiques. Premièrement, profiler systématiquement les pages avec Chrome DevTools Performance Tab pendant les interactions clés (scroll, formulaire, panier). Deuxièmement, tester sur un Galaxy A03 ou équivalent à 200 EUR avec une connexion 4G dégradée — c’est le contexte réel d’usage majoritaire. Troisièmement, prioriser l’event delegation et les passive listeners dès le départ, plutôt que d’attendre les retours utilisateur frustrés. Pour les patterns modernes JS dans le contexte ouest-africain, voir aussi le tutoriel de validation de formulaire JS.
Cycle de vie d’un événement : capture et bubble
Un événement DOM ne se déclenche pas instantanément sur l’élément cliqué — il traverse l’arbre HTML en deux phases. Première phase : la capture, du document jusqu’à l’élément cible, en passant par tous les ancêtres. Deuxième phase : le bubbling, en sens inverse, de l’élément cible jusqu’au document. Par défaut, addEventListener écoute en phase de bubbling, ce qui correspond à 95 % des usages courants.
Le troisième paramètre permet de contrôler ce comportement. addEventListener('click', handler, true) ou { capture: true } écoute en phase de capture. C’est utile dans des cas avancés : intercepter un événement avant qu’il atteigne l’élément cible (par exemple bloquer la propagation à des composants tiers), ou pour les événements qui ne bubblent pas par défaut (focus, blur, mouseenter).
Trois méthodes contrôlent la propagation. e.stopPropagation() arrête la propagation aux ancêtres restants. e.stopImmediatePropagation() arrête en plus les autres listeners attachés au même élément. e.preventDefault() bloque l’action par défaut du navigateur (suivre un lien, soumettre un formulaire) sans arrêter la propagation. Maîtriser ces trois méthodes évite des heures de débuggage sur des bugs de propagation cachée.
Événements clavier : raccourcis et accessibilité
Les événements keydown, keypress et keyup permettent de capturer la saisie clavier. keypress est déprécié depuis 2022 et ne doit plus être utilisé en 2026. keydown et keyup couvrent tous les besoins. La propriété e.key donne la touche pressée sous forme de chaîne lisible (‘Enter’, ‘Escape’, ‘a’, ‘ArrowUp’), nettement plus simple que les anciens e.keyCode ou e.which qui sont également dépréciés.
Pour implémenter des raccourcis clavier (Ctrl+S pour sauvegarder, Escape pour fermer un modal), tester les modificateurs e.ctrlKey, e.shiftKey, e.altKey, e.metaKey. Sur Mac, metaKey correspond à Cmd ; sur Windows et Linux, à la touche Windows. Penser à appeler preventDefault() quand vous interceptez Ctrl+S ou Ctrl+P pour empêcher l’action par défaut du navigateur (sauvegarder la page, imprimer).
Async et événements : Promises et async iterators
Combiner événements et code asynchrone est un pattern courant en 2026. Trois techniques modernes sont à connaître. Première technique : encapsuler un événement dans une Promise pour l’attendre avec await. Par exemple, attendre qu’une image charge : await new Promise(r => img.addEventListener('load', r, { once: true })). Le flag { once: true } retire automatiquement le listener après la première exécution, évitant les memory leaks.
Deuxième technique : EventTarget propose depuis 2022 une API de signal d’abandon via AbortController, particulièrement utile pour cancel un fetch ou un setInterval déclenché par un événement. Troisième technique : les async iterators permettent d’itérer sur des événements comme un flux. Combinés avec des bibliothèques comme rxjs ou des helpers DOM, ils simplifient les patterns réactifs complexes (auto-complete, drag-and-drop, jeux interactifs).
Pour les équipes qui démarrent un projet React, Vue ou Svelte, le système d’événements du framework remplace généralement addEventListener direct — utiliser onClick, v-on:click, on:click respectivement. La connaissance du DOM brut reste néanmoins essentielle pour les cas avancés où les abstractions du framework ne suffisent pas, ou pour intégrer une bibliothèque tierce qui ne connaît que les événements DOM standards.