Installation
Get started with Plate Plus components in your project.
This guide will walk you through the process of installing and setting up Plate Plus components in your project. We'll cover the core setup, editor configuration, and optional enhancements.
Core Setup
Add dependencies
Install the core Plate dependencies:
npm install @udecode/plate-common slate slate-react slate-history slate-hyperscript react react-dom
Install the UI dependencies:
npm install @udecode/cn class-variance-authority lucide-react sonner tailwind-merge tailwindcss-animate @tailwindcss/typography tailwind-scrollbar-hide
Add Tailwind CSS
Components are styled using Tailwind CSS. You need to install Tailwind CSS in your project.
Follow the Tailwind CSS installation instructions to get started.
Configure path aliases
We use the @
alias:
{
"compilerOptions": {
"baseUrl": "src",
"paths": { "@/": ["./"] }
}
}
The @
alias is a preference. You can use other aliases if you want.
If you use a different alias such as ~, you'll need to update import statements when adding components.
Configure tailwind.config.ts
Tailwind CSS is a crucial part of Plate Plus's styling system. Here's what your tailwind.config.ts
file should look like, excluding content
and presets
:
import type { Config } from 'tailwindcss';
import nextAdminPreset from '@premieroctet/next-admin/preset';
import typography from '@tailwindcss/typography';
import scrollbarHide from 'tailwind-scrollbar-hide';
import { fontFamily } from 'tailwindcss/defaultTheme';
import plugin from 'tailwindcss/plugin';
import animate from 'tailwindcss-animate';
const config: Config = {
content: [
'./src/**/*.{js,ts,jsx,tsx,mdx}',
'../../packages/ui/src/**/*.{js,ts,jsx,tsx,mdx}',
'../../packages/editor/src/**/*.{js,ts,jsx,tsx,mdx}',
'./node_modules/@premieroctet/next-admin/dist/**/*.{js,ts,jsx,tsx}',
'./src/content/**/*.mdx',
'./src/registry/**/*.{ts,tsx}',
],
darkMode: 'class',
plugins: [
scrollbarHide,
animate,
typography,
plugin(function ({ addUtilities }) {
addUtilities({
'.transition-bg-ease': {
'transition-duration': '20ms',
'transition-property': 'background-color',
'transition-timing-function': 'ease-in',
},
});
}),
],
presets: [nextAdminPreset],
theme: {
container: {
screens: {
'2xl': '1400px',
},
},
extend: {
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
'ai-bounce': 'ai-bounce 0.4s infinite',
'fade-down': 'fade-down 0.5s',
'fade-in': 'fade-in 0.4s',
'fade-out': 'fade-out 0.4s',
'fade-up': 'fade-up 0.5s',
popover: 'popover 100ms ease-in',
pulse: 'pulse var(--duration) ease-out infinite',
shimmer: 'shimmer 4s ease-in-out infinite',
shine: 'shine 8s ease-in-out infinite',
sunlight: 'sunlight 4s linear infinite',
zoom: 'zoom 100ms ease-in',
},
animationTimingFunction: {
vaul: 'cubic-bezier(0.32, 0.72, 0, 1)',
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
xl: `calc(var(--radius) + 4px)`,
xs: 'calc(var(--radius) - 6px)',
},
boxShadow: {
floating:
'rgba(16,16,16,0.06) 0px 0px 0px 1px, rgba(16,16,16,0.11) 0px 3px 7px, rgba(16,16,16,0.21) 0px 9px 25px',
toolbar:
'rgba(0, 0, 0, 0.08) 0px 16px 24px 0px, rgba(0, 0, 0, 0.1) 0px 2px 6px 0px, rgba(0, 0, 0, 0.1) 0px 0px 1px 0px',
},
colors: {
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
background: 'hsl(var(--background))',
border: 'hsl(var(--border))',
brand: {
DEFAULT: 'hsl(var(--brand))',
active: 'hsl(var(--brand-active))',
foreground: 'hsl(var(--brand-foreground))',
hover: 'hsl(var(--brand-hover))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
foreground: 'hsl(var(--foreground))',
highlight: {
DEFAULT: 'hsl(var(--highlight))',
foreground: 'hsl(var(--highlight-foreground))',
},
input: 'hsl(var(--input))',
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
ring: 'hsla(210, 77%, 51%, 0.57)',
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
'subtle-foreground': 'hsl(var(--subtle-foreground))',
},
fontFamily: {
heading: ['var(--font-heading)', ...fontFamily.sans],
sans: ['var(--font-sans)', ...fontFamily.sans],
},
keyframes: {
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' },
},
'ai-bounce': {
'0%, 100%': {
animationTimingFunction: 'cubic-bezier(0,0,0.2,1)',
transform: 'translateY(20%)',
},
'50%': {
animationTimingFunction: 'cubic-bezier(0.8,0,1,1)',
transform: 'translateY(-20%)',
},
},
'fade-down': {
'0%': {
opacity: '0',
transform: 'translateY(-10px)',
},
'80%': {
opacity: '0.6',
},
'100%': {
opacity: '1',
transform: 'translateY(0px)',
},
},
'fade-in': {
'0%': {
opacity: '0',
},
'50%': {
opacity: '0.6',
},
'100%': {
opacity: '1',
},
},
'fade-out': {
'0%': {
opacity: '0',
},
'50%': {
opacity: '0.6',
},
'100%': {
opacity: '1',
},
},
'fade-up': {
'0%': {
opacity: '0',
transform: 'translateY(10px)',
},
'80%': {
opacity: '0.7',
},
'100%': {
opacity: '1',
transform: 'translateY(0px)',
},
},
popover: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
pulse: {
'0%, 100%': { boxShadow: '0 0 0 0 var(--pulse-color)' },
'50%': { boxShadow: '0 0 0 8px var(--pulse-color)' },
},
shimmer: {
'0%': { transform: 'translateX(-150%)' },
'100%': { transform: 'translateX(150%)' },
},
shine: {
from: { backgroundPosition: '200% 0' },
to: { backgroundPosition: '-200% 0' },
},
sunlight: {
'0%': { transform: 'translateX(-50%) rotate(0deg)' },
'100%': { transform: 'translateX(0%) rotate(0deg)' },
},
zoom: {
'0%': { opacity: '0', transform: 'scale(0.95)' },
'100%': { opacity: '1', transform: 'scale(1)' },
},
},
},
},
};
export default config;
Configure styles
These styles provide the base styling for Plate Plus components. Add the following to your styles/globals.css
file:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--subtle-foreground: 240 5% 34%;
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 72.22% 50.59%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5% 64.9%;
--radius: 0.5rem;
--brand: 211 77% 51%;
--highlight: 48 100% 50%;
:focus-visible {
@apply outline-none;
}
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 85.7% 97.3%;
--border: 240 3.7% 25%;
--input: 240 3.7% 25%;
--ring: 240 3.7% 25%;
--brand: 211 77% 51%;
--highlight: 48 100% 50%;
}
}
@layer base {
* {
@apply border-border;
}
/* html {
@apply scroll-smooth;
} */
body {
@apply bg-background text-foreground;
font-feature-settings:
'rlig' 1,
'calt' 1;
/*font-synthesis-weight: none;*/
/*text-rendering: optimizeLegibility;*/
}
svg {
@apply size-4;
}
[aria-label='Open Tanstack query devtools'] {
width: 20px !important;
height: 20px !important;
}
}
@layer utilities {
.container {
@apply px-8 mx-auto;
}
.custom-scrollbar::-webkit-scrollbar {
width: 5px;
height: 3px;
border-radius: 2px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
border-radius: 50px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #888;
border-radius: 50px;
}
::selection {
@apply bg-brand/25;
}
.step {
counter-increment: step;
}
.step:before {
@apply absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 border-background bg-muted text-center -indent-px font-mono text-base font-medium;
@apply ml-[-50px] mt-[-4px];
content: counter(step);
}
.blur-white {
@apply bg-white/95 backdrop-blur-sm supports-[backdrop-filter]:bg-white/60;
}
.chunk-container {
@apply shadow-none;
}
.chunk-container::after {
content: '';
@apply absolute -inset-4 shadow-xl rounded-xl border;
}
.focus-ring:focus-visible {
@apply ring-2 ring-ring ring-offset-2;
}
.no-focus-ring:focus-visible {
@apply !ring-0 !ring-offset-0;
}
}
@media (max-width: 640px) {
.container {
@apply px-4;
}
}
Setting up the editor layout
Layout
When setting up your editor component, you'll need to import some styles and set up the basic structure:
import '@/styles/globals.css';
import { cn } from '@udecode/cn';
import { Toaster } from 'sonner';
export default function Layout() {
return (
<div
className={cn(
'min-h-screen bg-background font-sans antialiased [&_.slate-selection-area]:bg-brand/15'
)}
>
{/* Your editor component goes here */}
<Toaster />
</div>
);
}
(Optional) Fonts
If you're using Next.js and want to use custom fonts, you can set them up like this:
- First, create a file for your fonts:
import {
Inter as FontSans,
JetBrains_Mono as FontMono,
} from 'next/font/google';
export const fontSans = FontSans({
subsets: ['latin'],
variable: '--font-sans',
});
export const fontMono = FontMono({
subsets: ['latin'],
variable: '--font-mono',
});
- Then, import and use these fonts in your component:
import '@/styles/globals.css';
import { cn } from '@udecode/cn';
import { Toaster } from 'sonner';
import { fontSans, fontMono } from '@/lib/fonts';
export default function Layout() {
return (
<div
className={cn(
'min-h-screen bg-background font-sans antialiased [&_.slate-selection-area]:bg-brand/15',
fontSans.variable,
fontMono.variable
)}
>
{/* Your editor component goes here */}
<Toaster />
</div>
);
}
(Optional) Dark mode
If you want to add dark mode to your project, you can use next-themes
. Here's how:
- Install next-themes:
npm install next-themes
- Create a theme provider:
'use client';
import * as React from 'react';
import { ThemeProvider as NextThemesProvider } from 'next-themes';
import { type ThemeProviderProps } from 'next-themes/dist/types';
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}
- Use the ThemeProvider in your root layout:
import { cn } from '@udecode/cn';
import { ThemeProvider } from '@/components/theme-provider';
import { fontSans } from '@/lib/fonts';
import { Toaster } from 'sonner';
export default function Layout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={cn(
'min-h-screen bg-background font-sans antialiased [&_.slate-selection-area]:bg-brand/15',
fontSans.variable,
fontMono.variable
)}
>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
<Toaster />
</ThemeProvider>
</body>
</html>
);
}
Editor Configuration
Editor
Now let's set up the core editor component:
import React from 'react';
import { Plate, usePlateEditor } from '@udecode/plate-common/react';
import { Editor } from '@/components/potion-ui/editor';
import { createPotionUI } from '@/components/editor/create-potion-ui';
import { commonPlugins } from '@/components/editor/plugins';
export default function Demo() {
const editor = usePlateEditor({
id: 'main',
override: {
components: createPotionUI(),
},
plugins: [
...commonPlugins,
// Add any additional plugins here
],
});
return (
<Plate editor={editor}>
<Editor />
</Plate>
);
}
'use client';
import { AlignPlugin } from '@udecode/plate-alignment/react';
import { AutoformatPlugin } from '@udecode/plate-autoformat/react';
import { BasicMarksPlugin } from '@udecode/plate-basic-marks/react';
import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
import { ExitBreakPlugin, SoftBreakPlugin } from '@udecode/plate-break/react';
import { CalloutPlugin } from '@udecode/plate-callout/react';
import { CaptionPlugin } from '@udecode/plate-caption/react';
import { CodeBlockPlugin } from '@udecode/plate-code-block/react';
import { CommentsPlugin } from '@udecode/plate-comments/react';
import { ParagraphPlugin } from '@udecode/plate-common/react';
import { DatePlugin } from '@udecode/plate-date/react';
import { DndPlugin } from '@udecode/plate-dnd';
import { DocxPlugin } from '@udecode/plate-docx';
import { EmojiPlugin } from '@udecode/plate-emoji/react';
import {
FontBackgroundColorPlugin,
FontColorPlugin,
} from '@udecode/plate-font/react';
import { HEADING_KEYS, HEADING_LEVELS } from '@udecode/plate-heading';
import { HeadingPlugin, TocPlugin } from '@udecode/plate-heading/react';
import { HighlightPlugin } from '@udecode/plate-highlight/react';
import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';
import { IndentPlugin } from '@udecode/plate-indent/react';
import { IndentListPlugin } from '@udecode/plate-indent-list/react';
import { JuicePlugin } from '@udecode/plate-juice';
import { KbdPlugin } from '@udecode/plate-kbd/react';
import { ColumnPlugin } from '@udecode/plate-layout/react';
import { LinkPlugin } from '@udecode/plate-link/react';
import { MarkdownPlugin } from '@udecode/plate-markdown';
import {
EquationPlugin,
InlineEquationPlugin,
} from '@udecode/plate-math/react';
import {
AudioPlugin,
FilePlugin,
ImagePlugin,
MediaEmbedPlugin,
PlaceholderPlugin,
VideoPlugin,
} from '@udecode/plate-media/react';
import { MentionPlugin } from '@udecode/plate-mention/react';
import { NodeIdPlugin } from '@udecode/plate-node-id';
import { DeletePlugin, SelectOnBackspacePlugin } from '@udecode/plate-select';
import {
BlockMenuPlugin,
BlockSelectionPlugin,
} from '@udecode/plate-selection/react';
import { SlashPlugin } from '@udecode/plate-slash-command/react';
import { TableCellPlugin, TablePlugin } from '@udecode/plate-table/react';
import { TogglePlugin } from '@udecode/plate-toggle/react';
import { TrailingBlockPlugin } from '@udecode/plate-trailing-block';
import Prism from 'prismjs';
import { autoformatRules } from '@/components/editor/autoformatOptions';
import { aiPlugins } from '@/example/ai-plugins';
import { BlockContextMenu } from '@/components/potion-ui/block-context-menu';
import { CommentsPopover } from '@/components/potion-ui/comments-popover';
import { ImageElement } from '@/components/potion-ui/image-element';
import { ImagePreview } from '@/components/potion-ui/image-preview';
import {
TodoLi,
TodoMarker,
} from '@/components/potion-ui/indent-todo-marker';
import { LinkFloatingToolbar } from '@/components/potion-ui/link-floating-toolbar';
import { SelectionOverlayPlugin } from '@/components/potion-ui/selection-overlay';
import { resetNodePlugin } from './reset-node-plugin';
export const commonPlugins = [
// Nodes
HeadingPlugin.configure({ options: { levels: 3 } }),
BlockquotePlugin,
CodeBlockPlugin.configure({ options: { prism: Prism } }),
HorizontalRulePlugin,
LinkPlugin.extend({
render: { afterEditable: () => <LinkFloatingToolbar /> },
}),
ImagePlugin.extend({
options: { disableUploadInsert: true },
render: { afterEditable: ImagePreview },
}),
MediaEmbedPlugin,
CaptionPlugin.configure({
options: { plugins: [ImagePlugin, MediaEmbedPlugin] },
}),
DatePlugin,
MentionPlugin.configure({
options: { triggerPreviousCharPattern: /^$|^[\s"']$/ },
}),
SlashPlugin,
TablePlugin.configure({ options: { enableMerging: true } }),
TogglePlugin,
TocPlugin.configure({
options: {
isScroll: true,
scrollContainerSelector: '#scroll_container',
topOffset: 80,
},
}),
PlaceholderPlugin,
ImagePlugin.extend({
render: {
afterEditable: ImagePreview,
node: ImageElement,
},
}),
VideoPlugin,
AudioPlugin,
MediaEmbedPlugin,
FilePlugin,
CaptionPlugin.configure({
options: { plugins: [ImagePlugin] },
}),
InlineEquationPlugin,
EquationPlugin,
CalloutPlugin,
ColumnPlugin,
// Marks
BasicMarksPlugin,
FontColorPlugin,
FontBackgroundColorPlugin,
HighlightPlugin,
KbdPlugin,
// Block Style
AlignPlugin.extend({
inject: {
targetPlugins: [
ParagraphPlugin.key,
MediaEmbedPlugin.key,
HEADING_KEYS.h1,
HEADING_KEYS.h2,
HEADING_KEYS.h3,
HEADING_KEYS.h4,
HEADING_KEYS.h5,
ImagePlugin.key,
HEADING_KEYS.h6,
],
},
}),
IndentPlugin.extend({
inject: {
targetPlugins: [
ParagraphPlugin.key,
HEADING_KEYS.h1,
HEADING_KEYS.h2,
HEADING_KEYS.h3,
HEADING_KEYS.h4,
HEADING_KEYS.h5,
HEADING_KEYS.h6,
BlockquotePlugin.key,
CodeBlockPlugin.key,
TogglePlugin.key,
],
},
}),
IndentListPlugin.extend({
inject: {
targetPlugins: [
ParagraphPlugin.key,
HEADING_KEYS.h1,
HEADING_KEYS.h2,
HEADING_KEYS.h3,
HEADING_KEYS.h4,
HEADING_KEYS.h5,
HEADING_KEYS.h6,
BlockquotePlugin.key,
CodeBlockPlugin.key,
TogglePlugin.key,
],
},
options: {
listStyleTypes: {
todo: {
liComponent: TodoLi,
markerComponent: TodoMarker,
type: 'todo',
},
},
},
}),
// Functionality
AutoformatPlugin.configure({
options: {
enableUndoOnDelete: true,
rules: autoformatRules,
},
}),
BlockSelectionPlugin.configure({
options: {
areaOptions: {
behaviour: {
scrolling: {
startScrollMargins: { x: 0, y: 0 },
},
},
boundaries: '#scroll_container',
container: '#scroll_container',
selectables: '#scroll_container .slate-selectable',
selectionAreaClass: 'slate-selection-area',
},
enableContextMenu: true,
},
}),
BlockMenuPlugin.extend(({ api }) => ({
handlers: {
onMouseDown: ({ event, getOptions }) => {
// Prevent unset block selection when menu is closing
if (event.button === 0 && getOptions().openId) {
event.preventDefault();
api.blockMenu.hide();
}
if (event.button === 2) event.preventDefault();
},
},
})).configure({
render: { aboveEditable: BlockContextMenu },
}),
DndPlugin.configure({
options: {
enableScroller: true,
onDropFiles: ({ dragItem, editor, target }) => {
editor
.getTransforms(PlaceholderPlugin)
.insert.media(dragItem.files, target);
},
},
}),
EmojiPlugin,
ExitBreakPlugin.configure({
options: {
rules: [
{
hotkey: 'mod+enter',
},
{
before: true,
hotkey: 'mod+shift+enter',
},
{
hotkey: 'enter',
level: 1,
query: {
allow: HEADING_LEVELS,
end: true,
start: true,
},
relative: true,
},
],
},
}),
NodeIdPlugin,
resetNodePlugin,
SelectOnBackspacePlugin.configure({
options: {
query: {
allow: [ImagePlugin.key, HorizontalRulePlugin.key],
},
},
}),
DeletePlugin,
SoftBreakPlugin.configure({
options: {
rules: [
{ hotkey: 'shift+enter' },
{
hotkey: 'enter',
query: {
allow: [
CodeBlockPlugin.key,
BlockquotePlugin.key,
TableCellPlugin.key,
CalloutPlugin.key,
],
},
},
],
},
}),
TrailingBlockPlugin.configure({
options: { type: ParagraphPlugin.key },
}),
// SelectionOverlay,
SelectionOverlayPlugin,
// Collaboration
CommentsPlugin.configure({
options: {
myUserId: '1',
users: {
1: {
id: '1',
avatarUrl:
'https://avatars.githubusercontent.com/u/19695832?s=96&v=4',
name: 'zbeyens',
},
},
},
render: { afterEditable: () => <CommentsPopover /> },
}),
// Deserialization
DocxPlugin,
MarkdownPlugin.configure({
options: {
indentList: true,
},
}),
JuicePlugin,
// AI
...aiPlugins,
];
import {
type AutoformatRule,
autoformatArrow,
autoformatLegal,
autoformatLegalHtml,
autoformatMath,
autoformatPunctuation,
autoformatSmartQuotes,
} from '@udecode/plate-autoformat';
import {
BoldPlugin,
CodePlugin,
ItalicPlugin,
StrikethroughPlugin,
SubscriptPlugin,
SuperscriptPlugin,
UnderlinePlugin,
} from '@udecode/plate-basic-marks/react';
import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
import { insertEmptyCodeBlock } from '@udecode/plate-code-block';
import {
CodeBlockPlugin,
CodeLinePlugin,
} from '@udecode/plate-code-block/react';
import {
type SlateEditor,
getParentNode,
insertNodes,
isElement,
isType,
setNodes,
} from '@udecode/plate-common';
import { ParagraphPlugin } from '@udecode/plate-common/react';
import { HEADING_KEYS } from '@udecode/plate-heading';
import { HighlightPlugin } from '@udecode/plate-highlight/react';
import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';
import {
INDENT_LIST_KEYS,
ListStyleType,
toggleIndentList,
} from '@udecode/plate-indent-list';
import { TogglePlugin, openNextToggles } from '@udecode/plate-toggle/react';
export const format = (editor: SlateEditor, customFormatting: any) => {
if (editor.selection) {
const parentEntry = getParentNode(editor, editor.selection);
if (!parentEntry) return;
const [node] = parentEntry;
if (
isElement(node) &&
!isType(editor, node, CodeBlockPlugin.key) &&
!isType(editor, node, CodeLinePlugin.key)
) {
customFormatting();
}
}
};
export const autoformatMarks: AutoformatRule[] = [
{
match: '***',
mode: 'mark',
type: [BoldPlugin.key, ItalicPlugin.key],
},
{
match: '__*',
mode: 'mark',
type: [UnderlinePlugin.key, ItalicPlugin.key],
},
{
match: '__**',
mode: 'mark',
type: [UnderlinePlugin.key, BoldPlugin.key],
},
{
match: '___***',
mode: 'mark',
type: [UnderlinePlugin.key, BoldPlugin.key, ItalicPlugin.key],
},
{
match: '**',
mode: 'mark',
type: BoldPlugin.key,
},
{
match: '__',
mode: 'mark',
type: UnderlinePlugin.key,
},
{
match: '*',
mode: 'mark',
type: ItalicPlugin.key,
},
{
match: '_',
mode: 'mark',
type: ItalicPlugin.key,
},
{
match: '~~',
mode: 'mark',
type: StrikethroughPlugin.key,
},
{
match: '^',
mode: 'mark',
type: SuperscriptPlugin.key,
},
{
match: '~',
mode: 'mark',
type: SubscriptPlugin.key,
},
{
match: '==',
mode: 'mark',
type: HighlightPlugin.key,
},
{
match: '≡',
mode: 'mark',
type: HighlightPlugin.key,
},
{
match: '`',
mode: 'mark',
type: CodePlugin.key,
},
];
export const autoformatBlocks: AutoformatRule[] = [
{
match: '# ',
mode: 'block',
type: HEADING_KEYS.h1,
},
{
match: '## ',
mode: 'block',
type: HEADING_KEYS.h2,
},
{
match: '### ',
mode: 'block',
type: HEADING_KEYS.h3,
},
{
match: '#### ',
mode: 'block',
type: HEADING_KEYS.h4,
},
{
match: '##### ',
mode: 'block',
type: HEADING_KEYS.h5,
},
{
match: '###### ',
mode: 'block',
type: HEADING_KEYS.h6,
},
{
match: '> ',
mode: 'block',
type: BlockquotePlugin.key,
},
{
format: (editor) => {
insertEmptyCodeBlock(editor, {
defaultType: ParagraphPlugin.key,
insertNodesOptions: { select: true },
});
},
match: '```',
mode: 'block',
triggerAtBlockStart: false,
type: CodeBlockPlugin.key,
},
{
match: '+ ',
mode: 'block',
preFormat: openNextToggles,
type: TogglePlugin.key,
},
{
format: (editor) => {
setNodes(editor, { type: HorizontalRulePlugin.key });
insertNodes(editor, {
children: [{ text: '' }],
type: ParagraphPlugin.key,
});
},
match: ['---', '—-', '___ '],
mode: 'block',
type: HorizontalRulePlugin.key,
},
];
export const autoformatIndentLists: AutoformatRule[] = [
{
format: (editor) => {
toggleIndentList(editor, {
listStyleType: ListStyleType.Disc,
});
},
match: ['* ', '- '],
mode: 'block',
type: 'list',
},
{
format: (editor) =>
toggleIndentList(editor, {
listStyleType: ListStyleType.Decimal,
}),
match: [String.raw`^\d+\.$ `, String.raw`^\d+\)$ `],
matchByRegex: true,
mode: 'block',
type: 'list',
},
{
format: (editor) => {
toggleIndentList(editor, {
listStyleType: INDENT_LIST_KEYS.todo,
});
setNodes(editor, {
checked: false,
listStyleType: INDENT_LIST_KEYS.todo,
});
},
match: ['[] '],
mode: 'block',
type: 'list',
},
{
format: (editor) => {
toggleIndentList(editor, {
listStyleType: INDENT_LIST_KEYS.todo,
});
setNodes(editor, {
checked: true,
listStyleType: INDENT_LIST_KEYS.todo,
});
},
match: ['[x] '],
mode: 'block',
type: 'list',
},
];
export const autoformatRules: AutoformatRule[] = [
...autoformatBlocks,
...autoformatMarks,
...autoformatSmartQuotes,
...autoformatPunctuation,
...autoformatLegal,
...autoformatLegalHtml,
...autoformatArrow,
...autoformatMath,
...autoformatIndentLists,
];
'use client';
import { withProps } from '@udecode/cn';
import { AIPlugin } from '@udecode/plate-ai/react';
import {
BoldPlugin,
CodePlugin,
ItalicPlugin,
StrikethroughPlugin,
UnderlinePlugin,
} from '@udecode/plate-basic-marks/react';
import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
import { CalloutPlugin } from '@udecode/plate-callout/react';
import {
CodeBlockPlugin,
CodeLinePlugin,
CodeSyntaxPlugin,
} from '@udecode/plate-code-block/react';
import { CommentsPlugin } from '@udecode/plate-comments/react';
import { ParagraphPlugin, PlateLeaf } from '@udecode/plate-common/react';
import { DatePlugin } from '@udecode/plate-date/react';
import { EmojiInputPlugin } from '@udecode/plate-emoji/react';
import { HEADING_KEYS } from '@udecode/plate-heading';
import { TocPlugin } from '@udecode/plate-heading/react';
import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';
import { ColumnItemPlugin, ColumnPlugin } from '@udecode/plate-layout/react';
import { LinkPlugin } from '@udecode/plate-link/react';
import {
EquationPlugin,
InlineEquationPlugin,
} from '@udecode/plate-math/react';
import {
AudioPlugin,
FilePlugin,
ImagePlugin,
MediaEmbedPlugin,
PlaceholderPlugin,
VideoPlugin,
} 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 '@/components/potion-ui/ai-leaf';
import { BlockquoteElement } from '@/components/potion-ui/blockquote-element';
import { CalloutElement } from '@/components/potion-ui/callout-element';
import { CodeBlockElement } from '@/components/potion-ui/code-block-element';
import { CodeLeaf } from '@/components/potion-ui/code-leaf';
import { CodeLineElement } from '@/components/potion-ui/code-line-element';
import { CodeSyntaxLeaf } from '@/components/potion-ui/code-syntax-leaf';
import { ColumnElement } from '@/components/potion-ui/column-element';
import { ColumnGroupElement } from '@/components/potion-ui/column-group-element';
import { CommentLeaf } from '@/components/potion-ui/comment-leaf';
import { DateElement } from '@/components/potion-ui/date-element';
import { EmojiInputElement } from '@/components/potion-ui/emoji-input-element';
import { EquationElement } from '@/components/potion-ui/equation-element';
import { HeadingElement } from '@/components/potion-ui/heading-element';
import { HrElement } from '@/components/potion-ui/hr-element';
import { ImageElement } from '@/components/potion-ui/image-element';
import { InlineEquationElement } from '@/components/potion-ui/inline-equation-element';
import { LinkElement } from '@/components/potion-ui/link-element';
import { MediaAudioElement } from '@/components/potion-ui/media-audio-element';
import { MediaEmbedElement } from '@/components/potion-ui/media-embed-element';
import { MediaFileElement } from '@/components/potion-ui/media-file-element';
import { MediaPlaceholderElement } from '@/components/potion-ui/media-placeholder-element';
import { MediaVideoElement } from '@/components/potion-ui/media-video-element';
import { MentionElement } from '@/components/potion-ui/mention-element';
import { MentionInputElement } from '@/components/potion-ui/mention-input-element';
import { ParagraphElement } from '@/components/potion-ui/paragraph-element';
import { withPlaceholders } from '@/components/potion-ui/placeholder';
import { SlashInputElement } from '@/components/potion-ui/slash-input-element';
import {
TableCellElement,
TableCellHeaderElement,
} from '@/components/potion-ui/table-cell-element';
import { TableElement } from '@/components/potion-ui/table-element';
import { TableRowElement } from '@/components/potion-ui/table-row-element';
import { TocElement } from '@/components/potion-ui/toc-element';
import { ToggleElement } from '@/components/potion-ui/toggle-element';
import { withDraggables } from '@/components/potion-ui/with-draggables';
export const createPotionUI = () => {
return withPlaceholders(
withDraggables({
[AIPlugin.key]: AILeaf,
[AudioPlugin.key]: MediaAudioElement,
[BlockquotePlugin.key]: BlockquoteElement,
[BoldPlugin.key]: withProps(PlateLeaf, { as: 'strong' }),
[CalloutPlugin.key]: CalloutElement,
[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,
[EquationPlugin.key]: EquationElement,
[FilePlugin.key]: MediaFileElement,
[HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }),
[HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }),
[HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }),
[HorizontalRulePlugin.key]: HrElement,
[ImagePlugin.key]: ImageElement,
[InlineEquationPlugin.key]: InlineEquationElement,
[ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }),
[LinkPlugin.key]: LinkElement,
[MediaEmbedPlugin.key]: MediaEmbedElement,
[MentionInputPlugin.key]: MentionInputElement,
[MentionPlugin.key]: MentionElement,
[ParagraphPlugin.key]: ParagraphElement,
[PlaceholderPlugin.key]: MediaPlaceholderElement,
[SlashInputPlugin.key]: SlashInputElement,
[StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }),
[TableCellHeaderPlugin.key]: TableCellHeaderElement,
[TableCellPlugin.key]: TableCellElement,
[TablePlugin.key]: TableElement,
[TableRowPlugin.key]: TableRowElement,
[TocPlugin.key]: TocElement,
[TogglePlugin.key]: ToggleElement,
[UnderlinePlugin.key]: withProps(PlateLeaf, { as: 'u' }),
[VideoPlugin.key]: MediaVideoElement,
})
);
};
The commonPlugins
array includes all plugins used by the demo, while createPotionUI()
registers the components.
- Remove plugins and components you don't need.
- Install the plugin packages. See the full list in Plate Docs.
npm install @udecode/plate-alignment @udecode/plate-autoformat @udecode/plate-basic-marks @udecode/plate-block-quote @udecode/plate-break @udecode/plate-callout @udecode/plate-caption @udecode/plate-code-block @udecode/plate-comments @udecode/plate-common @udecode/plate-date @udecode/plate-dnd @udecode/plate-docx @udecode/plate-emoji @udecode/plate-font @udecode/plate-heading @udecode/plate-highlight @udecode/plate-horizontal-rule @udecode/plate-indent @udecode/plate-indent-list @udecode/plate-juice @udecode/plate-kbd @udecode/plate-layout @udecode/plate-link @udecode/plate-markdown @udecode/plate-math @udecode/plate-media @udecode/plate-mention @udecode/plate-node-id @udecode/plate-reset-node @udecode/plate-select @udecode/plate-selection @udecode/plate-slash-command @udecode/plate-table @udecode/plate-toggle @udecode/plate-trailing-block prismjs
- Copy/paste the components you need into
@/components/potion-ui
. See the full list in the sidebar.
(Optional) Editor Container
For the full demo experience, add EditorContainer
, FloatingToolbar
and CursorOverlay
:
import { Editor, EditorContainer } from '@/components/potion-ui/editor';
import { FloatingToolbar } from '@/components/potion-ui/floating-toolbar';
import { FloatingToolbarButtons } from '@/components/potion-ui/floating-toolbar-buttons';
export default function Demo() {
const containerRef = React.useRef<HTMLDivElement>(null);
// ... (editor setup)
return (
<Plate editor={editor}>
<EditorContainer ref={containerRef}>
<Editor />
<FloatingToolbar>
<FloatingToolbarButtons />
</FloatingToolbar>
<CursorOverlay containerRef={containerRef} />
</EditorContainer>
</Plate>
);
}
(Optional) Drag & Drop
If you need drag-and-drop functionality in your editor, add this wrapper:
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
export default function Demo() {
// ... (editor setup)
return (
<DndProvider backend={HTML5Backend}>
<Plate editor={editor}>{/* ... */}</Plate>
</DndProvider>
);
}
Full example
import React from 'react';
import { Plate, usePlateEditor } from '@udecode/plate-common/react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { createPotionUI } from '@/components/editor/create-potion-ui';
import { commonPlugins } from '@/components/editor/plugins';
import { CursorOverlay } from '@/components/potion-ui/selection-overlay';
import { Editor, EditorContainer } from '@/components/potion-ui/editor';
import { FloatingToolbar } from '@/components/potion-ui/floating-toolbar';
import { FloatingToolbarButtons } from '@/components/potion-ui/floating-toolbar-buttons';
export default function Demo() {
const containerRef = React.useRef<HTMLDivElement>(null);
const editor = usePlateEditor({
override: {
components: createPotionUI(),
},
plugins: [
...commonPlugins,
// Add additional plugins here
],
value: 'Hello world!',
});
return (
<DndProvider backend={HTML5Backend}>
<Plate editor={editor}>
<EditorContainer ref={containerRef} variant="demo">
<Editor variant="demo" />
<FloatingToolbar>
<FloatingToolbarButtons />
</FloatingToolbar>
<CursorOverlay containerRef={containerRef} />
</EditorContainer>
</Plate>
</DndProvider>
);
}
Next steps
Congratulations! You've now set up our demo editor. From here, you can start customizing and extending your editor:
-
Add Plate plugins and Plate Plus components to your project. Each component will have its own installation instructions, typically including:
- Dependencies to install
- Files to copy into your project
- Import path updates
-
Customize the editor's appearance and behavior by modifying the
plugins
andcreatePotionUI()
. -
Implement your own custom plugins and components to tailor the editor to your specific needs.
Remember, Plate is highly customizable, so don't hesitate to experiment and adapt the editor to fit your project's unique requirements.