Edição no Navegador com contentEditable
Edição contenteditable no navegador: ative texto inline, capture eventos input, lide com limites do execCommand e evite XSS.
Qualquer elemento HTML torna-se editável diretamente quando se adiciona o atributo contenteditable — sem controles de formulário, sem bibliotecas, sem dependências.
Se você já construiu um formulário inteiro apenas para permitir que alguém renomeie um título, na primeira vez que experimentar isso vai parecer um truque de mágica.
O navegador transforma o elemento em um host de edição, posiciona o cursor e permite que o usuário digite diretamente no DOM renderizado. Isso faz do contenteditable a forma mais rápida de implementar um título editável, um campo de edição por clique ou uma área de notas leve. No entanto, o atributo também apresenta armadilhas que a documentação oficial raramente menciona: não há um evento change nativo, a marcação gerada varia entre navegadores, a antiga API de formatação está depreciada, e renderizar o resultado para outros usuários é um vetor clássico de XSS. Este artigo explica como ativá-lo, capturar e persistir edições corretamente, lidar com esses casos extremos e decidir quando recorrer a outra solução.
Principais Conclusões
- O atributo
contenteditableaceita três valores:true(ou uma string vazia) torna um elemento editável,falseo desativa, eplaintext-onlypermite a edição de texto puro enquanto remove a formatação rich-text. contentEditablenão possui um eventochangenativo. Utilize o eventoinput, que dispara a cada modificação no host de edição.document.execCommand()para negrito, itálico e links está depreciado e não é padronizado; use as APIs Selection e Range combeforeinput/input, ou uma biblioteca de editor dedicada, para rich text de verdade.- Nunca escreva HTML inserido pelo usuário via
contenteditablede volta na página sem sanitizá-lo — use DOMPurify, ou o método nativosetHTML()do navegador onde disponível, com DOMPurify como fallback. contenteditable="plaintext-only"agora funciona em todos os navegadores modernos, tendo sido implementado no Firefox 136 (março de 2025), juntamente com o suporte já existente no Chromium e no WebKit.
Como ativar: o atributo contenteditable
O atributo global contenteditable aceita três valores, e escolher o correto é a maior parte do desafio. true (ou uma string vazia) torna um elemento editável; false o desativa; e plaintext-only torna o texto puro editável enquanto desativa a formatação rich-text. Conforme a referência do MDN sobre contenteditable, trata-se de um atributo enumerado, não booleano: um valor ausente ou inválido herda a editabilidade do elemento pai.
A versão de uma linha:
<h1 contenteditable="true">Edit this heading</h1>
Para um campo somente texto (um título renomeável, uma entrada de tag, uma nota de linha única), prefira plaintext-only. Ele bloqueia a formatação rich-text colada na origem: o conteúdo colado em um elemento com contenteditable="true" mantém toda a formatação, enquanto o conteúdo colado em contenteditable="plaintext-only" tem toda a formatação removida.
Alterne a edição via JavaScript através da propriedade contentEditable (em camelCase):
const el = document.querySelector('#note');
el.contentEditable = 'plaintext-only'; // ou 'true' / 'false'
Como capturar e persistir edições com contenteditable?
Discover how at OpenReplay.com.
contentEditable não possui um evento change nativo. Para capturar edições, utilize o evento input, que dispara a cada modificação no host de edição. Este é o erro mais comum em tutoriais antigos, que recorrem a keypress ou keyup e perdem colagens, arrastar e soltar e entrada via IME. Leia element.innerHTML quando precisar preservar a formatação, ou element.textContent quando quiser texto puro, e então persista e restaure ao carregar.
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);
});
Substitua textContent por innerHTML se estiver armazenando marcação rich-text, mas leia a seção de segurança primeiro, pois essa escolha é o que transforma um campo de notas em uma superfície de ataque. Para um controle mais refinado, o evento beforeinput dispara antes da mutação do DOM e permite inspecionar ou cancelar uma edição; ele se aplica a elementos contenteditable e a qualquer elemento em designMode.
As armadilhas
É aqui que o contenteditable justifica sua reputação. Três problemas surgem em produção.
Marcação inconsistente e desorganizada. Os navegadores divergem no HTML que uma região contenteditable produz, portanto o resultado salvo raramente é tão limpo quanto o esperado. Como Scott O’Hara documentou, o Safari historicamente envolvia quebras de linha em elementos <div> enquanto o Firefox inseria elementos <br>, e <div> é um filho inválido de <p>, o que causa problemas de renderização se você tornou um parágrafo editável. Um modo de falha comum em produção é um usuário colando conteúdo do Word ou do Google Docs e arrastando uma sopa de wrappers <span> e estilos inline; replays de sessão dessas sessões de edição são uma forma de observar essa saída malformada sendo gerada em tempo real, em vez de tentar reconstruí-la a partir de uma linha corrompida no banco de dados. A solução artesanal é preferir plaintext-only, ou sanitizar no evento input/paste.
execCommand está depreciado. document.execCommand(), amplamente utilizado para formatação de negrito, itálico e links, agora está depreciado e não é padronizado, conforme o MDN, portanto não construa novos recursos de rich-text sobre ele. Ele sobrevive em código legado porque não existe um substituto completo e imediato. O MDN observa que ele ainda preserva de forma única o buffer de desfazer. Para novos projetos, utilize as APIs Selection e Range em conjunto com beforeinput/input. Seja honesto sobre o custo: são primitivas de baixo nível, não um substituto direto, e o comportamento de Range difere entre navegadores. Para qualquer coisa não trivial, use um framework de editor dedicado.
XSS. Nunca renderize HTML inserido pelo usuário via contenteditable de volta para outros usuários sem sanitizá-lo primeiro. Uma escrita innerHTML sem sanitização é um vetor de injeção direto. Sanitize com DOMPurify (ativamente mantido, versão atual 3.x), ou use a API Sanitizer nativa do navegador onde disponível, com DOMPurify como fallback:
function safeRender(el, html) {
if ('setHTML' in Element.prototype) {
el.setHTML(html); // native, strips scripts/handlers
} else {
el.innerHTML = DOMPurify.sanitize(html);
}
}
O caminho nativo é genuinamente novo. O Firefox 148, lançado em 24 de fevereiro de 2026, adicionou suporte à API HTML Sanitizer juntamente com métodos como setHTML(), que sanitiza o HTML antes de inseri-lo no DOM para reduzir o risco de ataques XSS. Chrome e Edge seguiram o mesmo caminho, mas setHTML() ainda não é Baseline, portanto mantenha o fallback. O artigo do OpenReplay sobre a primeira análise da API HTML Sanitizer cobre os detalhes técnicos em profundidade.
Acessibilidade
Uma região editável precisa se comportar como um controle real. Adicione estilização visível com :focus para que usuários de teclado possam ver onde está o cursor, e rotule a região. Elementos contenteditable não possuem um nome acessível implícito, portanto adicione um aria-label ou um rótulo associado:
[contenteditable]:focus {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
<div contenteditable="plaintext-only" aria-label="Note body" role="textbox"></div>
Elementos editáveis são focalizáveis e participam da navegação sequencial por teclado, embora elementos editáveis aninhados não sejam adicionados à ordem de tabulação por padrão. Gerencie o foco quando sua interface mudar: se um botão desaparecer após o usuário ativá-lo (um controle de desfazer que alterna para refazer, por exemplo), mova o foco de volta para um elemento visível com .focus() para que usuários de teclado não fiquem desorientados — um ponto que Scott O’Hara aborda em sua implementação de desfazer/refazer.
Quando usar contenteditable, e quando não usar?
Use contenteditable para edições inline leves: um título editável, um campo de edição por clique, um experimento de código/prévia ao vivo. Recorra a um controle de formulário simples quando precisar de entrada confiável e previsível, e a um framework de editor dedicado quando precisar de rich text estruturado com saída limpa.
| Necessidade | Melhor ferramenta |
|---|---|
| Texto simples de uma ou várias linhas, envio de formulário | <input> / <textarea> |
| Edição inline de conteúdo exibido, texto puro | contenteditable="plaintext-only" |
| Experimento de código/prévia ao vivo no navegador | contenteditable |
| Rich text confiável, conteúdo estruturado/colaborativo | Biblioteca de editor (ProseMirror, Lexical, Tiptap) |
A decisão depende da previsibilidade da saída. Um <textarea> fornece uma string limpa e um evento change real; contenteditable fornece HTML renderizado cuja forma exata depende do navegador e do que o usuário colou. Uma biblioteca de editor madura existe precisamente porque domar essa saída — marcação normalizada, um modelo de documento, histórico de desfazer, sanitização — é um problema complexo que alguém já resolveu.
Recorra ao contenteditable quando a superfície de edição for pequena e a saída for texto puro ou descartável. No momento em que você precisar de HTML estruturado confiável, ou restrinja a entrada com plaintext-only e sanitização, ou delegue o trabalho a uma ferramenta construída para isso.
Perguntas Frequentes
O contenteditable dispara um evento change quando o usuário termina de editar?
Não. Um elemento contenteditable não possui um evento change nativo, razão pela qual tutoriais antigos que usam keypress ou keyup perdem colagens, arrastar e soltar e entrada via IME. Utilize o evento input, que dispara a cada modificação no host de edição, independentemente de como a alteração foi feita. Se você precisar interceptar ou cancelar uma edição antes que o DOM seja mutado, use o evento beforeinput, que também se aplica a elementos contenteditable.
Devo usar contenteditable ou textarea para um campo de texto de várias linhas?
Use textarea para texto puro que você planeja enviar ou armazenar, pois ele retorna uma string limpa e dispara um evento change real. Recorra ao contenteditable apenas quando precisar de edição inline do conteúdo exibido diretamente, sem um controle de formulário separado. Se o campo for somente texto, contenteditable='plaintext-only' é a opção mais adequada, pois remove a formatação rich-text colada na origem enquanto ainda edita o conteúdo renderizado diretamente.
Ainda é seguro usar execCommand para formatação de negrito e itálico?
Não construa novos recursos de rich-text sobre execCommand; o MDN o marca como depreciado e não padronizado. Ele sobrevive em código legado porque não existe um substituto completo e imediato, e porque preserva de forma única o buffer de desfazer do navegador. Para novos projetos, use as APIs Selection e Range em conjunto com os eventos beforeinput e input, embora sejam primitivas de baixo nível com comportamento de Range que difere entre navegadores. Para qualquer coisa não trivial, use uma biblioteca de editor dedicada.
O contenteditable plaintext-only funciona no Firefox?
Sim. O valor plaintext-only foi implementado no Firefox 136 (março de 2025), tornando-o compatível com todos os navegadores modernos, juntamente com o suporte já existente no Chromium e no WebKit. Ele torna o texto puro editável enquanto desativa a formatação rich-text, de modo que o conteúdo colado em um elemento plaintext-only tem toda a formatação removida. Isso o torna a melhor opção para campos somente texto, pois bloqueia marcação desorganizada colada na origem, em vez de exigir que você a sanitize após o fato.
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