
When Your Rich Text Editor Needs to Be as Capable as Your App: Diving into Plate
Building a rich text editor from scratch is a nightmare. Leveraging a robust framework like Plate, which integrates with modern UI libraries and even AI, changes the game for creating sophisticated content experiences within your applications.
by Sunil Band
The Peril of Custom Rich Text
Every now and then, a project comes along where a simple textarea just won't cut it. You need bold, italics, lists, maybe even embeds or custom components. My first instinct, years ago, was to reach for a contentEditable div and start wrestling with DOM mutations and selection APIs. It's a dark path, riddled with cross-browser inconsistencies and an almost guaranteed security vulnerability if you're not careful. You quickly realize that building a rich text editor isn't just hard; it's a specialized domain that few should tackle from scratch.
Then came libraries like Draft.js and Slate.js, offering a more structured, React-friendly approach. They abstracted away the nastiness of contentEditable, providing a declarative data model and a component-based rendering system. Slate, in particular, stood out with its highly customizable plugin architecture, letting you define how your content looks and behaves without forcing a specific UI on you. But even with Slate, getting a fully-featured, production-ready editor felt like a significant undertaking. This is where Plate enters the picture: it's a framework built on top of Slate, designed to accelerate the development of complex rich text experiences, complete with first-class integration for modern UI libraries and, increasingly, AI features.
Why Plate Matters: Beyond Basic Rich Text
Plate isn't just a wrapper for Slate; it's a comprehensive rich text framework. It provides a plugin system that handles many common rich text features out of the box, saving you from writing boilerplate Slate plugins for every little thing. Think bold, italic, code blocks, headings, lists, links – Plate has pre-built plugins for these, and they are highly configurable. This means you can focus on the unique aspects of your editor, rather than reinventing the wheel on every basic text formatting option.
What truly sets Plate apart is its focus on modern frontend development paradigms. It integrates seamlessly with libraries like shadcn/ui, allowing you to build beautiful, accessible editor UIs without having to design every button and toolbar from scratch. This is a huge win for developer experience and consistency, especially in applications that already use shadcn/ui. Furthermore, the push towards integrating AI capabilities directly into the editor workflow points to a future where content creation is not just about formatting, but about intelligent assistance and generation.
Getting Started with a Plate Editor
Let's build a simple Plate editor that supports basic text formatting, headings, and lists. We'll leverage shadcn/ui components for the toolbar to keep things looking clean and modern.
First, you'll need to install Plate and its dependencies, along with shadcn/ui components if you haven't already. This example assumes you've set up shadcn/ui in your project.
npm install @udecode/plate @udecode/plate-ui-shadcn-editor @udecode/plate-ui-shadcn-button @udecode/plate-ui-shadcn-toolbar @udecode/plate-toolbar @udecode/plate-basic-elements @udecode/plate-block-quote @udecode/plate-heading @udecode/plate-list @udecode/plate-paragraph @udecode/plate-link @udecode/plate-code-block @udecode/plate-combobox @udecode/plate-line-height @udecode/plate-floating @udecode/plate-alignment @udecode/plate-indent @udecode/plate-normalizers @udecode/plate-selection @udecode/plate-serializer @udecode/plate-core @udecode/plate-ui-basic-elements @udecode/plate-ui-code-block @udecode/plate-ui-indent @udecode/plate-ui-list @udecode/plate-ui-alignment @udecode/plate-ui-block-quote @udecode/plate-ui-heading @udecode/plate-ui-link
# And don't forget the underlying Slate and React dependencies if not already present
npm install slate slate-reactNow, let's create a basic editor component. We'll define a set of plugins and pass them to the Plate component. The Plate component then takes care of rendering the editor and applying all the plugin logic.
'use client';
import React, { useState } from 'react';
import { createPlateEditor, Plate, Value } from '@udecode/plate-core';
import { createParagraphPlugin, ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph';
import { createHeadingPlugin, ELEMENT_H1, ELEMENT_H2, ELEMENT_H3 } from '@udecode/plate-heading';
import { createBoldPlugin, createItalicPlugin, createUnderlinePlugin } from '@udecode/plate-basic-marks';
import { createBlockquotePlugin, ELEMENT_BLOCKQUOTE } from '@udecode/plate-block-quote';
import { createCodeBlockPlugin, ELEMENT_CODE_BLOCK } from '@udecode/plate-code-block';
import { createLinkPlugin, ELEMENT_LINK } from '@udecode/plate-link';
import { createListPlugin, ELEMENT_UL, ELEMENT_OL } from '@udecode/plate-list';
import {
PlateContent,
createBasicElementsPlugin,
createBasicMarksPlugin,
createReactPlugin
} from '@udecode/plate-ui-shadcn';
import { Button } from '@/components/ui/button'; // Assuming shadcn button component path
import { Toggle } from '@/components/ui/toggle'; // Assuming shadcn toggle component path
import {
InsertDropdownMenu,
MarkToolbarButton,
Toolbar,
HeadingToolbarButton,
ListToolbarButton,
LinkToolbarButton
} from '@udecode/plate-ui-shadcn-toolbar';
const plugins = [
createReactPlugin(),
createBasicElementsPlugin(), // Provides paragraph, heading plugins etc.
createBasicMarksPlugin(), // Provides bold, italic, underline plugins etc.
createParagraphPlugin(),
createHeadingPlugin(),
createBlockquotePlugin(),
createCodeBlockPlugin(),
createLinkPlugin(),
createListPlugin()
];
const initialValue: Value = [
{
type: ELEMENT_H1,
children: [{ text: 'My Awesome Plate Editor' }],
},
{
type: ELEMENT_PARAGRAPH,
children: [{ text: 'This is a paragraph with ' }, { text: 'bold', bold: true }, { text: ' and ' }, { text: 'italic', italic: true }, { text: ' text.' }],
},
{
type: ELEMENT_UL,
children: [
{ type: ELEMENT_PARAGRAPH, children: [{ text: 'First list item' }] },
{ type: ELEMENT_PARAGRAPH, children: [{ text: 'Second list item' }] },
],
},
{
type: ELEMENT_CODE_BLOCK,
children: [{ type: ELEMENT_PARAGRAPH, children: [{ text: 'console.log("Hello Plate!");' }] }],
},
];
export function MyPlateEditor() {
const [value, setValue] = useState<Value>(initialValue);
// Initialize the Plate editor instance
const editor = createPlateEditor({
plugins,
// You can pass editor options here
});
return (
<div className="rounded-md border p-4 shadow-sm w-full max-w-3xl mx-auto">
<Toolbar className="flex flex-wrap gap-2 mb-4 p-2 border-b">
{/* Mark Buttons */}
<MarkToolbarButton nodeType="bold" tooltip="Bold">
<b>B</b>
</MarkToolbarButton>
<MarkToolbarButton nodeType="italic" tooltip="Italic">
<i>I</i>
</MarkToolbarButton>
<MarkToolbarButton nodeType="underline" tooltip="Underline">
<u>U</u>
</MarkToolbarButton>
{/* Heading Buttons */}
<HeadingToolbarButton nodeType={ELEMENT_H1} tooltip="Heading 1">
H1
</HeadingToolbarButton>
<HeadingToolbarButton nodeType={ELEMENT_H2} tooltip="Heading 2">
H2
</HeadingToolbarButton>
<HeadingToolbarButton nodeType={ELEMENT_H3} tooltip="Heading 3">
H3
</HeadingToolbarButton>
{/* List Buttons */}
<ListToolbarButton nodeType={ELEMENT_UL} tooltip="Bulleted List">
UL
</ListToolbarButton>
<ListToolbarButton nodeType={ELEMENT_OL} tooltip="Numbered List">
OL
</ListToolbarButton>
{/* Other block types */}
<InsertDropdownMenu>
<LinkToolbarButton nodeType={ELEMENT_LINK} tooltip="Link">
Link
</LinkToolbarButton>
<Button variant="ghost" size="sm" onClick={() => editor.insertNodes({ type: ELEMENT_CODE_BLOCK, children: [{ type: ELEMENT_PARAGRAPH, children: [{ text: '' }] }] })}>Code Block</Button>
<Button variant="ghost" size="sm" onClick={() => editor.insertNodes({ type: ELEMENT_BLOCKQUOTE, children: [{ type: ELEMENT_PARAGRAPH, children: [{ text: '' }] }] })}>Blockquote</Button>
</InsertDropdownMenu>
</Toolbar>
<Plate
editor={editor}
value={value}
onChange={newValue => {
setValue(newValue);
// console.log(JSON.stringify(newValue, null, 2)); // Log the editor's state
}}
>
<PlateContent
className="min-h-[300px] p-4 focus:outline-none"
decorate={([node, path]) => {
// Example: Add a class to paragraphs at the root for custom styling
if (node.type === ELEMENT_PARAGRAPH && path.length === 1) {
return { className: 'my-custom-paragraph-style' };
}
return {};
}}
/>
</Plate>
</div>
);
}In this example, we:
- Define an array of
plugins. Plate's plugin system is incredibly modular. Each plugin corresponds to a specific rich text feature (e.g.,createParagraphPluginfor paragraphs,createBoldPluginfor bold text). - Use
createPlateEditorto instantiate the editor with our chosen plugins. This is the core Plate editor instance. - Render the
<Plate>component, passing oureditorinstance and thevaluestate. TheonChangeprop updates our component's state as the editor content changes. - The
<PlateContent>component is where the actual editable area lives. It's a wrapper around Slate'sEditablecomponent, applying Plate's styling and rendering logic. - We build a toolbar using shadcn/ui components (
Toggle,Button,Toolbar) and Plate's provided toolbar components (MarkToolbarButton,HeadingToolbarButton, etc.). These components interact directly with the Plate editor instance to apply formatting or insert elements.
Notice how the value prop is an array of objects. This is Slate's JSON-based content model, where each object represents a block element (like a paragraph or heading) and its children array contains either text nodes or inline elements. This structured data model makes it easy to save, load, and manipulate content programmatically, which is crucial for features like AI integration or collaborative editing.
The Power of the Plugin System
The real power of Plate comes from its robust plugin system. Every feature, from basic text marks to complex embeds, is implemented as a plugin. This allows for an extremely modular and extensible architecture. Want to add a custom image uploader? Write an image plugin. Need to integrate a mention system? Build a mention plugin.
Plate simplifies plugin creation by providing a lot of utility functions and hooks. For example, a plugin can define:
-
key: A unique identifier for the plugin. -
type: The type of element or mark it handles (e.g.,ELEMENT_H1,MARK_BOLD). -
component: A React component to render the element/mark. -
handlers: Functions that react to editor events (e.g.,onKeyDown). -
withOverrides: A function to extend Slate's editor object with custom methods.
This structure ensures that your editor remains performant and maintainable, even as it grows in complexity. You're not just hacking on top of contentEditable; you're extending a well-thought-out, performant content model.
// Example: A simplified custom plugin for a 'highlight' mark
import { createPluginFactory, MARK_HIGHLIGHT } from '@udecode/plate-core';
export const createHighlightPlugin = createPluginFactory({
key: MARK_HIGHLIGHT,
is</div>{

When Your React App Renders Twice and No One Knows Why: Understanding Hydration
Ever built a React app that seems to flicker on load, or worse, throws hydration errors that make no sense? You're not alone. I've been down the React hydration rabbit hole, and it's a critical concept for anyone building performant and stable server-rendered React applications.

When Your Database Needs to Think: Adding AI Search with pgvector
Semantic search using embeddings is a game-changer, but integrating it often feels like a separate service problem. What if your existing Postgres database could handle it natively?

When Your Data Visualization Needs to Be More Than Just a Static Chart: Diving into Plotly.js
Tired of static charts that only tell half the story? Plotly.js offers a powerful way to bring your data to life with rich interactivity, letting users explore and understand complex datasets directly in their browser. It's not just about pretty graphs; it's about making data explorable.


















