As a developer building complex React dashboards, I’ve often hit a wall when a standard <input> or <textarea> just wasn’t enough.
There are times when you need your users to edit text that contains bold tags, links, or even custom React components inside the editable area.
Standard form elements can’t render HTML children, which is why we turn to the contentEditable attribute.
In this guide, I will show you exactly how to build a robust React ContentEditable component that handles children gracefully.
ContentEditable in the React Ecosystem
The contentEditable attribute allows any HTML element, like a <div> or <span>, to behave like a text editor.
However, using it in React is notoriously tricky because React’s virtual DOM often clashes with the browser’s direct manipulation of the DOM.
When you add children into the mix, like nesting a <strong> tag inside your editable div, managing the cursor position becomes a real challenge.
I remember the first time I tried this; every time the state updated, the cursor would jump back to the start of the line. It was incredibly frustrating.
Method 1: Build a Basic ContentEditable Wrapper
The simplest way to start is by creating a wrapper that passes the children prop into a div with the contentEditable attribute.
I prefer using this method for simple tasks, like an editable headline where you might want to allow some basic inline styling.
Here is the full code for a functional wrapper.
import React, { useRef } from 'react';
const SimpleEditable = ({ children, onChange }) => {
const contentRef = useRef(null);
const handleInput = () => {
if (onChange) {
onChange(contentRef.current.innerHTML);
}
};
return (
<div
ref={contentRef}
contentEditable
onInput={handleInput}
style={{
border: '1px solid #ccc',
padding: '15px',
borderRadius: '5px',
minHeight: '50px'
}}
suppressContentEditableWarning={true}
>
{children}
</div>
);
};
// Usage in a USA-based Real Estate App example
const App = () => {
const handleUpdate = (val) => {
console.log("New Property Description:", val);
};
return (
<div style={{ padding: '20px' }}>
<h1>Property Listing: 1600 Pennsylvania Ave</h1>
<SimpleEditable onChange={handleUpdate}>
This <strong>Luxury Executive Suite</strong> is located in the heart of DC.
It features <em>hardwood floors</em> and premium finishes.
</SimpleEditable>
</div>
);
};
export default App;You can refer to the screenshot below to see the output.

In this example, I used suppressContentEditableWarning because React will warn you that you are nesting children inside an editable element.
This is a “soft” fix, but for professional apps, we need something more controlled.
Method 2: Manage State and Cursor Position
If you want the component to be truly reactive, you need to sync the state without losing the user’s cursor (caret) position.
During a project for a New York-based fintech firm, I had to build a tool where users could tag “Stock Tickers” inside a paragraph.
If the component re-renders and replaces the HTML, the browser forgets where the cursor was. We solve this by only updating the DOM when the content actually changes from an outside source.
import React, { Component } from 'react';
class ControlledEditable extends Component {
constructor(props) {
super(props);
this.el = React.createRef();
}
shouldComponentUpdate(nextProps) {
// Only update if the html prop is different from the current DOM content
return nextProps.html !== this.el.current.innerHTML;
}
emitChange = () => {
const html = this.el.current.innerHTML;
if (this.props.onChange && html !== this.lastHtml) {
this.props.onChange({
target: { value: html }
});
}
this.lastHtml = html;
};
render() {
return (
<div
ref={this.el}
onInput={this.emitChange}
onBlur={this.emitChange}
contentEditable
dangerouslySetInnerHTML={{ __html: this.props.html }}
style={{
border: '2px solid #007bff',
padding: '10px',
margin: '10px 0'
}}
/>
);
}
}
// Example: California Tech Job Posting Editor
const JobEditor = () => {
const [description, setDescription] = React.useState(
"Join our <b>Silicon Valley</b> team as a Senior Engineer. <span style='color: green;'>Remote options available.</span>"
);
return (
<div style={{ maxWidth: '600px', margin: 'auto' }}>
<h3>Edit Job Description</h3>
<ControlledEditable
html={description}
onChange={(e) => setDescription(e.target.value)}
/>
<p>Preview Output: {description}</p>
</div>
);
};
export default JobEditor;You can refer to the screenshot below to see the output.

This method uses dangerouslySetInnerHTML. While the name sounds scary, it is necessary here to keep the children (HTML tags) intact.
I’ve found that checking shouldComponentUpdate is the secret sauce to preventing that annoying cursor jump.
Method 3: Handle Rich Text with Sanitize-HTML
When you allow children in a contentEditable component, users might paste text from Word or Google Docs that contains “dirty” HTML.
If you are building a tool for a US law firm or a medical office, you cannot afford to have broken styles or malicious scripts in your data.
I always recommend sanitizing the input before saving it to your state.
import React, { useState, useRef } from 'react';
import sanitizeHtml from 'sanitize-html';
const SecureEditable = () => {
const [content, setContent] = useState("San Francisco <b>Health Clinic</b> Report");
const editableRef = useRef();
const handleChange = () => {
const rawHtml = editableRef.current.innerHTML;
// We sanitize to allow only specific tags
const cleanHtml = sanitizeHtml(rawHtml, {
allowedTags: ['b', 'i', 'em', 'strong', 'a', 'br'],
allowedAttributes: {
'a': ['href']
}
});
setContent(cleanHtml);
};
return (
<div style={{ padding: '20px', fontFamily: 'Arial' }}>
<h2>Patient Notes (Internal Use Only)</h2>
<div
ref={editableRef}
contentEditable
onInput={handleChange}
dangerouslySetInnerHTML={{ __html: content }}
style={{
minHeight: '150px',
border: '1px solid #333',
padding: '10px',
backgroundColor: '#f9f9f9'
}}
/>
<div style={{ marginTop: '20px', fontSize: '12px', color: '#666' }}>
<strong>Database Record:</strong> {content}
</div>
</div>
);
};
export default SecureEditable;You can refer to the screenshot below to see the output.

In this approach, I use the sanitize-html library. It ensures that even if a user pastes a whole website into your component, only the bold, italic, and link tags remain.
Deal with Line Breaks and Formatting
One thing I’ve noticed in US-based enterprise apps is the inconsistency of the “Enter” key.
Some browsers insert a <p> tag, while others insert a <div> or a <br>.
To make your component consistent, you can use the execCommand API, although it is technically deprecated, it is still widely used for basic formatting.
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
// Custom logic for Chicago Style formatting or simple breaks
// document.execCommand('insertHTML', false, '<br><br>');
}
};Why Use Children instead of a Value Prop?
You might wonder why we don’t just use a simple string.
When you use children, you can render other React components inside the editable area, like a “Mention” badge or an “Email Template Variable.”
This creates a “WYSIWYG” (What You See Is What You Get) experience that feels premium and modern.
Common Issues to Avoid
After years of doing this, I’ve seen developers make the same three mistakes:
- Forgetting suppressContentEditableWarning: Without this, your console will be flooded with warnings every time the component renders.
- Not Handling “Paste”: If you don’t intercept the paste event, users will bring in weird CSS styles from the clipboard.
- Ignoring Accessibility: Screen readers often struggle with contentEditable. Always add an aria-label.
Building a React ContentEditable component with children is definitely a step up from basic form handling.
It gives you the flexibility to create rich text editors tailored to specific business needs, whether it’s for a social media tool in Austin or a logistics tracker in Chicago.
I hope this tutorial helps you avoid the cursor-jumping headaches I faced early in my career.
You may also like to read:
- React Component Lifecycle Phases
- React Component Types in TypeScript
- Convert React Component to PowerPoint Presentation
- How to Get Props of Component in React

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.