Library / SDK
FormidableLabs/use-editable avatar
FormidableLabs/use-editable

use-editable makes a contenteditable element safe to render with React

A small React hook to turn elements into fully renderable & editable content surfaces, like code editors, using contenteditable (and magic)

620 stars16 forksTypeScriptMIT

At a glance

What is it?
use-editable is a small React hook that turns any element into an editable surface while still letting you render normal React children into it, using a MutationObserver to roll back direct DOM edits before React sees them. It suits small in-app code and prose editors that cannot justify a full editor framework.
Who is it for?
Adopt use-editable if you need a small editable surface in a React app, a code snippet box or a prose field, and you want to keep rendering React children into it rather than dropping to innerHTML. Do not adopt it if you need a full document editor, since the hook only gives back the element's text content and cannot reconstruct structure.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 4 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The three options for editing in React, and the gap between them

The project documentation frames the problem by listing what a React developer has traditionally had to choose between. You can go for a large project like ProseMirror or CodeMirror, which take control over much of the editing and rendering events and are therefore rather opinionated. You can use contenteditable and render to raw HTML that is replaced in the element's content. Or you can combine a textarea with an overlapping div that renders stylised content. The documentation states plainly that all three do not allow much customisation in terms of what actually gets rendered, or put unreasonable restrictions on how easy it is to render and manage an editable's content. use-editable is the fourth answer, and the one it is built for is the second option done properly. The audience is React developers who want an editable surface where the content is still rendered by React, because they need styling, components and live layout inside the field, but the field is small enough that a full editor framework is overkill. The stated size of the whole thing is about 2kB, which is the number that makes this trade make sense.

How the hook fights the DOM and React at the same time

The mechanism is the interesting part, and the documentation explains it carefully. Rendering to a contenteditable element is hard because the user edits the DOM directly, which causes most rendering libraries, React and Preact among them, to become confused, since their virtual DOMs no longer match the actual DOM structure. The fix is a MutationObserver watching all changes made to the contenteditable element. Before it reports those changes to React, it rolls back all changes to the DOM, so React sees what it expects. It also preserves the current position of the caret and the selection, and restores it once React has updated the DOM itself. The documentation is honest that this caret and selection technique is a rather common one for contenteditable editors, and that the MutationObserver addition is the part that lets another view library update the element's content. That is an accurate and useful distinction. The rollback makes React and the browser agree about the DOM, and the observer is the mechanism that lets the hook notice the user typed without React and the browser fighting over the same subtree.

The three arguments, and what onChange actually gives you

The API is a single hook with three arguments and a returned handle. The first is elementRef, a ref object of type RefObject pointing at the element that should become editable, and the ref is allowed to be null or to change during the runtime of the hook, with the caveat that as long as the changes of the ref are triggered by React everything should behave as expected. The second is onChange, a callback of type (text: string, pos: Position) to void, called whenever the content of the contenteditable changes, and this needs to be set up so that it triggers a rerender of the element's contents. The text it receives is the textual representation of the element's contents, while the Position object contains the current position of the cursor, the line number which is zero-indexed, and the content of the current line up until the cursor. That last piece is what makes auto-suggestions possible, since the suggestion can then be applied with the update function that useEditable returns to move the cursor position. The third argument is an optional options object with two options, disabled, which removes the contentEditable attribute again to stop editing, and indentation, a number of displayed spaces.

Indentation turns the element into something like a code editor

The indentation option is the one that pushes this beyond a prose field. It may be a number of displayed spaces for indentation, and it also enables improved Tab key behaviour, which will indent the current line or dedent it when shift is held. The documentation warns that this will make the editor act as a focus trap, which is a real and useful warning because Tab will no longer leave the field, and in a form or a modal that changes keyboard behaviour significantly. When the option is set, useEditable prevents the insertion of tab characters and instead inserts the specified amount of whitespace, which makes handling of columns much easier. That is the whole point. A tab character inside a pre element is a single character that the browser does not render as a column, so code indentation drifts as soon as the content is not at the left margin, and inserting spaces instead keeps the visual column aligned with the logical one. The disabled option is the counterpart, and it is implemented by removing the contentEditable attribute rather than by guarding event handlers, so toggling it does not leave stale state behind.

The example, and the constraint that comes with it

The usage example in the documentation is a rainbow code editor, and it shows the shape of the whole thing. You install the package alongside react, version 18 or newer:

sh
npm install use-editable

Then you render a pre element with a ref and call the hook with the ref and your state setter:

js
import React, { useState, useRef } from 'react';
import { useEditable } from 'use-editable';

const RainbowCode = () => {
  const [code, setCode] = useState('function test() {}\nconsole.log("hello");');
  const editorRef = useRef(null);

  useEditable(editorRef, setCode);

The pre element receives the ref, and the state setter is the onChange handler, so the flow is that the browser edits the DOM, the observer notices, the hook rolls the DOM back, and your setter is called with the new text. The important limitation is stated in the documentation's own section on what is currently possible. Either the rendered elements' text content has to eventually exactly match the code input, or your implementation must be able to convert the rendered text content back into what you are using as state. This is a limitation of how contenteditable works, since it only captures the actual DOM content. Since use-editable does not aim to be a full component that manages the render cycle, it does not have to keep any extra state, and it passes only the DOM's text back to the onChange callback.

Where a 2kB hook stops being the right answer

The boundary is the size of the editing surface and the fidelity of the content model. Because the hook returns only text, any structure you render has to survive a round trip through plain text, and the documentation is explicit that either your rendered text has to match the input exactly or you have to write the conversion back into state yourself. That is fine for a code block where the content is lines, and it becomes painful the moment the field holds rich content, because a bold run or a list is not recoverable from text. So for a rich text editor, a document model, anything with collaborative editing, spell checking, history or a proper cursor model per node, you need a real editor library and this is the wrong tool. There is a second limitation in the browser support. The library has been tested against, and should work properly in Chrome, Safari, iOS Safari and Firefox, and the published build targets ES2022 so it requires a modern evergreen browser, with IE 11 explicitly not supported. Beyond that, the nature of the technique brings its own cost. The MutationObserver plus rollback means every keystroke produces a DOM change, a rollback, and then a React update, so a large field with heavy re-renders will feel less responsive than a purpose-built editor, and the caret restoration is a heuristic rather than a guarantee. The project is at version 3.0.0, released on 2026-10-19, MIT licensed, with the last push on the same day, and the repository shows a.changeset/ directory and a CHANGELOG.md, and the v3.0.0 release on that date shows it is maintained with changesets and tagged releases.

Editorial conclusion

Adopt use-editable if you need a small editable surface in a React app, a code snippet box or a prose field, and you want to keep rendering React children into it rather than dropping to innerHTML. Do not adopt it if you need a full document editor, since the hook only gives back the element's text content and cannot reconstruct structure. Before you start, confirm your React version is 18 or newer, and test the round trip, because the rendered text must match what the user typed or your implementation has to convert the text back into state.

Frequently asked questions

What does the use-editable hook do?

It makes an element contenteditable while still allowing normal React elements to be rendered into it, so you avoid working with innerHTML or raw HTML. It is aimed at small code editors and prose textareas, and the whole library is about 2kB.

How does use-editable handle the conflict between contenteditable and React?

It creates a MutationObserver watching all changes to the contenteditable element, and before reporting them to React it rolls back the DOM changes so React sees what it expects. It also preserves the caret and selection and restores them after React updates the DOM.

What arguments does useEditable take?

The first is a ref object pointing at the element to make editable, the second is an onChange callback of type (text: string, pos: Position) to void, and the third is an optional options object with disabled and indentation. The hook also returns an Edit handle with methods, including an update function for moving the cursor.

What does the indentation option in use-editable do?

It sets a number of displayed spaces for indentation and enables improved Tab key behaviour, indenting the current line or dedenting it when shift is held. Tab characters are prevented and the specified whitespace is inserted instead, and the documentation warns that this makes the editor act as a focus trap.

Which browsers and React versions does use-editable support?

It has been tested against Chrome, Safari, iOS Safari and Firefox, and the published build targets ES2022 so it needs a modern evergreen browser, with IE 11 not supported. The peer dependency is react at 18.0.0 or newer.

Official sources

  1. FormidableLabs/use-editable on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/formidablelabs-use-editable.svg)](https://hysenlabs.com/projects/formidablelabs-use-editable)

Was this useful?