In the previous chapter, Consumer Application Implementation, we built a Next.js page that renders our editor. We used two key components: <EditorRoot> and <EditorContent>.
But what exactly are these components? Why do we need them?
Welcome to the Headless Editor Wrapper. If the novel library is a car, this chapter explains the Chassis. It is the metal frame that holds the engine (Tiptap), the electronics (State Management), and the interior features (React Components) together.
The underlying engine we use is Tiptap. Tiptap is powerful, but it works directly with the raw HTML (DOM).
However, we are building a React application. We want to use React components for our "Slash Menu" or "Floating Bubble Menu."
The Headless Editor Wrapper solves this by creating a Context and a Tunnel.
Imagine a house with two floors:
The Wrapper installs a "dumbwaiter" (a small elevator) so you can send items (React Components) from Floor 1 directly into a specific room on Floor 2.
To understand the wrapper, we need to understand three small concepts:
EditorRoot): The parent container. It sets up the shared memory (State) and the elevator system (Tunnel).EditorContent): The actual text box user types in. It connects the Tiptap engine to the Root.tunnel-rat): A library we use to teleport React components into the editor.Let's look at how we use this in code to solve the "State and Rendering" problem.
First, we wrap everything in EditorRoot. This doesn't render any visible UI. It just sets up the invisible wiring.
import { EditorRoot } from "novel";
const MyEditor = () => {
// EditorRoot creates the "context" for state and tunnels
return (
<EditorRoot>
{/* The editor goes here */}
</EditorRoot>
);
};
Result: Nothing is visible yet, but the application is now ready to share data.
Next, we place the EditorContent inside the root. This initializes Tiptap.
import { EditorContent } from "novel";
// Inside EditorRoot...
<EditorContent
className="border p-4"
initialContent={{ type: "doc", content: [] }}
/>
Result: Now you see a text box. You can type in it. It has access to the wiring set up by the Root.
How does EditorRoot actually set up this "Tunnel"? Let's visualize the initialization process.
Let's look at the actual source code in packages/headless/src/components/editor.tsx to see this magic.
This component is surprisingly simple. It uses two specific tools:
// packages/headless/src/components/editor.tsx
export const EditorRoot: FC<EditorRootProps> = ({ children }) => {
// 1. Create the tunnel instance
const tunnelInstance = useRef(tunnel()).current;
return (
// 2. Provide the Store (Jotai)
<Provider store={novelStore}>
{/* 3. Provide the Tunnel (Context) */}
<EditorCommandTunnelContext.Provider value={tunnelInstance}>
{children}
</EditorCommandTunnelContext.Provider>
</Provider>
);
};
We create the tunnel once (useRef) and pass it down so any child component (like the Slash Command menu we will build in Slash Command System) can access it.
This component wraps Tiptap's native EditorProvider. It acts as the bridge.
// packages/headless/src/components/editor.tsx
export const EditorContent = forwardRef<HTMLDivElement, EditorContentProps>(
({ children, initialContent, ...rest }, ref) => (
<div ref={ref}>
{/* Initialize Tiptap Engine */}
<EditorProvider {...rest} content={initialContent}>
{children}
</EditorProvider>
</div>
),
);
By placing EditorProvider inside EditorRoot, the Tiptap engine can now access the Jotai store and the Tunnel we created earlier.
We mentioned novelStore. In packages/headless/src/utils/store.ts, this is simply an empty container created by Jotai.
import { createStore } from "jotai";
// A global bucket to hold state like "is the menu open?"
export const novelStore: any = createStore();
You might notice there are no buttons, toolbars, or styles defined here.
This allows you (the developer) to build your own UI around it, while novel handles the hard part of connecting React state to the text editor engine.
You now understand the chassis of our car.
EditorRoot sets up the electrical wiring (Jotai) and the portals (Tunnel).EditorContent installs the engine (Tiptap).But a chassis without wheels or customized parts is boring. To make the editor do cool things—like Markdown shortcuts or AI text generation—we need to plug things into the engine.
In the next chapter, we will learn how to add features to this engine.
Next Chapter: Custom Tiptap Extensions
Generated by Code IQ