Rich text editing is a common requirement in modern React applications, but rarely simple to implement. Allowing users to format text with headings, lists, and emphasis introduces challenges around state, content consistency, and maintainability.
Many applications rely on raw HTML strings or direct use of . While this can work at first, it often leads to unpredictable behavior as the application grows. Content becomes harder to validate and version, and easy to break when multiple users or automated systems are involved.
A more reliable approach is to treat rich text as structured data instead of markup. Explicit content models give applications control over content creation, rendering, and extensibility, while align…
Rich text editing is a common requirement in modern React applications, but rarely simple to implement. Allowing users to format text with headings, lists, and emphasis introduces challenges around state, content consistency, and maintainability.
Many applications rely on raw HTML strings or direct use of . While this can work at first, it often leads to unpredictable behavior as the application grows. Content becomes harder to validate and version, and easy to break when multiple users or automated systems are involved.
A more reliable approach is to treat rich text as structured data instead of markup. Explicit content models give applications control over content creation, rendering, and extensibility, while aligning naturally with React’s declarative, state-driven model.
In this article, we will explore why rich text editing is hard in React, how structured content helps address these challenges, and how extension-based editors enable safe customization. We will also walk through a step-by-step example using Puck’s rich text editor to demonstrate these concepts in practice.
Rich text editing conflicts with several core principles of how React applications are designed. Many traditional solutions rely on browser-level APIs rather than explicit React state, which makes them harder to control as complexity increases.
- Many implementations depend on and browser-managed HTML mutations. These operate outside React’s state model and make editor behavior difficult to reason about.
- Rendering user-generated HTML without an explicit content model limits an application’s ability to validate, transform, or reason about content reliably over time, which in turn makes it difficult to diff, version, or audit, especially in collaborative or frequently edited environments.
- Small markup changes can easily break layouts or violate design and accessibility rules without providing any clear signal to the application.
- Raw HTML alone does not convey intent, which makes it unsafe for automated transformations, programmatic editing, or AI-driven workflows without an additional layer of structure and control.
In practice, many editors expose HTML as an interface while managing structured state internally. The core challenge is not the presence of HTML itself, but whether applications retain control and intent over how that HTML is produced and evolved.

Puck is an open source visual editor for React that lets teams build custom page builders using their own components. The core of Puck is simply a React editor and a renderer, making it easy to integrate into any React application and define editing behavior through configuration.
- Schema-driven fields: Rich text in Puck is defined through explicit field schemas, making the allowed structure and behavior clear and enforceable at the application level.
- Built on TipTap: Puck’s rich text editor is powered by TipTap, a widely adopted editor engine known for its extensible and state-driven architecture.
- Customizable: Formatting options, heading levels, and editor capabilities can be enabled or disabled.
- Extensible by design: New formatting features and behaviors can be added using extensions, without modifying or forking the editor core.
- Natural fit for React: Puck’s configuration model aligns with React’s component-driven approach, making rich text editing easier to reason about and maintain.
A complete working demo is available in this GitHub repository. You are encouraged to clone the repository and run the project locally to explore the full implementation, or follow the instructions below to set it up from scratch.
To run the demo locally, use the following commands:
Then navigate to http://localhost:3000/edit and experiment with the rich text editor.
1. Setting Up Puck
First up, let’s install Puck. If you’re adding it to an existing project, install it directly via npm:
Alternatively, you can scaffold a new project using the official recipe. This sets up a working React and Next.js application with the editor and rendering pipeline already configured.
Once the development server is running, navigate to http://localhost:3000/edit to edit a page and http://localhost:3000 to view it.

The Next.js Puck recipe includes the following:
- Editor: A visual editor interface where content blocks can be added, configured, and edited.
- Renderer: A rendering layer that takes saved editor data and outputs a live page using the same component configuration.
- Configuration-driven setup: All editor behavior, available blocks, and field types are defined through a single configuration file, rather than custom editor logic.
For this article, we focus only on the editor configuration and how rich text behavior is defined. Routing, persistence, and deployment details are intentionally kept minimal to keep attention on the core concepts.
2. Adding a Rich Text Editor to Puck
Rich text editing in Puck is defined declaratively through configuration. Instead of manually wiring an editor, handling DOM updates, or managing selection state, rich text behavior is enabled by adding a dedicated field type to a component definition.
To set it up, replace the contents of with the following Puck configuration object.
This file serves as the baseline for all upcoming sections, where we incrementally add control and extensibility.
To enable inline editing in Puck, the option is added to the rich text field definition.
Add the following line to the field of the in :

Inline editing works well for simple content updates, quick adjustments, and editorial workflows where users benefit from editing content in its final visual context.
3. Customizing Editor Behavior (Control)
Rich text editors are often expected to give users complete freedom, but in most real applications, this leads to inconsistent content and broken design systems. Teams usually need to restrict formatting options to ensure visual consistency, accessibility, and predictable rendering across the application.
Puck lets you selectively enable or disable formatting options based on the needs of the product or team.
Disabling a Formatting Option (Bold Example)
The example below shows how to disable the bold formatting option in a rich text field.
Add the following configuration to the field of the in :
This configuration removes the bold formatting capability from the editor.

Restricting Heading Levels (Structure)
In rich text editing, allowing unrestricted heading levels often leads to inconsistent content hierarchy and poor accessibility. Most applications only need a small, well-defined set of heading levels to maintain clarity and visual consistency.
Puck also lets you constrain heading behavior through configuration, ensuring that content follows a predictable structure.
The example below restricts headings to H1 and H2 only.
Add the following configuration to the object of the field in :
Once this is applied, the editor will only allow users to select H1 and H2. All other heading levels are removed from the editor interface and cannot be applied through shortcuts.

Default editor toolbars are designed to cover a wide range of use cases, but in real applications, they often expose more options than necessary. This can overwhelm users and lead to inconsistent content. Most teams benefit from presenting a simplified toolbar that reflects only the formatting options they want to support.
Puck lets you customize the rich text menu bar and decide which controls and components to render, making it possible to control the editor user experience without modifying the editor engine or writing custom UI code.
The example below shows how to define a custom menu bar that exposes only selected controls.
Add the function **to the ** field of the in :
This configuration replaces the default toolbar with a minimal menu that includes only the bold control. All other formatting options are hidden from the interface.

Modern rich text editors are not monolithic systems. Instead of baking every feature into the core editor, advanced functionality is added by extending the editor engine itself. This approach keeps the editor lightweight while allowing applications to introduce new capabilities only when needed.
Puck’s rich text editor follows this model. It is built on TipTap, which allows new formatting behaviors to be added by registering extensions through configuration.
The example below adds support for superscript formatting by registering the Superscript extension.
First, install the Superscript extension:
Then, add the extension to the rich text field configuration in :
This configuration enables superscript functionality at the editor engine level. The editor now understands superscript formatting and can apply it internally, but users do not yet see a toolbar button or control to trigger it.
Separating editor capability from editor UI is a key design principle. By adding functionality through extensions first, applications can control when and how features are exposed to users. This makes the editor easier to evolve, safer to customize, and better aligned with product requirements.
In the next step, this capability will be surfaced through a custom toolbar control.
Exposing the Extension via a Custom Control
To make the feature usable, the editor needs to expose this capability through the user interface.
This is done by connecting the editor state to the toolbar using a selector and rendering a custom control that responds to that state.
A selector allows the editor to expose information about its current state, such as whether a formatting option is active or whether it can be applied at a given cursor position. This state is then consumed by the menu bar to control button behavior.
The example below shows how to expose the Superscript extension through a custom toolbar button.
Update the field in as follows:

This configuration connects the Superscript extension to the editor interface. The selector exposes whether superscript is active and whether it can be applied, and the custom control uses that state to render an interactive toolbar button.
You can extend this demo by:
- Adding more TipTap extensions
- Introducing additional editor constraints
- Experimenting with different toolbar layouts
- Integrating persistence or collaboration features
Rich text editing works best when it is treated as a structured system rather than a free form input. By modeling content intentionally, enforcing clear rules, and extending behavior through configuration, teams can build editors that scale reliably with their applications.
Puck fits naturally into this model by combining a structured, rich text engine with a configuration-driven approach that aligns well with modern React development.
- Structure over uncontrolled markup: Rich text should encode intent and hierarchy through explicit structure, not rely solely on browser-managed HTML.
- Control over free-form editing: Editors should enforce design and content rules instead of relying on manual cleanup.
- Extensibility through configuration: New capabilities can be added safely without modifying core editor logic.
- Separation of concerns: Editing behavior, user interface, and rendering remain clearly decoupled.
Explore the full demo repository to see these concepts in action, experiment with editor constraints, and extend the rich text editor to fit your own use cases.