Редактирование в браузере с помощью contentEditable
Редактирование contenteditable в браузере: включите встроенное редактирование текста, ловите input и избегайте ограничений execCommand и XSS.
Любой HTML-элемент становится редактируемым прямо на месте, если добавить атрибут contenteditable — без элементов управления формой, без библиотек и зависимостей.
Если вы когда-либо создавали целую форму только для того, чтобы дать пользователю возможность переименовать один заголовок, при первом знакомстве с этим атрибутом возникает ощущение, что вы нашли секретный приём.
Браузер превращает элемент в хост редактирования, устанавливает курсор и позволяет пользователю вводить текст непосредственно в отрисованный DOM. Это делает contenteditable самым быстрым способом реализовать редактируемый заголовок, поле с редактированием по клику или лёгкую область для заметок. Однако атрибут имеет и подводные камни, о которых в справочнике не упоминается: отсутствие нативного события change, различия в генерируемой разметке между браузерами, устаревший API форматирования, а также то, что вывод результата другим пользователям является классическим вектором XSS-атак. В этой статье рассматривается, как включить редактирование, корректно перехватывать и сохранять правки, обрабатывать граничные случаи и понять, когда стоит обратиться к другим инструментам.
Ключевые выводы
- Атрибут
contenteditableпринимает три значения:true(или пустая строка) делает элемент редактируемым,falseотключает редактирование, аplaintext-onlyпозволяет редактировать чистый текст, удаляя форматирование. - У
contentEditableнет нативного событияchange. Используйте событиеinput, которое срабатывает при каждом изменении хоста редактирования. document.execCommand()для жирного текста, курсива и ссылок является устаревшим и нестандартным; для полноценной работы с форматированием используйте Selection и Range API вместе сbeforeinput/inputили специализированную библиотеку редактора.- Никогда не записывайте введённый пользователем HTML из
contenteditableобратно на страницу без санитизации — используйте DOMPurify или браузерныйsetHTML()там, где он доступен, с резервным вариантом на основе DOMPurify. contenteditable="plaintext-only"теперь поддерживается во всех основных браузерах: поддержка появилась в Firefox 136 (март 2025 года) наряду с давно существующей поддержкой в Chromium и WebKit.
Включение редактирования: атрибут contenteditable
Глобальный атрибут contenteditable принимает три значения, и правильный выбор между ними — это уже половина дела. true (или пустая строка) делает элемент редактируемым; false отключает редактирование; plaintext-only позволяет редактировать чистый текст, отключая форматирование. Согласно справочнику MDN по contenteditable, это перечисляемый атрибут, а не булев: отсутствующее или недопустимое значение наследует редактируемость от родительского элемента.
Минимальный пример:
<h1 contenteditable="true">Edit this heading</h1>
Для текстовых полей (переименовываемый заголовок, ввод тегов, однострочная заметка) предпочтительнее использовать plaintext-only. Это блокирует вставку форматированного содержимого на уровне источника: содержимое, вставленное в элемент с contenteditable="true", сохраняет всё форматирование, тогда как содержимое, вставленное в contenteditable="plaintext-only", лишается его полностью.
Управление редактированием из JavaScript осуществляется через свойство contentEditable (в camelCase):
const el = document.querySelector('#note');
el.contentEditable = 'plaintext-only'; // or 'true' / 'false'
Как перехватывать и сохранять правки в contenteditable?
Discover how at OpenReplay.com.
У contentEditable нет нативного события change. Для перехвата правок используйте событие input, которое срабатывает при каждом изменении хоста редактирования. Это наиболее распространённая ошибка в устаревших руководствах, которые используют keypress или keyup и пропускают вставку, перетаскивание и ввод через IME. Читайте element.innerHTML, если нужно сохранить форматирование, или element.textContent для получения чистого текста, а затем сохраняйте данные и восстанавливайте их при загрузке.
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);
});
Замените textContent на innerHTML, если сохраняете форматированную разметку, но сначала прочитайте раздел о безопасности — именно этот выбор превращает поле для заметок в потенциальную уязвимость. Для более тонкого контроля используйте событие beforeinput, которое срабатывает до мутации DOM и позволяет проверить или отменить правку; оно применяется как к элементам contenteditable, так и к любому элементу в режиме designMode.
Подводные камни
Именно здесь contenteditable оправдывает свою репутацию. В продакшене встречаются три характерные проблемы.
Непредсказуемая и несогласованная разметка. Браузеры по-разному формируют HTML внутри области contenteditable, поэтому сохранённый результат редко бывает таким чистым, как ожидается. Как задокументировал Скотт О’Хара, Safari исторически оборачивал переносы строк в элементы <div>, тогда как Firefox вставлял элементы <br>, а <div> является недопустимым дочерним элементом <p>, что вызывает артефакты отрисовки при редактировании параграфа. Типичная проблема в продакшене — пользователь вставляет текст из Word или Google Docs, принося с собой целый набор вложенных <span> и инлайновых стилей. Записи сессий таких сеансов редактирования позволяют наблюдать за формированием некорректного вывода в реальном времени, вместо того чтобы восстанавливать картину по повреждённой строке в базе данных. Практическое решение — отдавать предпочтение plaintext-only или выполнять санитизацию в обработчиках input/paste.
execCommand устарел. document.execCommand(), долгое время использовавшийся для форматирования жирным, курсивом и создания ссылок, теперь является устаревшим и нестандартным согласно MDN, поэтому не стоит строить на нём новые функции форматирования. Он сохраняется в устаревшем коде, поскольку полноценной замены «из коробки» не существует. MDN отмечает, что он по-прежнему уникально сохраняет буфер отмены. Для новых разработок используйте API Selection и Range совместно с beforeinput/input. Стоит честно оценить затраты: это низкоуровневые примитивы, а не готовая замена, и поведение Range различается в разных браузерах. Для всего нетривиального используйте специализированный фреймворк редактора.
XSS. Никогда не отображайте HTML, введённый пользователем через contenteditable, другим пользователям без предварительной санитизации. Небезопасная запись через innerHTML является прямым вектором инъекции. Выполняйте санитизацию с помощью DOMPurify (активно поддерживается, актуальная версия 3.x) или используйте нативный Sanitizer API браузера там, где он доступен, с резервным вариантом на DOMPurify:
function safeRender(el, html) {
if ('setHTML' in Element.prototype) {
el.setHTML(html); // native, strips scripts/handlers
} else {
el.innerHTML = DOMPurify.sanitize(html);
}
}
Нативный путь является действительно новым. Firefox 148, выпущенный 24 февраля 2026 года, добавил поддержку HTML Sanitizer API вместе с такими методами, как setHTML(), который санитизирует HTML перед вставкой в DOM для снижения риска XSS-атак. Chrome и Edge последовали за ним, однако setHTML() ещё не входит в Baseline, поэтому резервный вариант необходим. Подробный разбор механики приведён в статье OpenReplay «Первый взгляд на HTML Sanitizer API».
Доступность
Редактируемая область должна вести себя как полноценный элемент управления. Добавьте видимые стили :focus, чтобы пользователи клавиатуры видели положение курсора, и подпишите область. Элементы contenteditable не имеют неявного доступного имени, поэтому добавьте aria-label или связанную метку:
[contenteditable]:focus {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
<div contenteditable="plaintext-only" aria-label="Note body" role="textbox"></div>
Редактируемые элементы получают фокус и участвуют в последовательной навигации с клавиатуры, хотя вложенные редактируемые элементы по умолчанию не добавляются в порядок обхода табуляцией. Управляйте фокусом при изменениях интерфейса: если кнопка исчезает после активации пользователем (например, элемент управления отменой, переключающийся в повтор), переместите фокус обратно на видимый элемент с помощью .focus(), чтобы пользователи клавиатуры не потеряли ориентацию — этот момент Скотт О’Хара подчёркивает в своей реализации отмены/повтора.
Когда использовать contenteditable, а когда — нет?
Используйте contenteditable для лёгкого инлайнового редактирования: редактируемый заголовок, поле с редактированием по клику, интерактивная площадка для кода с предпросмотром. Обращайтесь к стандартным элементам управления формой, когда нужен надёжный и предсказуемый ввод, и к специализированным фреймворкам редактора, когда требуется структурированный форматированный текст с чистым выводом.
| Задача | Лучший инструмент |
|---|---|
| Однострочный/многострочный чистый текст, отправка формы | <input> / <textarea> |
| Инлайновое редактирование отображаемого содержимого, чистый текст | contenteditable="plaintext-only" |
| Интерактивная площадка для кода/предпросмотра в браузере | contenteditable |
| Надёжный форматированный текст, структурированный/совместный контент | Библиотека редактора (ProseMirror, Lexical, Tiptap) |
Решение зависит от предсказуемости вывода. <textarea> возвращает чистую строку и генерирует настоящее событие change; contenteditable возвращает отрисованный HTML, точный вид которого зависит от браузера и от того, что вставил пользователь. Зрелые библиотеки редакторов существуют именно потому, что управление этим выводом (нормализованная разметка, модель документа, история отмен, санитизация) — это масштабная задача, которую кто-то уже решил.
Используйте contenteditable, когда область редактирования невелика, а вывод представляет собой чистый текст или данные, не требующие долгосрочного хранения. Как только вам понадобится надёжный структурированный HTML, либо жёстко ограничьте ввод с помощью plaintext-only и санитизации, либо передайте задачу инструменту, созданному для этого.
Часто задаваемые вопросы
Генерирует ли contenteditable событие change, когда пользователь завершает редактирование?
Нет. У элемента contenteditable нет нативного события change, именно поэтому устаревшие руководства, использующие keypress или keyup, пропускают вставку, перетаскивание и ввод через IME. Вместо этого используйте событие input, которое срабатывает при каждом изменении хоста редактирования независимо от способа внесения правки. Если нужно перехватить или отменить правку до мутации DOM, используйте событие beforeinput, которое также применяется к элементам contenteditable.
Что лучше использовать для многострочного текстового поля: contenteditable или textarea?
Используйте textarea для чистого текста, который планируется отправлять или сохранять, поскольку он возвращает чистую строку и генерирует настоящее событие change. Обращайтесь к contenteditable только тогда, когда нужно инлайновое редактирование отображаемого содержимого прямо на месте, а не отдельный элемент управления формой. Если поле предназначено только для текста, contenteditable='plaintext-only' является наиболее близким аналогом, поскольку удаляет форматирование при вставке на уровне источника, при этом позволяя редактировать отрисованное содержимое напрямую.
Безопасно ли использовать execCommand для форматирования жирным и курсивом?
Не стройте новые функции форматирования на execCommand: MDN помечает его как устаревший и нестандартный. Он сохраняется в устаревшем коде, поскольку полноценной готовой замены не существует, а также потому, что уникально сохраняет буфер отмены браузера. Для новых разработок используйте Selection и Range API совместно с событиями beforeinput и input, хотя это низкоуровневые примитивы с различающимся поведением Range в разных браузерах. Для всего нетривиального используйте специализированную библиотеку редактора.
Поддерживается ли contenteditable plaintext-only в Firefox?
Да. Значение plaintext-only появилось в Firefox 136 (март 2025 года), обеспечив кроссбраузерную поддержку наряду с давно существующей поддержкой в Chromium и WebKit. Оно позволяет редактировать чистый текст, отключая форматирование, поэтому содержимое, вставленное в элемент с plaintext-only, лишается всего форматирования. Это делает данный режим наиболее подходящим выбором для текстовых полей, поскольку он блокирует некорректную разметку при вставке на уровне источника, не требуя последующей санитизации.
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