12k
All articles

Édition dans le navigateur avec contentEditable

Édition contenteditable dans le navigateur : activez l’édition de texte en ligne, captez les événements input et évitez les risques XSS.

OpenReplay Team
OpenReplay Team
Édition dans le navigateur avec contentEditable

N’importe quel élément HTML devient modifiable en place lorsque vous ajoutez l’attribut contenteditable — sans contrôle de formulaire, sans bibliothèque, sans dépendances.

Si vous avez déjà construit un formulaire entier juste pour permettre à quelqu’un de renommer un titre, la première fois que vous essayez cette approche, vous aurez l’impression d’avoir trouvé un raccourci inespéré.

Le navigateur transforme l’élément en hôte d’édition, y place un curseur et laisse l’utilisateur saisir directement dans le DOM rendu. contenteditable est ainsi le moyen le plus rapide de livrer un titre modifiable, un champ à édition par clic, ou une zone de notes légère. Il comporte également des aspects délicats que la référence de l’attribut ne mentionne jamais : il n’existe pas d’événement change natif, le balisage généré varie selon les navigateurs, l’ancienne API de mise en forme est dépréciée, et restituer le résultat à d’autres utilisateurs constitue un vecteur d’attaque XSS classique. Cet article explique comment l’activer, capturer et persister les modifications correctement, gérer ces cas particuliers, et décider quand utiliser autre chose.

Points clés à retenir

  • L’attribut contenteditable accepte trois valeurs : true (ou une chaîne vide) rend un élément modifiable, false le désactive, et plaintext-only permet l’édition du texte brut tout en supprimant la mise en forme enrichie.
  • contentEditable ne dispose d’aucun événement change natif. Écoutez l’événement input, qui se déclenche à chaque modification de l’hôte d’édition.
  • document.execCommand() pour le gras, l’italique et les liens est déprécié et non standard ; utilisez les API Selection et Range avec beforeinput/input, ou une bibliothèque d’éditeur dédiée, pour du texte enrichi réel.
  • N’écrivez jamais le code HTML saisi par l’utilisateur via contenteditable dans la page sans le désinfecter — utilisez DOMPurify, ou setHTML() du navigateur lorsque disponible avec DOMPurify en solution de repli.
  • contenteditable="plaintext-only" est désormais compatible avec tous les navigateurs, ayant été intégré dans Firefox 136 (mars 2025) aux côtés du support de longue date dans Chromium et WebKit.

Activation : l’attribut contenteditable

L’attribut global contenteditable accepte trois valeurs, et choisir la bonne est l’essentiel du travail. true (ou une chaîne vide) rend un élément modifiable ; false le désactive ; et plaintext-only rend le texte brut modifiable tout en désactivant la mise en forme enrichie. Selon la référence MDN de contenteditable, il s’agit d’un attribut énuméré, et non d’un booléen : une valeur manquante ou invalide hérite de la modifiabilité du parent.

La version en une ligne :

<h1 contenteditable="true">Edit this heading</h1>

Pour un champ en texte seul (un titre renommable, une saisie de balise, une note sur une seule ligne), préférez plaintext-only. Il bloque la mise en forme enrichie collée à la source : le contenu collé dans un élément avec contenteditable="true" conserve toute la mise en forme, tandis que le contenu collé dans contenteditable="plaintext-only" en est entièrement dépouillé.

Activez ou désactivez la modification depuis JavaScript via la propriété contentEditable (en camelCase) :

const el = document.querySelector('#note');
el.contentEditable = 'plaintext-only'; // or 'true' / 'false'

Comment capturer et persister les modifications contenteditable ?

contentEditable ne dispose d’aucun événement change natif. Pour capturer les modifications, écoutez l’événement input, qui se déclenche à chaque modification de l’hôte d’édition. C’est l’erreur la plus courante dans les anciens tutoriels, qui utilisent keypress ou keyup et ratent les collages, le glisser-déposer et la saisie IME. Lisez element.innerHTML lorsque vous souhaitez conserver la mise en forme, ou element.textContent lorsque vous voulez du texte brut, puis persistez et restaurez au chargement.

const el = document.querySelector('#note');

// Restore on load
el.textContent = localStorage.getItem('note') ?? '';

// Debounced persistence on every edit
let t;
el.addEventListener('input', () => {
  clearTimeout(t);
  t = setTimeout(() => {
    localStorage.setItem('note', el.textContent);
    // or: fetch('/api/note', { method: 'POST', body: el.textContent })
  }, 400);
});

Remplacez textContent par innerHTML si vous stockez du balisage enrichi, mais lisez d’abord la section sécurité, car ce choix est ce qui transforme un champ de notes en surface d’attaque. Pour un contrôle plus fin, l’événement beforeinput se déclenche avant la mutation du DOM et vous permet d’inspecter ou d’annuler une modification ; il s’applique aux éléments contenteditable et à tout élément en designMode.

Les aspects délicats

C’est là que contenteditable mérite sa réputation. Trois problèmes surviennent en production.

Un balisage désordonné et incohérent. Les navigateurs ne s’accordent pas sur le HTML produit par une région contenteditable, de sorte que la sortie sauvegardée est rarement aussi propre qu’attendu. Comme Scott O’Hara l’a documenté, Safari a historiquement encapsulé les sauts de ligne dans des éléments <div> tandis que Firefox insère des éléments <br>, et <div> est un enfant invalide de <p>, ce qui provoque des anomalies de rendu si vous avez rendu un paragraphe modifiable. Un mode d’échec courant en production est un utilisateur qui colle du contenu depuis Word ou Google Docs et importe une soupe de balises <span> et de styles en ligne ; les replays de session de ces séances d’édition sont un moyen de réellement observer la génération de cette sortie malformée plutôt que de la reconstituer à partir d’une ligne de base de données corrompue. La correction artisanale consiste à préférer plaintext-only, ou à désinfecter lors de l’événement input/paste.

execCommand est déprécié. document.execCommand(), longtemps utilisé pour le gras, l’italique et la mise en forme des liens, est désormais à la fois déprécié et non standard selon MDN, donc ne construisez pas de nouvelles fonctionnalités de texte enrichi dessus. Il subsiste dans le code hérité car aucun remplacement clé en main complet n’existe. MDN note qu’il préserve encore de manière unique le tampon d’annulation. Pour les nouveaux développements, utilisez les API Selection et Range conjointement avec beforeinput/input. Soyez honnête sur le coût : ce sont des primitives de bas niveau, pas un remplacement direct, et le comportement de Range diffère selon les navigateurs. Pour tout ce qui est non trivial, utilisez un framework d’éditeur dédié.

XSS. Ne restituez jamais le HTML contenteditable saisi par l’utilisateur à d’autres utilisateurs sans le désinfecter au préalable. Une écriture innerHTML non désinfectée est un vecteur d’injection direct. Désinfectez avec DOMPurify (activement maintenu, version actuelle 3.x), ou utilisez l’API Sanitizer native du navigateur lorsque disponible avec DOMPurify en solution de repli :

function safeRender(el, html) {
  if ('setHTML' in Element.prototype) {
    el.setHTML(html);              // native, strips scripts/handlers
  } else {
    el.innerHTML = DOMPurify.sanitize(html);
  }
}

La voie native est véritablement récente. Firefox 148, publié le 24 février 2026, a ajouté la prise en charge de l’API HTML Sanitizer ainsi que des méthodes comme setHTML(), qui désinfecte le HTML avant de l’insérer dans le DOM pour réduire le risque d’attaques XSS. Chrome et Edge ont suivi, mais setHTML() n’est pas encore dans Baseline, donc conservez la solution de repli. Le premier aperçu de l’API HTML Sanitizer par OpenReplay couvre les mécanismes en détail.

Accessibilité

Une région modifiable doit se comporter comme un vrai contrôle. Ajoutez un style :focus visible afin que les utilisateurs au clavier puissent voir où se trouve le curseur, et étiquetez la région. Les éléments contenteditable n’ont pas de nom accessible implicite, donc associez-leur un aria-label ou une étiquette liée :

[contenteditable]:focus {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}
<div contenteditable="plaintext-only" aria-label="Note body" role="textbox"></div>

Les éléments modifiables sont focalisables et participent à la navigation séquentielle au clavier, bien que les éléments modifiables imbriqués ne soient pas ajoutés à l’ordre de tabulation par défaut. Gérez le focus lorsque votre interface change : si un bouton disparaît après que l’utilisateur l’a activé (un contrôle d’annulation qui bascule vers rétablir, par exemple), déplacez le focus vers un élément visible avec .focus() afin que les utilisateurs au clavier ne se retrouvent pas perdus — un point que Scott O’Hara soulève dans son implémentation d’annulation/rétablissement.

Quand utiliser contenteditable, et quand ne pas l’utiliser ?

Utilisez contenteditable pour des modifications légères en ligne : un titre modifiable, un champ à édition par clic, un outil de code/aperçu en direct. Optez pour un contrôle de formulaire classique lorsque vous avez besoin d’une saisie fiable et prévisible, et pour un framework d’éditeur dédié lorsque vous avez besoin de texte enrichi structuré avec une sortie propre.

BesoinMeilleur outil
Texte brut sur une ou plusieurs lignes, soumission de formulaire<input> / <textarea>
Édition en ligne du contenu affiché, texte brutcontenteditable="plaintext-only"
Outil de code/aperçu en direct dans le navigateurcontenteditable
Texte enrichi fiable, contenu structuré/collaboratifBibliothèque d’éditeur (ProseMirror, Lexical, Tiptap)

La décision repose sur la prévisibilité de la sortie. Un <textarea> vous donne une chaîne propre et un vrai événement change ; contenteditable vous donne du HTML rendu dont la forme exacte dépend du navigateur et de ce que l’utilisateur a collé. Une bibliothèque d’éditeur mature existe précisément parce que maîtriser cette sortie (balisage normalisé, modèle de document, historique d’annulation, désinfection) est un problème complexe que quelqu’un a déjà résolu.

Utilisez contenteditable lorsque la surface d’édition est petite et que la sortie est du texte brut ou éphémère. Dès que vous avez besoin d’un HTML structuré fiable, soit contraignez fortement la saisie avec plaintext-only et la désinfection, soit confiez le travail à un outil conçu pour cela.

Questions fréquentes

Est-ce que contenteditable déclenche un événement change lorsque l'utilisateur termine l'édition ?

Non. Un élément contenteditable ne dispose d'aucun événement change natif, c'est pourquoi les anciens tutoriels utilisant keypress ou keyup ratent les collages, le glisser-déposer et la saisie IME. Écoutez plutôt l'événement input, qui se déclenche à chaque modification de l'hôte d'édition, quelle que soit la manière dont la modification a été effectuée. Si vous devez intercepter ou annuler une modification avant que le DOM ne mute, utilisez l'événement beforeinput, qui s'applique également aux éléments contenteditable.

Dois-je utiliser contenteditable ou un textarea pour un champ de texte multiligne ?

Utilisez un textarea pour du texte brut que vous prévoyez de soumettre ou de stocker, car il retourne une chaîne propre et déclenche un vrai événement change. Optez pour contenteditable uniquement lorsque vous avez besoin d'une édition en ligne du contenu affiché en place plutôt qu'un contrôle de formulaire séparé. Si le champ est en texte seul, contenteditable='plaintext-only' est la correspondance la plus proche, car il supprime la mise en forme enrichie collée à la source tout en éditant directement le contenu rendu.

Est-il encore sûr d'utiliser execCommand pour la mise en forme gras et italique ?

Ne construisez pas de nouvelles fonctionnalités de texte enrichi sur execCommand ; MDN le marque comme déprécié et non standard. Il subsiste dans le code hérité car aucun remplacement clé en main complet n'existe et il préserve de manière unique le tampon d'annulation du navigateur. Pour les nouveaux développements, utilisez les API Selection et Range conjointement avec les événements beforeinput et input, bien que ce soient des primitives de bas niveau dont le comportement de Range diffère selon les navigateurs. Pour tout ce qui est non trivial, utilisez une bibliothèque d'éditeur dédiée.

Est-ce que contenteditable plaintext-only fonctionne dans Firefox ?

Oui. La valeur plaintext-only a été intégrée dans Firefox 136 (mars 2025), la rendant compatible avec tous les navigateurs aux côtés du support de longue date dans Chromium et WebKit. Elle rend le texte brut modifiable tout en désactivant la mise en forme enrichie, de sorte que le contenu collé dans un élément plaintext-only est entièrement dépouillé de sa mise en forme. C'est le choix le plus propre pour les champs en texte seul, car il bloque le balisage collé désordonné à la source plutôt que de vous obliger à le désinfecter après coup.

Open-source session replay

Gain control over your UX

See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.