Getting Started
A quick tutorial to get you up and running with Plate.
Use our interactive builder to generate personalized installation steps.
Create project
You can choose one of the following templates to get started:
Option | NextJS | Plate | Plugins | AI & Backend |
---|---|---|---|---|
Notion clone template | ✅ | ✅ | ✅ | ✅ |
Plate playground template | ✅ | ✅ | ✅ | |
Plate minimal template | ✅ | ✅ | ||
NextJS template | ✅ |
For an existing project, jump to the next step.
Add dependencies
Install the core and the plugins you need. You need at least:
npm install @udecode/plate-common slate slate-react slate-history slate-hyperscript react react-dom
Alternatively you can install @udecode/plate
that contains all the packages excluding the ones with heavy dependencies (e.g. @udecode/plate-dnd
).
npm install @udecode/plate slate slate-react slate-history slate-hyperscript react react-dom
Basic Editor
Let's start with a minimal editor setup.
import {
usePlateEditor,
Plate,
PlateContent,
} from '@udecode/plate-common/react';
export default function BasicEditor() {
const editor = usePlateEditor();
return (
<Plate editor={editor}>
<PlateContent placeholder="Type..." />
</Plate>
);
}
Plate
manages the editor state and PlateContent
renders the editor content.
Styling
Let's give our editor some styles: Editor is a styled version of PlateContent
.
Note: Editor
is just an example of a styled editor using Tailwind, and
if you're using it, make sure to follow the installation steps in the Manual
Installation guide. You can create your
own styled version of PlateContent
.
import React from 'react';
import type { PlateContentProps } from '@udecode/plate-common/react';
import type { VariantProps } from 'class-variance-authority';
import { cn } from '@udecode/cn';
import { PlateContent } from '@udecode/plate-common/react';
import { cva } from 'class-variance-authority';
const editorVariants = cva(
cn(
'relative overflow-x-auto whitespace-pre-wrap break-words text-foreground',
'min-h-[80px] w-full rounded-md bg-background px-6 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none',
'[&_[data-slate-placeholder]]:text-muted-foreground [&_[data-slate-placeholder]]:!opacity-100',
'[&_[data-slate-placeholder]]:top-[auto_!important]',
'[&_strong]:font-bold'
),
{
defaultVariants: {
focusRing: true,
size: 'sm',
variant: 'outline',
},
variants: {
disabled: {
true: 'cursor-not-allowed opacity-50',
},
focusRing: {
false: '',
true: 'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
},
focused: {
true: 'ring-2 ring-ring ring-offset-2',
},
size: {
md: 'text-base',
sm: 'text-sm',
},
variant: {
aiChat:
'max-h-[min(70vh,320px)] w-full overflow-y-auto rounded-none border-b px-3 py-2 text-sm',
demo: 'min-h-full w-full px-16 pb-72 text-base sm:px-[max(64px,calc(50%-350px))]',
ghost: '',
outline: 'border border-input',
},
},
}
);
export type EditorProps = PlateContentProps &
VariantProps<typeof editorVariants>;
const Editor = React.forwardRef<HTMLDivElement, EditorProps>(
(
{
className,
disabled,
focusRing,
focused,
readOnly,
size,
variant,
...props
},
ref
) => {
return (
<div ref={ref} className="relative w-full">
<PlateContent
className={cn(
editorVariants({
disabled,
focusRing,
focused,
size,
variant,
}),
className
)}
readOnly={disabled ?? readOnly}
aria-disabled={disabled}
data-plate-selectable
disableDefaultStyles
{...props}
/>
</div>
);
}
);
Editor.displayName = 'Editor';
export { Editor };
Initializing Editor's Value
Let's specify the initial content of the editor: a single paragraph.
// ...
const value = [
{
type: 'p',
children: [
{
text: 'This is editable plain text with react and history plugins, just like a <textarea>!',
},
],
},
];
export default function BasicEditor() {
const editor = usePlateEditor({
value,
});
return (
<Plate editor={editor}>
<PlateContent />
</Plate>
);
}
Note: Plate uses the type
property to enable plugins to render nodes
by type.
Implementing Change Handler
At this stage, it's crucial to monitor editor modifications in order to store the values appropriately. The onChange
prop will serve this purpose. You can also persist the editor's state by saving the value to local storage or a database and loading it back when needed.
// ...
export default function BasicEditor() {
const localValue =
typeof window !== 'undefined' && localStorage.getItem('editorContent');
const editor = usePlateEditor({
value: localValue ? JSON.parse(localValue) : value,
});
return (
<Plate
editor={editor}
onChange={({ value }) => {
localStorage.setItem('editorContent', JSON.stringify(value));
}}
>
<PlateContent />
</Plate>
);
}
Plugins
Use our interactive builder to pick your plugins.
Let's use the basic plugins for a rich-text editor.
// ...
import {
BoldPlugin,
ItalicPlugin,
UnderlinePlugin,
CodePlugin,
} from '@udecode/plate-basic-marks/react';
import { HeadingPlugin } from '@udecode/plate-heading/react';
import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
import { CodeBlockPlugin } from '@udecode/plate-code-block/react';
const value = [
// ...
];
export default function BasicEditor() {
const editor = usePlateEditor({
value,
plugins: [
HeadingPlugin,
BlockquotePlugin,
CodeBlockPlugin,
BoldPlugin,
ItalicPlugin,
UnderlinePlugin,
CodePlugin,
],
});
return (
<Plate editor={editor}>
<PlateContent />
</Plate>
);
}
The plugins are functioning correctly. However, since we haven't specified any custom components for rendering, the editor is using the default (unstyled) components. Specifically, the default element component is a div
, and the default leaf component is a span
.
Note: You don't need to add core plugins such as ReactPlugin
,
HistoryPlugin
and ParagraphPlugin
as usePlateEditor
already
does it.
Components
Note: Plate plugins are packaged unstyled, implying that you have complete control over markup and styling, hence you can integrate your own design system or Plate UI. If using the latter, use our interactive builder to pick your components.
To plug-in all the components in one place, we can use the override.components
option in usePlateEditor
:
// ...
// Import Prism for code highlighting
import Prism from 'prismjs';
// This is a local file, you will need to create this file in your project
import { createPlateUI } from '@/lib/create-plate-ui';
export default function BasicEditor() {
const editor = usePlateEditor({
plugins: [
HeadingPlugin,
BlockquotePlugin,
CodeBlockPlugin.configure({ options: { prism: Prism } }),
BoldPlugin,
ItalicPlugin,
UnderlinePlugin,
CodePlugin,
],
override: {
components: createPlateUI(),
},
});
return (
<Plate editor={editor}>
<PlateContent />
</Plate>
);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import { withProps } from '@udecode/cn';
import { AIPlugin } from '@udecode/plate-ai/react';
import {
BoldPlugin,
CodePlugin,
ItalicPlugin,
StrikethroughPlugin,
SubscriptPlugin,
SuperscriptPlugin,
UnderlinePlugin,
} from '@udecode/plate-basic-marks/react';
import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
import {
CodeBlockPlugin,
CodeLinePlugin,
CodeSyntaxPlugin,
} from '@udecode/plate-code-block/react';
import { CommentsPlugin } from '@udecode/plate-comments/react';
import { ParagraphPlugin } from '@udecode/plate-common/react';
import {
type NodeComponent,
PlateElement,
PlateLeaf,
} from '@udecode/plate-common/react';
import { DatePlugin } from '@udecode/plate-date/react';
import { EmojiInputPlugin } from '@udecode/plate-emoji/react';
import { ExcalidrawPlugin } from '@udecode/plate-excalidraw/react';
import { FindReplacePlugin } from '@udecode/plate-find-replace';
import { HEADING_KEYS } from '@udecode/plate-heading';
import { TocPlugin } from '@udecode/plate-heading/react';
import { HighlightPlugin } from '@udecode/plate-highlight/react';
import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';
import { KbdPlugin } from '@udecode/plate-kbd/react';
import { ColumnItemPlugin, ColumnPlugin } from '@udecode/plate-layout/react';
import { LinkPlugin } from '@udecode/plate-link/react';
import {
BulletedListPlugin,
ListItemPlugin,
NumberedListPlugin,
TodoListPlugin,
} from '@udecode/plate-list/react';
import { ImagePlugin, MediaEmbedPlugin } from '@udecode/plate-media/react';
import {
MentionInputPlugin,
MentionPlugin,
} from '@udecode/plate-mention/react';
import { SlashInputPlugin } from '@udecode/plate-slash-command/react';
import {
TableCellHeaderPlugin,
TableCellPlugin,
TablePlugin,
TableRowPlugin,
} from '@udecode/plate-table/react';
import { TogglePlugin } from '@udecode/plate-toggle/react';
import { AILeaf } from '@/registry/default/plate-ui/ai-leaf';
import { BlockquoteElement } from '@/registry/default/plate-ui/blockquote-element';
import { CodeBlockElement } from '@/registry/default/plate-ui/code-block-element';
import { CodeLeaf } from '@/registry/default/plate-ui/code-leaf';
import { CodeLineElement } from '@/registry/default/plate-ui/code-line-element';
import { CodeSyntaxLeaf } from '@/registry/default/plate-ui/code-syntax-leaf';
import { ColumnElement } from '@/registry/default/plate-ui/column-element';
import { ColumnGroupElement } from '@/registry/default/plate-ui/column-group-element';
import { CommentLeaf } from '@/registry/default/plate-ui/comment-leaf';
import { DateElement } from '@/registry/default/plate-ui/date-element';
import { EmojiInputElement } from '@/registry/default/plate-ui/emoji-input-element';
import { ExcalidrawElement } from '@/registry/default/plate-ui/excalidraw-element';
import { HeadingElement } from '@/registry/default/plate-ui/heading-element';
import { HighlightLeaf } from '@/registry/default/plate-ui/highlight-leaf';
import { HrElement } from '@/registry/default/plate-ui/hr-element';
import { ImageElement } from '@/registry/default/plate-ui/image-element';
import { KbdLeaf } from '@/registry/default/plate-ui/kbd-leaf';
import { LinkElement } from '@/registry/default/plate-ui/link-element';
import { ListElement } from '@/registry/default/plate-ui/list-element';
import { MediaEmbedElement } from '@/registry/default/plate-ui/media-embed-element';
import { MentionElement } from '@/registry/default/plate-ui/mention-element';
import { MentionInputElement } from '@/registry/default/plate-ui/mention-input-element';
import { ParagraphElement } from '@/registry/default/plate-ui/paragraph-element';
import { withPlaceholders } from '@/registry/default/plate-ui/placeholder';
import { SearchHighlightLeaf } from '@/registry/default/plate-ui/search-highlight-leaf';
import { SlashInputElement } from '@/registry/default/plate-ui/slash-input-element';
import {
TableCellElement,
TableCellHeaderElement,
} from '@/registry/default/plate-ui/table-cell-element';
import { TableElement } from '@/registry/default/plate-ui/table-element';
import { TableRowElement } from '@/registry/default/plate-ui/table-row-element';
import { TocElement } from '@/registry/default/plate-ui/toc-element';
import { TodoListElement } from '@/registry/default/plate-ui/todo-list-element';
import { ToggleElement } from '@/registry/default/plate-ui/toggle-element';
import { withDraggables } from '@/registry/default/plate-ui/with-draggables';
export const createPlateUI = ({
draggable,
placeholder,
}: { draggable?: boolean; placeholder?: boolean } = {}) => {
let components: Record<string, NodeComponent> = {
[AIPlugin.key]: AILeaf,
[BlockquotePlugin.key]: BlockquoteElement,
[BoldPlugin.key]: withProps(PlateLeaf, { as: 'strong' }),
[BulletedListPlugin.key]: withProps(ListElement, { variant: 'ul' }),
[CodeBlockPlugin.key]: CodeBlockElement,
[CodeLinePlugin.key]: CodeLineElement,
[CodePlugin.key]: CodeLeaf,
[CodeSyntaxPlugin.key]: CodeSyntaxLeaf,
[ColumnItemPlugin.key]: ColumnElement,
[ColumnPlugin.key]: ColumnGroupElement,
[CommentsPlugin.key]: CommentLeaf,
[DatePlugin.key]: DateElement,
[EmojiInputPlugin.key]: EmojiInputElement,
[ExcalidrawPlugin.key]: ExcalidrawElement,
[FindReplacePlugin.key]: SearchHighlightLeaf,
[HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }),
[HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }),
[HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }),
[HEADING_KEYS.h4]: withProps(HeadingElement, { variant: 'h4' }),
[HEADING_KEYS.h5]: withProps(HeadingElement, { variant: 'h5' }),
[HEADING_KEYS.h6]: withProps(HeadingElement, { variant: 'h6' }),
[HighlightPlugin.key]: HighlightLeaf,
[HorizontalRulePlugin.key]: HrElement,
[ImagePlugin.key]: ImageElement,
[ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }),
[KbdPlugin.key]: KbdLeaf,
[LinkPlugin.key]: LinkElement,
[ListItemPlugin.key]: withProps(PlateElement, { as: 'li' }),
[MediaEmbedPlugin.key]: MediaEmbedElement,
[MentionInputPlugin.key]: MentionInputElement,
[MentionPlugin.key]: MentionElement,
[NumberedListPlugin.key]: withProps(ListElement, { variant: 'ol' }),
[ParagraphPlugin.key]: ParagraphElement,
[SlashInputPlugin.key]: SlashInputElement,
[StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }),
[SubscriptPlugin.key]: withProps(PlateLeaf, { as: 'sub' }),
[SuperscriptPlugin.key]: withProps(PlateLeaf, { as: 'sup' }),
[TableCellHeaderPlugin.key]: TableCellHeaderElement,
[TableCellPlugin.key]: TableCellElement,
[TablePlugin.key]: TableElement,
[TableRowPlugin.key]: TableRowElement,
[TocPlugin.key]: TocElement,
[TodoListPlugin.key]: TodoListElement,
[TogglePlugin.key]: ToggleElement,
[UnderlinePlugin.key]: withProps(PlateLeaf, { as: 'u' }),
};
if (placeholder) {
components = withPlaceholders(components);
}
if (draggable) {
components = withDraggables(components);
}
return components;
};
Initializing Editor's Value with HTML String
You can also specify the initial content of the editor using an HTML string and the corresponding plugins.
// ...
const htmlValue = '<p>This is <b>bold</b> and <i>italic</i> text!</p>';
export default function BasicEditor() {
const editor = usePlateEditor({
value: htmlValue,
plugins: [BoldPlugin, ItalicPlugin],
});
return (
<Plate editor={editor}>
<PlateContent />
</Plate>
);
}
That's it!
You can now play around with the Playground and start building your own editor.
Customizable and extensible.