Manual Installation
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
Install Core Dependencies
First, install the necessary Plate packages. These packages provide the core editor functionality, React integration, and basic plugins for marks and elements.
npm add @udecode/plate @udecode/plate-basic-elements @udecode/plate-basic-marks
@udecode/plate
: The core Plate engine and React components.@udecode/plate-basic-elements
: Plugin for common block elements like paragraphs and headings.@udecode/plate-basic-marks
: Plugin for basic text formatting like bold, italic, and underline.
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
},
}
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.
import React from 'react';
import type { Value } from '@udecode/plate';
import { Plate, PlateContent, usePlateEditor } from '@udecode/plate/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 @udecode/plate/react
.
'use client';
import { Plate, usePlateEditor } from '@udecode/plate/react';
import { Editor, EditorContainer } from '@/components/ui/editor';
export default function MyEditorPage() {
const editor = usePlateEditor();
return (
<Plate editor={editor}>
<EditorContainer>
<Editor placeholder="Type your amazing content here..." />
</EditorContainer>
</Plate>
);
}
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.
Update your editor to include the BasicMarksPlugin
and define how these marks should be rendered.
import React from 'react';
import type { Value } from '@udecode/plate';
import { BasicMarksPlugin } from '@udecode/plate-basic-marks/react'; // Import the plugin
import { Plate, PlateContent, PlateLeaf, usePlateEditor, type PlateLeafProps } from '@udecode/plate/react';
const initialValue: Value = [
{
type: 'p',
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({
value: initialValue,
plugins: [BasicMarksPlugin], // Add the marks plugin
components: {
// Define how each mark type should be rendered
// We use PlateLeaf and pass an 'as' prop for semantic HTML, or render directly.
bold: (props: PlateLeafProps) => <PlateLeaf {...props} as="strong" />,
italic: (props: PlateLeafProps) => <PlateLeaf {...props} as="em" />,
underline: (props: PlateLeafProps) => <PlateLeaf {...props} as="u" />,
},
});
return (
<Plate editor={editor}>
{/* You would typically add a toolbar here to toggle marks */}
<PlateContent style={{ padding: '16px 64px', minHeight: '100px' }} />
</Plate>
);
}
Component Mapping is Key
When adding plugins for new mark types (like bold
), you must map their plugin key
(e.g., 'bold'
) to your React components in the components
option of usePlateEditor
. This tells Plate how to render them. PlateLeaf
is a helper component from @udecode/plate/react
that simplifies rendering leaf nodes.
'use client';
import * as React from 'react';
import type { Value } from '@udecode/plate';
import { BasicMarksPlugin } from '@udecode/plate-basic-marks/react';
import {
type PlateLeafProps,
Plate,
PlateLeaf,
usePlateEditor,
} from '@udecode/plate/react';
import { Editor, EditorContainer } from '@/components/ui/editor';
import { FixedToolbar } from '@/components/ui/fixed-toolbar';
import { MarkToolbarButton } from '@/components/ui/mark-toolbar-button';
// import { Bold, Italic, Underline } from 'lucide-react'; // Example icons
const initialValue: Value = [
{
children: [
{ text: 'Hello! Try out the ' },
{ bold: true, text: 'bold' },
{ text: ', ' },
{ italic: true, text: 'italic' },
{ text: ', and ' },
{ text: 'underline', underline: true },
{ text: ' formatting.' },
],
type: 'p',
},
];
export default function MyEditorPage() {
const editor = usePlateEditor({
components: {
bold: function Bold(props: PlateLeafProps) {
return <PlateLeaf {...props} as="strong" />;
},
italic: function Italic(props: PlateLeafProps) {
return <PlateLeaf {...props} as="em" />;
},
underline: function Underline(props: PlateLeafProps) {
return <PlateLeaf {...props} as="u" />;
},
},
plugins: [BasicMarksPlugin],
value: initialValue,
});
return (
<Plate editor={editor}>
<FixedToolbar className="justify-start rounded-t-lg">
<MarkToolbarButton nodeType="bold" tooltip="Bold (⌘+B)">
B
</MarkToolbarButton>
<MarkToolbarButton nodeType="italic" tooltip="Italic (⌘+I)">
I
</MarkToolbarButton>
<MarkToolbarButton nodeType="underline" tooltip="Underline (⌘+U)">
U
</MarkToolbarButton>
</FixedToolbar>
<EditorContainer>
<Editor placeholder="Type your amazing content here..." />
</EditorContainer>
</Plate>
);
}
You'll need to implement your own toolbar to apply these marks. For example, to toggle bold: editor.tf.toggleMark('bold')
.
Adding Basic Elements
Now, let's add support for block-level elements like paragraphs (which is often a default), headings, and blockquotes.
import React from 'react';
import type { Value } from '@udecode/plate';
import { BasicMarksPlugin } from '@udecode/plate-basic-marks/react';
import { BasicElementsPlugin } from '@udecode/plate-basic-elements/react'; // Import the plugin
import { Plate, PlateContent, PlateLeaf, PlateElement, usePlateEditor, type PlateLeafProps, type PlateElementProps } from '@udecode/plate/react';
const initialValue: Value = [
{ type: 'h3', children: [{ text: 'Title' }] },
{ type: 'blockquote', children: [{ text: 'This is a quote.' }] },
{
type: 'p',
children: [
{ text: 'With some ' },
{ text: 'bold', bold: true },
{ text: ' text for emphasis!' },
],
},
];
export default function App() {
const editor = usePlateEditor({
value: initialValue,
plugins: [BasicElementsPlugin, BasicMarksPlugin], // Add BasicElementsPlugin
components: {
// Define how each element type should be rendered
// We use PlateElement and pass an 'as' prop for semantic HTML, or render directly.
h1: (props: PlateElementProps) => <PlateElement {...props} as="h1" />,
h2: (props: PlateElementProps) => <PlateElement {...props} as="h2" />,
p: (props: PlateElementProps) => <PlateElement {...props} as="p" />,
blockquote: (props: PlateElementProps) => <PlateElement {...props} as="blockquote" style={{ borderLeft: '2px solid #eee', marginLeft: 0, marginRight: 0, paddingLeft: '16px 64px', color: '#aaa' }} />,
// Marks from previous step
bold: (props: PlateLeafProps) => <PlateLeaf {...props} as="strong" />,
italic: (props: PlateLeafProps) => <PlateLeaf {...props} as="em" />,
underline: (props: PlateLeafProps) => <PlateLeaf {...props} as="u" />,
},
});
return (
<Plate editor={editor}>
{/* Toolbar here */}
<PlateContent style={{ padding: '16px 64px', minHeight: '100px' }}/>
</Plate>
);
}
Similar to marks, you map element type
s (e.g., 'h1'
, 'p'
) to components. PlateElement
is a helper for rendering element nodes. You'll need to implement UI (e.g., a dropdown) to toggle block types, for example, using editor.tf.toggleBlock('h1')
.
'use client';
import * as React from 'react';
import type { Value } from '@udecode/plate';
import { BasicElementsPlugin } from '@udecode/plate-basic-elements/react';
import { BasicMarksPlugin } from '@udecode/plate-basic-marks/react';
import {
type PlateElementProps,
type PlateLeafProps,
Plate,
PlateLeaf,
usePlateEditor,
} from '@udecode/plate/react';
import { BlockquoteElement } from '@/components/ui/blockquote-element';
import { Editor, EditorContainer } from '@/components/ui/editor';
import { FixedToolbar } from '@/components/ui/fixed-toolbar';
import { HeadingElement } from '@/components/ui/heading-element';
import { MarkToolbarButton } from '@/components/ui/mark-toolbar-button';
import { ParagraphElement } from '@/components/ui/paragraph-element';
import { ToolbarButton } from '@/components/ui/toolbar';
const initialValue: Value = [
{
children: [{ text: 'Title' }],
type: 'h3',
},
{
children: [{ text: 'This is a quote.' }],
type: 'blockquote',
},
{
children: [
{ text: 'With some ' },
{ bold: true, text: 'bold' },
{ text: ' text for emphasis!' },
],
type: 'p',
},
];
export default function MyEditorPage() {
const editor = usePlateEditor({
components: {
blockquote: BlockquoteElement,
p: ParagraphElement,
bold: function Bold(props: PlateLeafProps) {
return <PlateLeaf {...props} as="strong" />;
},
h1: function H1(props: PlateElementProps) {
return <HeadingElement {...props} variant="h1" />;
},
h2: function H2(props: PlateElementProps) {
return <HeadingElement {...props} variant="h2" />;
},
h3: function H3(props: PlateElementProps) {
return <HeadingElement {...props} variant="h3" />;
},
italic: function Italic(props: PlateLeafProps) {
return <PlateLeaf {...props} as="em" />;
},
underline: function Underline(props: PlateLeafProps) {
return <PlateLeaf {...props} as="u" />;
},
},
plugins: [BasicElementsPlugin, BasicMarksPlugin],
value: initialValue,
});
return (
<Plate editor={editor}>
<FixedToolbar className="flex justify-start gap-1 rounded-t-lg">
<ToolbarButton onClick={() => editor.tf.toggleBlock('h1')}>
H1
</ToolbarButton>
<ToolbarButton onClick={() => editor.tf.toggleBlock('h2')}>
H2
</ToolbarButton>
<ToolbarButton onClick={() => editor.tf.toggleBlock('h3')}>
H3
</ToolbarButton>
<ToolbarButton onClick={() => editor.tf.toggleBlock('blockquote')}>
Quote
</ToolbarButton>
<MarkToolbarButton nodeType="bold" tooltip="Bold (⌘+B)">
B
</MarkToolbarButton>
<MarkToolbarButton nodeType="italic" tooltip="Italic (⌘+I)">
I
</MarkToolbarButton>
<MarkToolbarButton nodeType="underline" tooltip="Underline (⌘+U)">
U
</MarkToolbarButton>
</FixedToolbar>
<EditorContainer>
<Editor placeholder="Type your amazing content here..." />
</EditorContainer>
</Plate>
);
}
Handling Editor Value
To make the editor's content persistent, let's integrate state management to save and load the editor's value.
import React, { useCallback } from 'react';
import type { Value } from '@udecode/plate';
import { BasicMarksPlugin } from '@udecode/plate-basic-marks/react';
import { BasicElementsPlugin } from '@udecode/plate-basic-elements/react';
import { Plate, PlateContent, PlateLeaf, PlateElement, usePlateEditor, type PlateLeafProps, type PlateElementProps } from '@udecode/plate/react';
const initialValue: Value = [
{ type: 'h3', children: [{ text: 'Title' }] },
{ type: 'blockquote', children: [{ text: 'This is a quote.' }] },
{
type: 'p',
children: [
{ text: 'With some ' },
{ text: 'bold', bold: true },
{ text: ' text for emphasis!' },
],
},
];
export default function App() {
const editor = usePlateEditor({
plugins: [BasicElementsPlugin, BasicMarksPlugin],
components: {
h1: (props: PlateElementProps) => <PlateElement {...props} as="h1" />,
h2: (props: PlateElementProps) => <PlateElement {...props} as="h2" />,
p: (props: PlateElementProps) => <PlateElement {...props} as="p" />,
blockquote: (props: PlateElementProps) => (
<PlateElement {...props} as="blockquote" style={{ borderLeft: '2px solid #eee', marginLeft: 0, marginRight: 0, paddingLeft: '16px 64px', color: '#aaa' }} />
),
bold: (props: PlateLeafProps) => <PlateLeaf {...props} as="strong" />,
italic: (props: PlateLeafProps) => <PlateLeaf {...props} as="em" />,
underline: (props: PlateLeafProps) => <PlateLeaf {...props} as="u" />,
},
value: () => {
const savedValue = localStorage.getItem('plate-value-demo');
if (savedValue) {
return JSON.parse(savedValue);
}
return initialValue;
},
});
return (
<Plate
editor={editor}
onChange={({ value }) => {
localStorage.setItem('plate-value-demo', JSON.stringify(value));
}}
>
{/* <FixedToolbar> */}
{/* ...buttons */}
<button onClick={() => editor.tf.setValue(initialValue)}>
Reset
</button>
{/* </FixedToolbar> */}
<PlateContent
style={{ padding: '16px 64px', minHeight: '100px' }}
placeholder="Type your amazing content here..."
/>
</Plate>
);
}
Value Management
The example above demonstrates a basic pattern for managing editor value:
- Initial value is set through the
value
option inusePlateEditor
- Changes can be handled via the
onChange
prop on<Plate>
- The reset button uses
editor.tf.setValue()
to restore the initial value - To control the value, see Controlled Value
'use client';
import * as React from 'react';
import type { Value } from '@udecode/plate';
import { BasicElementsPlugin } from '@udecode/plate-basic-elements/react';
import { BasicMarksPlugin } from '@udecode/plate-basic-marks/react';
import {
type PlateElementProps,
type PlateLeafProps,
Plate,
PlateLeaf,
usePlateEditor,
} from '@udecode/plate/react';
import { BlockquoteElement } from '@/components/ui/blockquote-element';
import { Editor, EditorContainer } from '@/components/ui/editor';
import { FixedToolbar } from '@/components/ui/fixed-toolbar';
import { HeadingElement } from '@/components/ui/heading-element';
import { MarkToolbarButton } from '@/components/ui/mark-toolbar-button';
import { ParagraphElement } from '@/components/ui/paragraph-element';
import { ToolbarButton } from '@/components/ui/toolbar';
const initialValue: Value = [
{
children: [{ text: 'Title' }],
type: 'h3',
},
{
children: [{ text: 'This is a quote.' }],
type: 'blockquote',
},
{
children: [
{ text: 'With some ' },
{ bold: true, text: 'bold' },
{ text: ' text for emphasis!' },
],
type: 'p',
},
];
export default function MyEditorPage() {
const editor = usePlateEditor({
components: {
blockquote: BlockquoteElement,
p: ParagraphElement,
bold: function Bold(props: PlateLeafProps) {
return <PlateLeaf {...props} as="strong" />;
},
h1: function H1(props: PlateElementProps) {
return <HeadingElement {...props} variant="h1" />;
},
h2: function H2(props: PlateElementProps) {
return <HeadingElement {...props} variant="h2" />;
},
h3: function H3(props: PlateElementProps) {
return <HeadingElement {...props} variant="h3" />;
},
italic: function Italic(props: PlateLeafProps) {
return <PlateLeaf {...props} as="em" />;
},
underline: function Underline(props: PlateLeafProps) {
return <PlateLeaf {...props} as="u" />;
},
},
plugins: [BasicElementsPlugin, BasicMarksPlugin],
value: () => {
const savedValue = localStorage.getItem(
`nextjs-plate-value-demo-${new Date().toISOString().split('T')[0]}`
);
if (savedValue) {
return JSON.parse(savedValue);
}
return initialValue;
},
});
if (typeof window === 'undefined') {
return null;
}
return (
<Plate
onChange={({ value }) => {
localStorage.setItem(
`nextjs-plate-value-demo-${new Date().toISOString().split('T')[0]}`,
JSON.stringify(value)
);
}}
editor={editor}
>
<FixedToolbar className="flex justify-start gap-1 rounded-t-lg">
<ToolbarButton onClick={() => editor.tf.toggleBlock('h1')}>
H1
</ToolbarButton>
<ToolbarButton onClick={() => editor.tf.toggleBlock('h2')}>
H2
</ToolbarButton>
<ToolbarButton onClick={() => editor.tf.toggleBlock('h3')}>
H3
</ToolbarButton>
<ToolbarButton onClick={() => editor.tf.toggleBlock('blockquote')}>
Quote
</ToolbarButton>
<MarkToolbarButton nodeType="bold" tooltip="Bold (⌘+B)">
B
</MarkToolbarButton>
<MarkToolbarButton nodeType="italic" tooltip="Italic (⌘+I)">
I
</MarkToolbarButton>
<MarkToolbarButton nodeType="underline" tooltip="Underline (⌘+U)">
U
</MarkToolbarButton>
<div className="flex-1" />
<ToolbarButton
className="px-2"
onClick={() => {
editor.tf.setValue(initialValue);
}}
>
Reset
</ToolbarButton>
</FixedToolbar>
<EditorContainer>
<Editor placeholder="Type your amazing content here..." />
</EditorContainer>
</Plate>
);
}
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.,
@udecode/plate-table
) and add them to yourplugins
array. - Build a Toolbar: Create React components for toolbar buttons that use the Editor Transforms to apply formatting (e.g.,
editor.tf.toggleMark('bold')
,editor.tf.toggleBlock('h1')
). You can also the editor state with the Editor API. - Editor Configuration
- Plugin Configuration
- Plugin Components