
Total Views
10
Read Time
17 min read
Updated On
22.10.2026
Introduction
Why You Should Not Build on contentEditable in 2026 (and What to Use)
Raw contentEditable looks like a free rich text editor until paste, IME, undo and mobile bugs arrive. Here's why it breaks in 2026 and what to use instead.
TL;DR
Don't build a rich text editor on raw contentEditable. Browser output differs, execCommand is deprecated, and IME, paste and undo break. Use Lexical, ProseMirror or a drop-in like Eddyter.

Content
Here's how it usually goes. You need a comment box with bold, italics and links. You add contenteditable="true" to a div, wire up three toolbar buttons with document.execCommand, and it works on your laptop by lunch.
Then the bug reports start. Pasting from Google Docs brings in a font size nobody chose. Pressing Enter in Safari creates a <div> where Chrome made a <br>. A user typing Japanese sees characters duplicate. Undo wipes out half a paragraph. Your "one-day feature" is now a quarter of someone's year.
This post explains why raw contentEditable is the wrong foundation for a rich text editor in 2026, what the serious editors do instead, and how to pick the right replacement for your app.
The short answer
Don't build a rich text editor directly on raw contentEditable. The browser's editing behavior differs between engines, document.execCommand is deprecated, and IME, paste, undo and mobile input all need custom handling. Use a model-based framework (Lexical, ProseMirror, Tiptap) or a drop-in editor like Eddyter instead.
That's the whole argument. The rest of this article is the evidence, plus a decision table so you can choose quickly.
What contentEditable gives you (and what it leaves out)
contentEditable is an HTML attribute that makes any element editable. The browser handles the caret, text insertion, spellcheck and the on-screen keyboard. For a plain inline field, that's a good deal.
What it doesn't give you is a document model. The DOM is your data. Every browser decides for itself what HTML to produce when the user types, deletes, presses Enter or pastes. You get whatever markup the engine felt like generating, and you're expected to store it, sanitize it, render it again and keep it consistent across Chrome, Firefox and Safari.
People have been writing about this for over a decade. Nick Santos at Medium published "Why ContentEditable is Terrible" in 2014. Piotr Koszuliński of CKEditor followed with "ContentEditable: The Good, the Bad and the Ugly" in 2015. Both teams reached the same conclusion: keep your own model, and treat the DOM as a render target.
Ten years later, that's still the right call.
8 ways raw contentEditable breaks in production
1. The same text on screen can be different HTML underneath
Make the word "launch" bold and italic. The browser might store it as <strong><em>launch</em></strong>, or <em><strong>launch</strong></em>, or <b><i>launch</i></b>, or split it across several tags. They all look identical.
They don't behave identically. Put the caret right before "launch" and start typing. Depending on which DOM position the browser picks, the new text comes out bold, italic, or plain. Santos's Medium post walks through exactly this case: one visible caret position mapping to several DOM positions.
Now multiply that by every format your toolbar supports. Your "is this text bold?" check needs to handle every variant, forever.
2. The Enter key can't agree on what a paragraph is
Press Enter inside a contentEditable element and, depending on the browser and the surrounding markup, you'll get a <div>, a <p>, or a <br>. CKEditor's write-up lists this as one of the first problems any editor team hits.
If you save that HTML to a database and render it somewhere else (an email, a mobile app, a PDF export), those differences become visible spacing bugs. Then someone writes a regex to "normalize" them. Then the regex breaks.
3. execCommand is deprecated, and it was your toolbar
Most tutorials for "build a rich text editor" still reach for document.execCommand('bold'). MDN marks execCommand() as deprecated and non-standard, with no current specification. Its warning says the method is not implemented consistently by browsers, and that this isn't expected to change.
MDN's own notes list the gaps. formatBlock with headings isn't supported in Safari. Since Firefox 82, nested execCommand() calls always fail. beforeinput and input events may or may not fire, depending on browser and configuration.
So the one API that made raw contentEditable feel productive is the one you shouldn't start a new project on.
4. Paste drags in someone else's CSS
Copy a paragraph from Google Docs, Word or a web page and paste it into a raw contentEditable div. You'll get inline styles, <span> soup, class names from another app and sometimes invisible characters.
You now need a paste sanitizer that decides which tags survive, how lists map, what happens to tables and images, and how to strip styles without losing meaning. That's a project on its own.
5. IME and composition input break silently
This one hurts because you probably won't see it in testing.
Languages like Japanese, Chinese and Korean use an Input Method Editor (IME). The user types several keystrokes, picks a candidate, and only then is the text committed. If your code touches the DOM during that composition (to re-render, apply formatting or sync state), the browser can cancel or duplicate the input.
Google's own EditContext announcement describes this exact problem: changing the edited DOM during an active IME composition can cancel it. CKEditor's article adds keyboard layouts where one character takes two keys, and word navigation that fails in scripts without spaces.
If your users are global, this is a correctness bug, not a polish item.
6. Undo fights your app state
The browser keeps its own undo stack for contentEditable. Your app (React state, a Redux store, autosave) keeps another. Once you modify the DOM programmatically, those two histories drift.
The result: Ctrl+Z undoes something the user didn't expect, or nothing at all. CKEditor's team noted that mutation observers can reset the selection and bypass the native undo manager. Fixing it means owning undo yourself, which means you need a model. (See where this is going?)
7. React re-renders move the cursor
React wants to own the DOM. contentEditable lets the browser own the DOM. Put them together and React will happily re-render the element on a state change, the browser loses track of the selection, and the caret jumps to the start of the field.
Most teams patch this with suppressContentEditableWarning, a ref, and code that saves and restores the selection range on every keystroke. It works until you add mentions, embeds or code blocks, which are exactly the structured nodes that raw contentEditable handles worst.
8. Mobile and accessibility come last
CKEditor's list includes iPad taps that don't open the keyboard, and selection behavior on iPhone that hides the keyboard. Screen reader support for a hand-rolled editor is usually whatever the browser provides by accident. Neither shows up in a desktop Chrome demo, and both show up in your support inbox.
"But Lexical and ProseMirror use contentEditable too"
They do. And this is the most important point in the article.
Lexical, ProseMirror and Tiptap all render into a contentEditable element, because that's how you get the native caret, spellcheck and mobile keyboard. The difference is who's in charge.
- Raw approach: the browser mutates the DOM, and you read the HTML back as your data.
- Model-based approach: the editor intercepts input, turns it into an operation on its own document model, then renders the DOM from that model.
With a model, "bold" is one flag on a text node. Paste goes through a parser into known node types. Undo is a list of transactions the editor controls. IME composition is tracked and the editor holds off on DOM writes until it's done. The browser's quirks still exist, but someone else has already written (and tested) the code that absorbs them.
So the title of this post really means: don't build on raw contentEditable and execCommand. Build on something that treats contentEditable as a rendering surface, not a data store.
What about the EditContext API?
EditContext is the platform's attempt at a cleaner foundation. It lets an editor receive text and IME input directly, without fighting the browser's default editing behavior in a hidden contentEditable element. It shipped in Chrome and Edge 121 in January 2024, with the Microsoft Edge team driving standardization.
It's promising. It's also not something to build a product on alone today. MDN marks EditContext as experimental and not Baseline, and its compatibility table shows Chromium-based browsers supporting it while Firefox and Safari don't. Check MDN's compatibility data before you depend on it.
Even Google's announcement says that for simple text editing, <input>, <textarea> or contentEditable are still likely the better choice. EditContext is a tool for people writing editor frameworks. If you're shipping a product feature, let your editor library adopt it when it's ready.
What to use instead in 2026
Here's how the realistic options compare.
Option | Best for | What you own |
|---|---|---|
Raw contentEditable | A single inline field, like renaming a title | Every browser quirk, paste, undo, IME |
| ProseMirror | Custom editors with real-time collaboration, teams with editor experience | Schema, plugins, UI, toolbar, hosting |
Tiptap | Teams that want ProseMirror's model with a friendlier extension API | Extensions, UI, toolbar, storage |
| Lexical | React-heavy apps that want an immutable state model | Plugins, UI, toolbar, uploads, AI |
Drop-in editor (Eddyter, TinyMCE, CKEditor) | Shipping a full editor this week | Configuration and your content |
A few facts worth knowing before you pick:
- Draft.js is done. Meta's older React editor went into maintenance mode, and its README says the repo was fully archived on 31 December 2022, pointing users toward Meta's new framework. That framework is Lexical, which now powers posts and comments on Facebook.
- ProseMirror has the strongest collaboration story. Every change is a sequence of steps that can be rebased against concurrent edits. The trade-off is the steepest learning curve of the three.
- Tiptap sits on top of ProseMirror. You get the same model with less schema boilerplate and bindings for React, Vue and Svelte.
- A framework is still a framework. Lexical, ProseMirror and Tiptap fix the data-model problem. They don't give you a finished toolbar, image uploads, storage, comments or AI writing. You build those.
If you want a wider comparison, we tested ten options in Top 10 Rich Text Editors for Developers in 2026.
A quick way to decide
- You're building an editor as your product (a Notion-style app, a collaborative doc tool): pick ProseMirror or Lexical and budget for a dedicated engineer.
- You need rich text inside a product that does something else (a CMS, a support tool, an LMS, an admin panel): use a drop-in editor and spend your engineering time on your actual product.
- You need one editable line: raw contentEditable is fine. Keep it plain text.
The hidden bill of "we'll just use contentEditable"
The cost of raw contentEditable rarely shows up in the first sprint. It shows up as a long tail of tickets.
Here's the list of things you end up owning, roughly in the order they appear:
- A paste sanitizer
- Cross-browser Enter and Backspace handling
- Selection save/restore around re-renders
- Your own undo/redo stack
- IME composition guards
- A storage format decision (HTML, JSON or Markdown; we compared them in How to Store Rich Text Editor Content)
- Image and file uploads, plus somewhere to put the files
- Autosave and draft recovery
- Mobile fixes, then accessibility fixes
None of those are features your users asked for. They're the minimum for an editor that doesn't lose people's work.
Moving to a framework like Lexical removes the first five. The rest is still on you. That's the gap we built Eddyter to close: by our own estimate, assembling a production editor with AI features from parts takes 100+ hours, and Eddyter is designed to be running in about 10 minutes.
Switching to Eddyter: what it looks like
Eddyter is a drop-in rich text editor built on Lexical, so you get a model-based core without writing the model layer yourself. On top of that it ships the parts frameworks leave out: AI writing and rewriting, voice-to-text, inline comments, grammar fixes, managed file storage, and a settings panel that lets you toggle toolbar features without a redeploy.
It runs in React, Next.js, Vue, Angular, Svelte, Laravel and plain JavaScript.
Install it:
Drop it into a React or Next.js component:
Add your key (every plan, including Free, includes one) to .env.local as NEXT_PUBLIC_EDDYTER_API_KEY, run your dev server, and you have a working editor with a toolbar. The full walkthrough is on the quick install page, and framework-specific guides are in the docs.
On cost: Eddyter uses flat subscription pricing instead of charging per monthly active user or per editor load, and is priced to be 3x cheaper than TinyMCE. Plans are on the pricing page.
Who should ignore this advice
Short list. If your field is a single line of plain text (a tag name, a title rename, a chat input with no formatting), raw contentEditable or even a <textarea> is the simpler, better choice. And if you're on an editor-framework team experimenting with EditContext, you're the audience this platform work is meant for.
Everyone else is building a product, not an editor engine.
Frequently asked questions
Is contentEditable deprecated?
No. The contenteditable attribute is part of the HTML standard and isn't deprecated. What is deprecated is document.execCommand(), the API most hand-built editors use for bold, italic, lists and links. MDN advises against using execCommand in new projects.
Does Lexical use contentEditable?
Yes. Lexical, ProseMirror and Tiptap all render into a contentEditable element to get the native caret, spellcheck and mobile keyboard. The difference is that they keep their own document model and render the DOM from it, instead of treating the browser's HTML output as the source of truth.
Can I use contentEditable with React?
You can, but React and contentEditable both want to control the same DOM, which causes caret jumps and lost selections on re-render. For anything beyond plain text, use a React-ready editor such as Lexical or a drop-in like Eddyter that manages this for you.
What is the best alternative to contentEditable in 2026?
For a custom editor product, ProseMirror (best for collaboration) or Lexical (best for React). For rich text inside a SaaS app, CMS or internal tool, a drop-in editor such as Eddyter, TinyMCE or CKEditor gets you to production fastest.
Is the EditContext API ready for production?
Not as your only foundation. It shipped in Chrome and Edge 121 in January 2024, but MDN still lists it as experimental and not Baseline, with no support in Firefox or Safari in its compatibility table. Let your editor library adopt it when cross-browser support lands.
The bottom line
Raw contentEditable is a great attribute and a bad architecture. It hands you a caret and a keyboard, then leaves your data model to whatever each browser decides to generate. Every serious editor of the last decade, from Medium's to Meta's, solved this the same way: own the model, render the DOM.
You can build that layer yourself. Or you can install Eddyter, get a Lexical-based editor with AI, uploads and comments already wired up, and get back to the product your users are paying for.

Written by
Shreya Taneja
Project Manager

