Building a great text editor on the web has traditionally meant choosing between two imperfect options: rely on native DOM editing (like contenteditable) and accept limited control, or render everything yourself (often on <canvas>) and spend a lot of effort recreating input behavior—especially for selection, accessibility, and IME (Input Method Editor) support.
The experimental EditContext API aims to bridge that gap. It gives you a browser-supported pipeline for text input and selection—while letting you own the rendering.
What the EditContext API does
At a high level, EditContext acts like a model for editable text:
- It receives text input (typing, delete/backspace, paste-style updates, IME commits).
- It reports and updates selection/caret positions.
- It exposes composition and formatting events needed for IME scenarios.
- It can request character bounds so the browser can correctly position IME candidate windows, selection handles, and other UI—crucial when you’re not using native DOM text layout.
Instead of the browser directly mutating the DOM (as with contenteditable), the browser routes editing operations through your EditContext, and you decide how to update and draw the content.
Why this matters (especially beyond contenteditable)
If you’re building a custom editor (or anything that behaves like one), you often want:
- Consistent behavior across platforms
- Your own rendering pipeline (DOM, canvas, WebGL, etc.)
- Fine control over selection, layout, and formatting
- Better compatibility with IME input (Japanese, Chinese, Korean, etc.)
The EditContext API is designed for exactly those “custom editor” cases—where native editing is either too opinionated or not available.
Device Orientation API Demo
EditContext API Demonstration
Welcome to a demo of the experimental EditContext API.
This API empowers web developers to build custom rich text editors by providing fine-grained control
over text input, selection, and rendering, especially useful for elements like <canvas>.
Note: The EditContext API is an experimental technology and might not be supported in all browsers
without enabling experimental web platform features. This demo includes a basic support check.
Try typing in the "Editable Area" below, selecting text, or using system-level input methods (like IME
for East Asian languages, if configured on your system). Observe how the "Debug Output" reflects the internal
state changes.
Editable Area
Debug Output
Current Text:
Selection Start Index:
Selection End Index:
(Console will show additional event details)
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f4f4f4;
color: #333;
}
h1,
h2 {
color: #0056b3;
}
p {
line-height: 1.5;
}
/* Styling for the custom editable area */
#editable-area {
border: 2px solid #a0a0a0;
/* Default border */
min-height: 100px;
padding: 15px;
margin-top: 15px;
margin-bottom: 25px;
background-color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: text;
/* Indicate it's a text input area */
/* Crucial: remove default outline, EditContext handles focus visually */
outline: none;
/* Make it focusable programmatically and via tabindex */
tabindex: 0;
white-space: pre-wrap;
/* Preserve whitespace and allow wrapping */
font-size: 16px;
line-height: 1.5;
}
/* Highlight when the editable area is focused */
#editable-area:focus {
border-color: dodgerblue;
box-shadow: 0 0 0 3px rgba(30, 144, 255, 0.4);
}
/* Styling for the debug output section */
.debug-output {
background-color: #e9e9e9;
padding: 15px;
border: 1px solid #d0d0d0;
border-radius: 5px;
white-space: pre-wrap;
/* Preserve whitespace in output */
font-family: 'Courier New', monospace;
font-size: 14px;
color: #555;
margin-top: 15px;
}
.debug-output strong {
color: #222;
}
// --- Feature Detection ---
// Always good practice to check if an experimental API is supported.
if (!('EditContext' in window)) {
alert(
'Your browser does not support the EditContext API. This demo will not function as expected.\n\nPlease try a browser that supports this experimental API (e.g., Chrome with "Experimental Web Platform features" enabled via chrome://flags).'
);
document.body.innerHTML = `
EditContext API is not supported in your browser.
Please try a browser that supports this experimental API (e.g., Chrome with experimental web platform features enabled).
You can find more information about its current status on the MDN Web Docs.
`;
throw new Error('EditContext API not supported.');
}
// --- DOM Element References ---
const editableArea = document.querySelector('#editable-area');
const currentTextSpan = document.querySelector('#current-text');
const selectionStartSpan = document.querySelector('#selection-start');
const selectionEndSpan = document.querySelector('#selection-end');
// --- Step 1: Create a new EditContext instance ---
// The EditContext object acts as the "model" for your editable region.
// It manages the text content, selection, and formatting information.
const editContext = new EditContext();
// --- Step 2: Attach the EditContext to the desired DOM element ---
// This is the core step. By assigning an EditContext instance to an
// element's .editContext property, you inform the browser that this
// element should receive text input via EditContext.
// The browser will then route keyboard input and IME composition
// events to this EditContext instance, rather than directly to the DOM element's
// content if it were, for example, `contenteditable="true"`.
editableArea.editContext = editContext;
// --- Internal State Management ---
// These variables represent our editor's "model" derived from EditContext events.
// In a real editor, this might be a more complex data structure.
let currentContent = 'Start typing here...';
let currentSelectionStart = currentContent.length;
let currentSelectionEnd = currentContent.length;
// ADDITION: Add a flag to track if the initial placeholder content is still present.
let isInitialPlaceholder = true;
// --- Function to Update the Visuals ---
// This function is responsible for rendering the `currentContent` and
// `currentSelection` to the `editableArea` and updating the debug output.
// With EditContext, *you* are responsible for the rendering!
const updateDisplay = () => {
// Update the debug output section
// SUGGESTION: Changed JSON.stringify(currentContent) to currentContent for cleaner debug output.
currentTextSpan.textContent = currentContent;
selectionStartSpan.textContent = currentSelectionStart;
selectionEndSpan.textContent = currentSelectionEnd;
// Update the visual content of the editable div.
// This will update the DOM and might remove textNode if content is empty
editableArea.textContent = currentContent;
const selection = window.getSelection();
selection.removeAllRanges(); // Always clear existing selections first
try {
// ADDITION: Explicitly handle the case when content is empty to ensure caret placement
if (currentContent.length === 0) {
// If the content is empty, editableArea.textContent = '' will remove all child nodes.
// We need to re-add an empty text node to allow for caret placement at index 0.
const emptyTextNode = document.createTextNode('');
editableArea.appendChild(emptyTextNode);
const range = document.createRange();
range.setStart(emptyTextNode, 0);
range.setEnd(emptyTextNode, 0);
selection.addRange(range);
} else {
// For non-empty content, proceed as usual
// After editableArea.textContent = currentContent, firstChild will be the new TextNode
const textNode = editableArea.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
const range = document.createRange();
// Ensure selection indices don't exceed the actual text node length
const actualStart = Math.min(currentSelectionStart, textNode.length);
const actualEnd = Math.min(currentSelectionEnd, textNode.length);
range.setStart(textNode, actualStart);
range.setEnd(textNode, actualEnd);
selection.addRange(range);
} else {
console.warn(
'Editable area has content but no valid textNode. Could not set selection properly.'
);
// Fallback: place cursor at the very beginning of the editableArea if no textNode is found
const range = document.createRange();
range.setStart(editableArea, 0);
range.setEnd(editableArea, 0);
selection.addRange(range);
}
}
} catch (e) {
console.error('Error setting DOM selection:', e);
// If an error occurs, selection was already removed at the start of the function.
// The browser might default caret placement.
}
};
// --- EditContext Event Listeners ---
// These events are fired by the EditContext instance, informing our
// custom editor about user input and selection changes.
// --- EditContext Event Listeners ---
// These events are fired by the EditContext instance, informing our
// custom editor about user input and selection changes.
// Event: 'textupdate'
// Fired when the text content managed by the EditContext needs to be updated.
// This is where you receive the actual text typed by the user or composed by an IME.
editContext.addEventListener('textupdate', (event) => {
console.log('EditContext: textupdate event received', event);
let effectiveUpdateStart = event.updateRangeStart;
let effectiveUpdateEnd = event.updateRangeEnd;
const newText = event.text;
// ADDITION: Re-incorporate the placeholder clearing logic
if (isInitialPlaceholder) {
currentContent = ''; // Clear the placeholder content
// Adjust the effective update range to apply to the now empty string.
// This ensures the new text is inserted correctly at the beginning,
// effectively replacing the placeholder entirely.
effectiveUpdateStart = 0;
effectiveUpdateEnd = 0;
isInitialPlaceholder = false; // The placeholder is no longer active
}
// Apply the text update to our internal `currentContent` model.
// This logic correctly handles both insertions and deletions based on the effective range.
currentContent =
currentContent.substring(0, effectiveUpdateStart) +
newText +
currentContent.substring(effectiveUpdateEnd);
// Adopt the suggested new selection position after the text update.
// Use event.selectionStart and event.selectionEnd
currentSelectionStart = event.selectionStart; // Corrected property name
currentSelectionEnd = event.selectionEnd; // Corrected property name
// --- Diagnostic Log ---
console.log(
`EditContext: TextUpdate - newSelectionStart: ${currentSelectionStart}, newSelectionEnd: ${currentSelectionEnd}`
);
// Re-render the editor's display to reflect the changes.
updateDisplay();
});
// Event: 'selectionchange'
// Fired when the text selection within the editable content changes.
// This happens when the user clicks, drags, or uses arrow keys to move the cursor.
editContext.addEventListener('selectionchange', (event) => {
console.log('EditContext: selectionchange event received', event);
// `start` and `end`: The new start and end indices of the selection.
currentSelectionStart = event.start;
currentSelectionEnd = event.end;
// --- Diagnostic Log ---
console.log(
`EditContext: SelectionChange - start: ${currentSelectionStart}, end: ${currentSelectionEnd}`
);
// Re-render the display to show the updated selection.
updateDisplay();
});
// Event: 'characterboundsupdate'
// Fired when the browser needs to know the precise pixel bounds of characters
// within a given range of text. This is critical for positioning things like
// IME composition windows, spell-check underlines, or selection handles,
// especially when rendering text on a Demo Highlights
The EditContext API code above centers around a handful of events that drive your editor’s behavior: textupdate delivers the actual text edits (insertions/deletions or committed IME text) along with the range to replace and the suggested post-update selection, selectionchange reports caret/selection movement from mouse or keyboard navigation, composition events (compositionstart, compositionupdate, compositionend) expose the lifecycle of IME input so you can display and style in-progress composition text, and characterboundsupdate requests pixel-accurate character rectangles so the browser can correctly position IME candidate windows and related UI—especially important when you render text yourself (e.g., on <canvas>).
The experimental EditContext API gives web developers a new way to build fully custom text editors by separating input/selection handling from rendering: the browser routes typing, selection changes, and IME composition through an EditContext, while your code maintains the text/selection state and renders it however you want (including in non-DOM scenarios like <canvas>).
More To Explore

EditContext API: A New Foundation for Custom Web Editors
The experimental EditContext API gives developers a new foundation for building custom rich text editors by separating text input and selection from rendering. Instead of relying on contenteditable, you attach an EditContext to a focusable element and manage your own text model, selection state, and UI updates—while still receiving browser-grade events for typing, caret movement, and IME composition. This demo highlights the core event flow and why character bounds matter for accurate input UI, especially in custom-rendered editors.

Device Posture API: Detect Foldable Screen Posture on Web
The Device Posture API helps web apps adapt to foldable devices by detecting whether a screen is continuous or folded and reacting to posture changes.