Back to Blog
When Your Rich Text Editor Needs to Be as Capable as Your App: Diving into Plate
7 min readSep 10, 20261 views

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.

FrontendWeb DevelopmentUIReactSoftware Design
Share

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.

bash
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-react

Now, 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.

typescript
'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:

  1. Define an array of plugins. Plate's plugin system is incredibly modular. Each plugin corresponds to a specific rich text feature (e.g., createParagraphPlugin for paragraphs, createBoldPlugin for bold text).
  2. Use createPlateEditor to instantiate the editor with our chosen plugins. This is the core Plate editor instance.
  3. Render the <Plate> component, passing our editor instance and the value state. The onChange prop updates our component's state as the editor content changes.
  4. The <PlateContent> component is where the actual editable area lives. It's a wrapper around Slate's Editable component, applying Plate's styling and rendering logic.
  5. 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.

typescript
// 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>
json
{
  
More from the blog
Available for projectsReady to make something fun 🎈

Ready to build the next system?Wanna build something awesome together?

Currently accepting high-impact opportunities in frontend engineering and scalable web applications.Got a cool idea rattling around? Let's grab a virtual coffee and turn it into something people love. ☕