Get to know MDN better
The EditContext API can be used to build rich text editors on the web that support advanced text input experiences, such as Input Method Editor (IME) composition, emoji picker, or any other platform-specific editing-related UI surfaces.
This article goes over the necessary steps to build a text editor using the EditContext API. In this guide, you will review the main steps involved in building a simple HTML code editor that highlights the syntax of the code as you type, and that supports IME composition.
To see the final code, check out the source code on GitHub. It's a good idea to keep the source code open while reading, as the tutorial only shows the most important parts of the code.
The source code is organized into the following files:
To use the live demo, open Edit Context API: HTML editor demo in a browser that supports the EditContext API.
The first step is to create the UI for the editor. The editor is a <div> element with the spellcheck attribute set to false to disable spell checking:
To style the editor element, the following CSS code is used. The code makes the editor fill the entire viewport and scroll when there's too much content to fit. The white-space property is also used to preserve whitespace characters found in the HTML input text, and the tab-size property is used to make tab characters render as two spaces. Finally, some default background, text, and caret colors are set:
To make an element editable on the web, most of the time, you use an <input> element, a <textarea> element, or the contenteditable attribute.
However, with the EditContext API, you can make other types of elements editable without using an attribute. To see the list of elements that can be used with the EditContext API, see Possible elements on the HTMLElement editContext property page.
To make the editor editable, the demo app creates an EditContext instance, passing some initial HTML text to the constructor, and then sets the editContext property of the editor element to the EditContext instance:
These lines of code make the editor element focusable. Entering text in the element fires the textupdate event on the EditContext instance.
To render the syntax-highlighted HTML code in the editor when the user enters text, the demo app uses a function named render() that's called when new text is entered, when characters are deleted, or when the selection is changed.
One of the first things the render() function does is tokenize the HTML text content. Tokenizing the HTML text content is needed to highlight the HTML syntax, and involves reading the HTML code string, and determining where each opening tag, closing tag, attribute, comment node, and text node starts and ends.
The demo app uses the tokenizeHTML() function to achieve this, which iterates over the string character by character while maintaining a state machine. You can see the source code for the tokenizeHTML() function in tokenizer.js, on GitHub.
The function is imported into the demo app HTML file like this:
Whenever the render() function is called, which is when the user enters text, or when the selection changes, the function removes the content in the editor element, and then renders each token as a separate HTML element:
The EditContext API gives the ability to control the way the edited text is rendered. The above function renders it by using HTML elements, but it could render it in any other way, including by rendering it into a <canvas> element.
The demo app runs the render() function when necessary. This includes once when the app starts, and then again when the user enters text, by listening to the textupdate event:
As seen in the previous render() function code example, each token is given a class name that corresponds to the type of token it is. The demo app uses this class name to style the tokens, using CSS, as shown below:
Even though the demo app uses a <div> element for the editor, which already supports displaying a blinking text cursor and highlighting user selections, the EditContext API still requires to render the selection. This is because the EditContext API can be used with other types of elements that don't support these behaviors. Rendering the selection ourselves also gives us more control over how the selection is displayed. Finally, because the render() function clears the HTML content of the editor element every time it runs, any selection that the user might have made is lost the next time the render() function runs.
To render the selection, the demo app uses the Selection.setBaseAndExtent() method at the end of the render() function. To use the setBaseAndExtent() method, we need a pair of DOM nodes and character offsets that represent the start and end of the selection. However, the EditContext API maintains the state for the current selection only as a pair of start and end character offsets into the entire edit buffer. The demo app code uses another function, called fromOffsetsToSelection() that's used to convert these character offsets into four values:
You can see the code for the fromOffsetsToSelection() function in the converter.js file.
The EditContext API gives us a lot of flexibility to define our own text editor UI. However, this also means that we need to handle some things that are usually handled by the browser or operating system (OS).
For example, we must tell the OS where the editable text region is located on the page. This way, the OS can correctly position any text-editing UI that the user might be composing text with, such as an IME composition window.
The demo app uses the EditContext.updateControlBounds() method, providing it with a DOMRect object that represents the bounds of the editable text region. The demo app calls this method when the editor is initialized, and again when the window is resized:
The textupdate event used in the previous section isn't fired when the user presses the Tab or Enter keys, so we need to handle these keys separately.
To handle them, the demo app uses an event listener for the keydown event on the editor element, and uses this listener to update the EditContext instance's text content and selection, as shown below:
The above code also calls the updateSelection() function to update the selection after the text content has been updated. See Updating the selection state and selection bounds, below, for more information.
We could improve the code by handling other key combinations, such as Ctrl+C and Ctrl+V to copy and paste text, or Ctrl+Z and Ctrl+Y to undo and redo text changes.
As we saw earlier, the render() function handles rendering the current user selection in the editor element. But the demo app also needs to update the selection state and bounds when the user changes the selection. The EditContext API doesn't do this automatically, again because the editor UI might be implemented in a different way, such as by using a <canvas> element.
To know when the user changes the selection, the demo app uses the selectionchange event and the Document.getSelection() method, which provide a Selection object, telling us where the user's selection is. Using this information the demo app updates the EditContext selection state and selection bounds by using the EditContext.updateSelection() and EditContext.updateSelectionBounds() methods. This is used by the OS to position the IME composition window correctly.
However, because the EditContext API uses character offsets to represent the selection, the demo app also uses a function, fromSelectionToOffsets() that converts DOM selection objects to character offsets.
You can see the code for the fromSelectionToOffsets() function in the converter.js file.
On top of using the EditContext.updateControlBounds() and EditContext.updateSelectionBounds() methods to help the OS position a text editing UI that the user might be using, there's one more bit of information that the OS requires: the position and size of certain characters within the editor element.
To do this, the demo app listens to the characterboundsupdate event, uses it to calculate the bounds of some of the characters in the editor element, and then uses the EditContext.updateCharacterBounds() method to update the character bounds.
As seen before, the EditContext API only knows about character offsets, which means that the characterboundsupdate event provides the start and end offsets for the characters it needs bounds for. The demo app uses another function, fromOffsetsToRenderedTokenNodes(), to find the DOM elements that these characters have been rendered in, and uses this information to calculate the required bounds.
You can see the code for the fromOffsetsToRenderedTokenNodes() function in the converter.js file.
The demo app goes through a final step to fully support IME composition. When the user is composing text with an IME, the IME might decide that certain parts of the text being composed should be formatted differently to indicate the composition state. For example, the IME might decide to underline the text.
Because it's the demo app's responsibility to render the content in the editable text region, it's also its responsibility to apply the necessary IME formatting. The demo app achieves this by listening to the textformatupdate event to know when the IME wants to apply text formats, where, and what formats to apply.
As shown in the following code snippet, the demo app uses the textformatupdate event and the fromOffsetsToSelection() function again to find the text range that the IME composition wants to format:
The above event handler calls the function named addHighlight() to format text. This function uses the CSS Custom Highlight API to render the text formats. The CSS Custom Highlight API provides a mechanism for styling arbitrary text ranges by using JavaScript to create the ranges, and CSS to style them. To use this API, the ::highlight() pseudo-element is used to define the highlight styles:
Highlight instances are also created, stored in an object, and registered in the HighlightRegistry by using the CSS.highlights property:
With this in place, the addHighlight() function uses Range objects for the ranges that need to be styled, and adds them to the Highlight object:
This article showed you how to use the EditContext API to build a simple HTML code editor that supports IME composition and syntax highlighting.
The final code and live demo can be found on GitHub: live demo and source code.
More importantly, this article showed you that the EditContext API provides a lot of flexibility when it comes to the user interface of your editor. Based on this demo, you could build a similar text editor that uses a <canvas> element to render the syntax-highlighted HTML code instead of the <div> that the demo uses. You could also change how each token is rendered, or how the selection is rendered.
This page was last modified on Aug 20, 2025 by MDN contributors.
Your blueprint for a better internet.
Visit Mozilla Corporation’s not-for-profit parent, the Mozilla Foundation.
Portions of this content are ©1998–2026 by individual mozilla.org contributors. Content available under a Creative Commons license.