Introducing a new way to build custom web editing experiences using the EditContext API

It has not always been a simple task for developers to incorporate advanced editing capabilities in their web applications. The web platform provides editability for both plain text and HTML documents using elements like <input> and <textarea>, or by applying the contenteditable attribute to elements. However, the basic capabilities of these element types are often not sufficient for what developers want to achieve in their apps.

Developers have often ended up implementing their own custom editor view that implements the functionality their users need. The editor view might be built with a complex DOM—or even with a <canvas> element—but since the only way for the developer to receive text input requires a focused editable element, they'll still need to place a hidden contenteditable element on their page somewhere.

The result is while the user appears to be directly editing the content in the app's custom editor view, the developer is actually receiving the input with event handlers in the hidden element, and then mirroring it into the visible editor view. This can lead to problems as the developer ends up fighting with the browser-default editing behavior in the hidden contenteditable element.

To address this class of issues, the Microsoft Edge team has driven the standardization of EditContext, a new web platform API that allows developers to receive text input directly without being tied to the browser's default editing behaviors.

A real-world example

For example, when users are collaborating in Word Online. Users can co-edit and see each other's changes and cursor positions. However, if one collaborator is using an Input Method Editor (IME) window to compose Japanese text, for example, their editor won't be updated to show changes from other users until the IME user has finished their composition. This is because making changes to the area of the DOM being edited while there's an active IME composition can cause the composition to be prematurely canceled. The application must wait until the IME window is closed to update the view, which may cause delays and hinder collaboration.

Trouble collaborating in Word Online while composing text

To provide both a better developer and user experience, developers need a way to decouple text input from the HTML DOM view. The EditContext API is the solution to this problem.

The basics of EditContext

With EditContext, you can receive text and composition input directly through the EditContext API surface, rather than through observing changes to the DOM. This allows for tighter control over how the input is handled, and even allows for adding editability to the <canvas> element.

Associating an EditContext instance with an element makes it editable:

// This will be our editable element.
const element = document.querySelector('#editor-element');

// Creating the EditContext object.
const editContext = new EditContext();

// Associating the EditContext object with our DOM element.
// The element is now focusable and can receive text input.
element.editContext = editContext;

// In order to render the text typed by the user onto the
// page, as well as the user's selection, you'll need to
// receive the input in a textupdate event callback.
editContext.addEventListener('textupdate', event => {
  element.textContent = editContext.text;

  // For brevity, the code to render the selection
  // isn't shown here.
    renderSelection(event.selectionStart, event.selectionEnd);
 });

Responsibilities of the author

Using the EditContext API makes it easier to support advanced input methods such as IME composition windows, emoji pickers, and other operating system input surfaces. To make all of this possible in your editable element, the EditContext API requires some information. In addition to rendering the text and selection, there are some other things that you must do when using the EditContext API.

Managing the side of an editable region, or if the user's selection changes

Call the updateControlBounds() and updateSelectionBounds() methods to inform the EditContext instance whenever the size of the editable region or the user's selection changes. This helps the platform decide where to show IME windows and other platform-specific editing UI.

// It's necessary to provide bounds information because EditContext
// is generic enough to work with any type of web editor, even
// <canvas>-based editors. The API doesn't make any assumptions as
// to how the editor is implemented or how the selection is rendered.
// Bounds are given in the client coordinate space.
const controlBound = editorElement.getBoundingClientRect();
const selection = document.getSelection();
const selectionBound = selection.getRangeAt(0).getBoundingClientRect();
editContext.updateControlBounds(controlBound);
editContext.updateSelectionBounds(selectionBound);

Managing the position of the editor UI

Listen for the characterboundsupdate event and call updateCharacterBounds() in response to help the platform decide where to show IME windows and other platform-specific editing UI.

Applying formatting

Listen for the textformatupdate event and apply the formatting specified by the event to your editor view. These text decorations are used by IMEs when composing certain languages. For example, a Japanese IME will use an underline to show which part of the text is being actively composed.

A screenshot of an Input Method Editor window used for input of Japanese characters.

Handling rich text editing behaviors

Listen to the beforeinput event to handle any rich text editing behaviors you'd like to support, like hotkeys for bolding or italicizing text, or applying a spell check correction.

Managing changes in user selections

When the user's selection changes due to keyboard or mouse input, you'll need to inform the EditContext instance of the change. This is necessary because of the EditContext API's applicability to a broad number of use cases, including editors rendered with the <canvas> element where the browser can't detect selection changes automatically.

document.addEventListener('selectionchange', () => {
  const selection = document.getSelection();

  // EditContext doesn't handle caret navigation, so all the caret navigation/selection that happens
  // in DOM space needs to be mapped to plain text space by the author and passed to EditContext.
  // This example code assumes the editable area only contains text under a single node.
  editContext.updateSelection(selection.anchorOffset, selection.focusOffset);
});

If the element you're using with EditContext is a <canvas> element, you'll also need to implement selection and caret navigation behaviors, such as navigating through text with arrow keys. Additionally, the browser's built-in spell check only works in non-<canvas> elements.

EditContext versus contenteditable

EditContext is a great choice if you're implementing a fully-featured editor and want to have complete control over how text input is handled, or if you're adding advanced features like co-editing with multiple users. However, given all the preceding requirements for using EditContext, if all you need is simple text editing support, you'll likely still want to use <input>, <textarea> elements, or the contenteditable attribute.

Looking forward

The Microsoft Edge team has implemented EditContext in Chromium through a collaboration with Chrome engineers, and is shipping with release 121 (January 2024) of both Chrome and Edge. For now, it's only available in Chromium-based browsers, but you can read Mozilla's and WebKit's positions on the EditContext API.

We want it to be easier for web developers to build powerful custom editing experiences on the web, and we believe that the EditContext API achieves this by addressing existing challenges and offering a more direct way to handle text input.

If you want to learn more about the API, consult the MDN documentation. To submit feedback about the design of the API, open an issue in the EditContext API's Github repository. To report bugs with the implementation of the API, submit a bug at crbug.com.