loke.dev
Header image for Is the EditContext API the End of Your contenteditable Nightmares?

Is the EditContext API the End of Your contenteditable Nightmares?

Learn how to build more resilient rich-text editors by decoupling your UI from the operating system's native input methods and the brittle DOM-mutation loop.

· 4 min read

I once spent three days trying to figure out why a specific version of Android’s Gboard was duplicating sentences whenever a user hit "Enter" in our custom text editor. It turned out to be a hallucination caused by the browser trying to reconcile a virtual keyboard’s "composition" state with a DOM that I was mutating in real-time.

Building rich-text editors on the web has historically been a choice between two evils: use contenteditable and lose your mind to browser inconsistencies, or build a "headless" editor that fights the browser's native input behavior every step of the way.

The EditContext API is the first real sign that browser vendors finally realize how much we’ve been suffering.

The "Contenteditable" Trap

The fundamental problem with contenteditable is that it ties the data model (the text and state) directly to the view layer (the DOM). When a user types, the browser updates the DOM first, and then you have to try and figure out what it did.

If you try to intercept that update to apply formatting—say, turning *text* into <b>text</b>—the browser's internal cursor gets confused. You end up in a recursive loop of DOM mutations and selection resets. It's brittle, it’s slow, and it’s why libraries like ProseMirror or Slate have such massive codebases just to handle "normalization."

What is EditContext, anyway?

EditContext is a "headless" approach to text input. It allows you to decouple the OS-level input methods (like IMEs for Chinese/Japanese/Korean, handwriting recognition, or even just the basic virtual keyboard) from the DOM.

Instead of the browser saying "I'm going to put this 'A' inside this <div>," the browser says "The user wants to insert 'A' at this offset. You deal with the UI."

Setting it up

Here is how you actually initialize it. You don't need a hidden textarea or a massive contenteditable wrapper. You just create the context and tell the browser where your "editable" area is visually.

const canvas = document.getElementById('editor-canvas');
const editContext = new EditContext();

// Tell the browser this canvas is the "input" surface
canvas.editContext = editContext;

// When the user types, the OS sends updates here
editContext.addEventListener('textupdate', (e) => {
  console.log('New text:', e.updateText);
  console.log('Range to replace:', e.updateRangeStart, e.updateRangeEnd);
  
  // Now YOU update your internal state and re-render your UI
  myApp.updateState(e.updateText, e.updateRangeStart, e.updateRangeEnd);
  myApp.render();
});

The browser now treats your custom element as a first-class citizen for input. It doesn't try to touch your DOM. It just reports the *intent* of the user.

Why this solves the IME nightmare

If you've never had to support Input Method Editors (IMEs), count your blessings. For languages like Japanese, users type phonetic characters that open a small floating window to select the correct Kanji.

In a standard contenteditable world, that floating window is positioned by the browser's best guess. If you're using a custom-rendered editor (like a Canvas-based editor or a heavily virtualized list), that window usually ends up in the top-left corner of the screen. It looks broken.

With EditContext, you can explicitly tell the OS exactly where the "selection" is in coordinate space:

const selectionRect = myApp.getCursorScreenCoords();

editContext.updateSelectionBounds(
  selectionRect // { x, y, width, height }
);

Now, the OS knows exactly where to anchor its UI. It’s a bridge between the low-level OS input and your high-level JavaScript logic.

The catch (because there's always one)

Before you go deleting your Draft.js dependency, let’s talk about the current state of things.

1. Browser Support: As of right now, EditContext is primarily a Chromium affair (Chrome and Edge). Safari and Firefox are still dragging their feet.
2. No "Free" Rendering: You lose everything that comes with contenteditable. There is no native cursor. There is no native text selection blue-highlight. You have to draw the cursor yourself. You have to handle the "click-and-drag to select" logic yourself.
3. Accessibility: Because you're taking full control, you are responsible for telling screen readers what the text is. You'll likely still need an aria-live region or a hidden element with the full text content to ensure the experience isn't a black hole for users with visual impairments.

Is it worth it?

If you are building a simple blog comment section, no. Use a standard <textarea> or a simple contenteditable wrapper.

But if you are building the next Notion, Google Docs, or a code editor where you need pixel-perfect control over how text is rendered, EditContext is the holy grail. It moves the web away from "hacking a document to make it an app" toward "giving apps the tools they need to handle input."

The "nightmare" isn't quite over until Firefox and Safari jump on board, but the path toward a sane, decoupled text editor is finally becoming visible.