From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
  • Introduction
  • Installation
    • Plate UI
      • Next.js
      • React
    • Manual
    • RSC
    • Node.js
    • Local Docs
    • MCP
  • Releases

Manual Installation

PreviousNext

Install and configure Plate in your React project without relying on UI component libraries.

This guide walks you through setting up Plate from scratch, giving you full control over styling and component rendering. This approach is ideal if you're not using a UI library like shadcn/ui or Tailwind CSS.

Create Project

This guide uses Vite for demonstrating the initial project setup. Plate is framework-agnostic and integrates seamlessly with other React environments like Next.js or Remix. You can adapt the general setup principles to your chosen framework.

To begin with Vite, create a new project and select the React + TypeScript template:

pnpm create vite@latest
pnpm create vite@latest
ReactRSC

On This Page

Create ProjectInstall PlateTypeScript ConfigurationCreate Your First EditorAdding Basic MarksAdding Basic ElementsHandling Editor ValueNext Steps
Build your editor
Production-ready AI template and reusable components.
Get all-access

Install Plate

Install platejs for the core editor runtime and React components.

pnpm add platejs
pnpm add platejs

TypeScript Configuration

Plate provides ESM packages. If you're using TypeScript, ensure your tsconfig.json is configured correctly. The recommended setup for Plate requires TypeScript 5.0+ with the "moduleResolution": "bundler" setting:

// tsconfig.json
{
  "compilerOptions": {
    // ... other options
    "module": "esnext", // or commonjs if your setup requires it and handles ESM interop
    "moduleResolution": "bundler",
    // ... other options
  },
}
// tsconfig.json
{
  "compilerOptions": {
    // ... other options
    "module": "esnext", // or commonjs if your setup requires it and handles ESM interop
    "moduleResolution": "bundler",
    // ... other options
  },
}

If you cannot use "moduleResolution": "bundler" or are on an older TypeScript version, please see our full TypeScript guide for alternative configurations using path aliases.

Create Your First Editor

Start by creating a basic editor component. This example sets up a simple editor.

src/App.tsx
import React from 'react';
import type { Value } from 'platejs';
import { Plate, PlateContent, usePlateEditor } from 'platejs/react';
 
export default function App() {
  const editor = usePlateEditor();
 
  return (
    <Plate editor={editor}>
      <PlateContent 
        style={{ padding: '16px 64px', minHeight: '100px' }}
        placeholder="Type your amazing content here..."
      />
    </Plate>
  );
}
src/App.tsx
import React from 'react';
import type { Value } from 'platejs';
import { Plate, PlateContent, usePlateEditor } from 'platejs/react';
 
export default function App() {
  const editor = usePlateEditor();
 
  return (
    <Plate editor={editor}>
      <PlateContent 
        style={{ padding: '16px 64px', minHeight: '100px' }}
        placeholder="Type your amazing content here..."
      />
    </Plate>
  );
}

usePlateEditor creates a memoized editor instance, ensuring stability across re-renders. For a non-memoized version, use createPlateEditor from platejs/react.

Loading…

At this point, you'll have a very basic editor capable of displaying and editing plain text.

Adding Basic Marks

Let's add support for basic text formatting like bold, italic, and underline.

Install the basic nodes package before adding the mark plugins:

pnpm add @platejs/basic-nodes
pnpm add @platejs/basic-nodes
src/App.tsx
import React from 'react';
import type { Value } from 'platejs';
 
import {
  BoldPlugin,
  ItalicPlugin,
  UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
  Plate,
  PlateContent,
  usePlateEditor,
} from 'platejs/react';
 
const initialValue: Value = [
  {
    type: 'paragraph',
    children: [
      { text: 'Hello! Try out the ' },
      { text: 'bold', bold: true },
      { text: ', ' },
      { text: 'italic', italic: true },
      { text: ', and ' },
      { text: 'underline', underline: true },
      { text: ' formatting.' },
    ],
  },
];
 
export default function App() {
  const editor = usePlateEditor({
    plugins: [BoldPlugin, ItalicPlugin, UnderlinePlugin],
    initialValue,
  });
 
  return (
    <Plate editor={editor}>
      {/* You would typically add a toolbar here to toggle marks */}
      <PlateContent style={{ padding: '16px 64px', minHeight: '100px' }} />
    </Plate>
  );
}
src/App.tsx
import React from 'react';
import type { Value } from 'platejs';
 
import {
  BoldPlugin,
  ItalicPlugin,
  UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
  Plate,
  PlateContent,
  usePlateEditor,
} from 'platejs/react';
 
const initialValue: Value = [
  {
    type: 'paragraph',
    children: [
      { text: 'Hello! Try out the ' },
      { text: 'bold', bold: true





















Default Components

Mark plugins like BoldPlugin, ItalicPlugin, and UnderlinePlugin come with default components that render as <strong>, <em>, and <u> elements respectively. You don't need to register custom components unless you want to customize their appearance.

Loading…

You'll need to implement your own toolbar to apply these marks. For example, to toggle bold: editor.update.bold.toggle().

Adding Basic Elements

Now, let's add support for block-level elements like headings, and blockquotes.

src/App.tsx
import React from 'react';
import type { Value } from 'platejs';
 
import {
  BlockquotePlugin,
  BoldPlugin,
  HeadingPlugin,
  
  
  ItalicPlugin,
  UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
  Plate,
  PlateContent,
  PlateElement,
  usePlateEditor,
  type PlateElementProps,
} from 'platejs/react';
 
const initialValue: Value = [
  {
    children: [{ text: 'Title' }],
    type: 'heading', level: 3,
  },
  {
    children: [
      {
        children: [{ text: 'This is a quote.' }],
        type: 'paragraph',
      },
    ],
    type: 'blockquote',
  },
  {
    children: [
      { text: 'With some ' },
      { bold: true, text: 'bold' },
      { text: ' text for emphasis!' },
    ],
    type: 'paragraph',
  },
];
 
// Define element components
function HeadingElement(props: PlateElementProps<typeof HeadingPlugin>) {
  return <PlateElement as="h1" {...props} />;
}
 
function HeadingElement(props: PlateElementProps<typeof HeadingPlugin>) {
  return <PlateElement as="h2" {...props} />;
}
 
function HeadingElement(props: PlateElementProps<typeof HeadingPlugin>) {
  return <PlateElement as="h3" {...props} />;
}
 
function BlockquoteElement(props: PlateElementProps<typeof BlockquotePlugin>) {
  return (
    <PlateElement
      as="blockquote"
      style={{
        borderLeft: '2px solid #eee',
        marginLeft: 0,
        marginRight: 0,
        paddingLeft: '24px',
        color: '#666',
        fontStyle: 'italic',
      }}
      {...props}
    />
  );
}
 
export default function App() {
  const editor = usePlateEditor({
    plugins: [
      BoldPlugin,
      ItalicPlugin,
      UnderlinePlugin,
      HeadingPlugin.configure({ component: HeadingElement }),
      HeadingPlugin.configure({ component: HeadingElement }),
      HeadingPlugin.configure({ component: HeadingElement }),
      BlockquotePlugin.configure({ component: BlockquoteElement }),
    ],
    initialValue,
  });
 
  return (
    <Plate editor={editor}>
      {/* You would typically add a toolbar here to toggle elements and marks */}
      <PlateContent style={{ padding: '16px 64px', minHeight: '100px' }} />
    </Plate>
  );
}
src/App.tsx
import React from 'react';
import type { Value } from 'platejs';
 
import {
  BlockquotePlugin,
  BoldPlugin,
  HeadingPlugin,
  
  
  ItalicPlugin,
  UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
  Plate,
  PlateContent,
  PlateElement,
  usePlateEditor,
  type PlateElementProps,
} from 'platejs/react';
 
const initialValue: Value = [
  {
    children: [{ text: 







































































Notice how we use Plugin.configure({ component: Component }) to register components with block element plugins like headings and blockquotes. This is the recommended approach for associating React components with Plate plugins when you need custom styling or behavior.

Loading…

Handling Editor Value

To make the editor's content persistent, let's integrate state management to save and load the editor's value.

src/App.tsx
import React from 'react';
import type { Value } from 'platejs';
 
import {
  BlockquotePlugin,
  BoldPlugin,
  HeadingPlugin,
  
  
  ItalicPlugin,
  UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
  Plate,
  PlateContent,
  PlateElement,
  usePlateEditor,
  type PlateElementProps,
} from 'platejs/react';
 
const initialValue: Value = [
  {
    children: [{ text: 'Title' }],
    type: 'heading', level: 3,
  },
  {
    children: [
      {
        children: [{ text: 'This is a quote.' }],
        type: 'paragraph',
      },
    ],
    type: 'blockquote',
  },
  {
    children: [
      { text: 'With some ' },
      { bold: true, text: 'bold' },
      { text: ' text for emphasis!' },
    ],
    type: 'paragraph',
  },
];
 
// Define element components
function HeadingElement(props: PlateElementProps<typeof HeadingPlugin>) {
  return <PlateElement as="h1" {...props} />;
}
 
function HeadingElement(props: PlateElementProps<typeof HeadingPlugin>) {
  return <PlateElement as="h2" {...props} />;
}
 
function HeadingElement(props: PlateElementProps<typeof HeadingPlugin>) {
  return <PlateElement as="h3" {...props} />;
}
 
function BlockquoteElement(props: PlateElementProps<typeof BlockquotePlugin>) {
  return (
    <PlateElement
      as="blockquote"
      style={{
        borderLeft: '2px solid #eee',
        marginLeft: 0,
        marginRight: 0,
        paddingLeft: '24px',
        color: '#666',
        fontStyle: 'italic',
      }}
      {...props}
    />
  );
}
 
export default function App() {
  const editor = usePlateEditor({
    plugins: [
      BoldPlugin,
      ItalicPlugin,
      UnderlinePlugin,
      HeadingPlugin.configure({ component: HeadingElement }),
      HeadingPlugin.configure({ component: HeadingElement }),
      HeadingPlugin.configure({ component: HeadingElement }),
      BlockquotePlugin.configure({ component: BlockquoteElement }),
    ],
    initialValue: () => {
      const savedValue = localStorage.getItem('plate-manual-demo');
      return savedValue ? JSON.parse(savedValue) : initialValue;
    },
  });
 
  return (
    <Plate
      editor={editor}
      onValueChange={({ value }) => {
        localStorage.setItem('plate-manual-demo', JSON.stringify(value));
      }}
    >
      {/* Toolbar would go here */}
      <div style={{ padding: '8px 0' }}>
        <button
          onClick={() => {
            editor.update((tx) => {
              tx.value.replace({ children: initialValue });
            });
          }}
          style={{
            padding: '4px 8px',
            margin: '0 4px',
            border: '1px solid #ccc',
            borderRadius: '4px',
            cursor: 'pointer',
          }}
        >
          Reset
        </button>
      </div>
      <PlateContent
        style={{
          padding: '16px 64px',
          minHeight: '100px',
          border: '1px solid #eee',
          borderRadius: '4px',
        }}
        placeholder="Type your amazing content here..."
      />
    </Plate>
  );
}
src/App.tsx
import React from 'react';
import type { Value } from 'platejs';
 
import {
  BlockquotePlugin,
  BoldPlugin,
  HeadingPlugin,
  
  
  ItalicPlugin,
  UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
  Plate,
  PlateContent,
  PlateElement,
  usePlateEditor,
  type PlateElementProps,
} from 'platejs/react';
 
const initialValue: Value = [
  {
    children: [{ text: 'Title' }],









































































































Value Management

The example above demonstrates a basic pattern for managing editor value:

  • Initial value is set through the initialValue option in usePlateEditor
  • Value changes can be handled via the onValueChange prop on <Plate>
  • The reset button uses editor.update(...) with tx.value.replace(...) to restore the initial value
  • To control the value, see Controlled Value
Loading…

Next Steps

You've now set up a basic Plate editor manually! From here, you can:

  • Add Styling:
    • For a quick start with pre-built components, consider using Plate UI
    • Or continue styling manually using CSS, CSS-in-JS libraries, or your preferred styling solution
  • Add Plugins: Plate has a rich ecosystem of plugins for features like tables, mentions, images, lists, and more. Install their packages (e.g., @platejs/table) and add them to your plugins array.
  • Build a Toolbar: Create React components for toolbar buttons that use the Editor Transforms to apply formatting (e.g., editor.update.bold.toggle(), editor.plugin(HeadingPlugin).update.toggle({ level: 1 })). You can also use the editor state with the Editor API.
  • Learn More:
    • Editor Configuration
    • Plugin Configuration
    • Plugin Components
},
{ text: ', ' },
{ text: 'italic', italic: true },
{ text: ', and ' },
{ text: 'underline', underline: true },
{ text: ' formatting.' },
],
},
];
export default function App() {
const editor = usePlateEditor({
plugins: [BoldPlugin, ItalicPlugin, UnderlinePlugin],
initialValue,
});
return (
<Plate editor={editor}>
{/* You would typically add a toolbar here to toggle marks */}
<PlateContent style={{ padding: '16px 64px', minHeight: '100px' }} />
</Plate>
);
}
'Title'
}],
type: 'heading', level: 3,
},
{
children: [
{
children: [{ text: 'This is a quote.' }],
type: 'paragraph',
},
],
type: 'blockquote',
},
{
children: [
{ text: 'With some ' },
{ bold: true, text: 'bold' },
{ text: ' text for emphasis!' },
],
type: 'paragraph',
},
];
// Define element components
function HeadingElement(props: PlateElementProps<typeof HeadingPlugin>) {
return <PlateElement as="h1" {...props} />;
}
function HeadingElement(props: PlateElementProps<typeof HeadingPlugin>) {
return <PlateElement as="h2" {...props} />;
}
function HeadingElement(props: PlateElementProps<typeof HeadingPlugin>) {
return <PlateElement as="h3" {...props} />;
}
function BlockquoteElement(props: PlateElementProps<typeof BlockquotePlugin>) {
return (
<PlateElement
as="blockquote"
style={{
borderLeft: '2px solid #eee',
marginLeft: 0,
marginRight: 0,
paddingLeft: '24px',
color: '#666',
fontStyle: 'italic',
}}
{...props}
/>
);
}
export default function App() {
const editor = usePlateEditor({
plugins: [
BoldPlugin,
ItalicPlugin,
UnderlinePlugin,
HeadingPlugin.configure({ component: HeadingElement }),
HeadingPlugin.configure({ component: HeadingElement }),
HeadingPlugin.configure({ component: HeadingElement }),
BlockquotePlugin.configure({ component: BlockquoteElement }),
],
initialValue,
});
return (
<Plate editor={editor}>
{/* You would typically add a toolbar here to toggle elements and marks */}
<PlateContent style={{ padding: '16px 64px', minHeight: '100px' }} />
</Plate>
);
}
type: 'heading', level: 3,
},
{
children: [
{
children: [{ text: 'This is a quote.' }],
type: 'paragraph',
},
],
type: 'blockquote',
},
{
children: [
{ text: 'With some ' },
{ bold: true, text: 'bold' },
{ text: ' text for emphasis!' },
],
type: 'paragraph',
},
];
// Define element components
function HeadingElement(props: PlateElementProps<typeof HeadingPlugin>) {
return <PlateElement as="h1" {...props} />;
}
function HeadingElement(props: PlateElementProps<typeof HeadingPlugin>) {
return <PlateElement as="h2" {...props} />;
}
function HeadingElement(props: PlateElementProps<typeof HeadingPlugin>) {
return <PlateElement as="h3" {...props} />;
}
function BlockquoteElement(props: PlateElementProps<typeof BlockquotePlugin>) {
return (
<PlateElement
as="blockquote"
style={{
borderLeft: '2px solid #eee',
marginLeft: 0,
marginRight: 0,
paddingLeft: '24px',
color: '#666',
fontStyle: 'italic',
}}
{...props}
/>
);
}
export default function App() {
const editor = usePlateEditor({
plugins: [
BoldPlugin,
ItalicPlugin,
UnderlinePlugin,
HeadingPlugin.configure({ component: HeadingElement }),
HeadingPlugin.configure({ component: HeadingElement }),
HeadingPlugin.configure({ component: HeadingElement }),
BlockquotePlugin.configure({ component: BlockquoteElement }),
],
initialValue: () => {
const savedValue = localStorage.getItem('plate-manual-demo');
return savedValue ? JSON.parse(savedValue) : initialValue;
},
});
return (
<Plate
editor={editor}
onValueChange={({ value }) => {
localStorage.setItem('plate-manual-demo', JSON.stringify(value));
}}
>
{/* Toolbar would go here */}
<div style={{ padding: '8px 0' }}>
<button
onClick={() => {
editor.update((tx) => {
tx.value.replace({ children: initialValue });
});
}}
style={{
padding: '4px 8px',
margin: '0 4px',
border: '1px solid #ccc',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Reset
</button>
</div>
<PlateContent
style={{
padding: '16px 64px',
minHeight: '100px',
border: '1px solid #eee',
borderRadius: '4px',
}}
placeholder="Type your amazing content here..."
/>
</Plate>
);
}