Docs

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. Configure it in your tsconfig.json:

{
  "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.js

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-4 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. Here's a simple example:

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:

  1. First, create a file for your fonts (e.g., lib/utils/fonts.ts):
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',
});
  1. 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/utils/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:

  1. Install next-themes:
npm install next-themes
  1. Create a theme provider (in components/theme-provider.tsx):
'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>;
}
  1. Use the ThemeProvider in your root layout (e.g., app/layout.tsx):
import { cn } from '@udecode/cn';
import { ThemeProvider } from '@/components/theme-provider';
import { fontSans } from '@/lib/utils/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/demo/plugins';
 
export default function EditorDemo() {
  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 {
  isCodeBlockEmpty,
  isSelectionAtCodeBlockStart,
  unwrapCodeBlock,
} from '@udecode/plate-code-block';
import { CodeBlockPlugin } from '@udecode/plate-code-block/react';
import { CommentsPlugin } from '@udecode/plate-comments/react';
import {
  isBlockAboveEmpty,
  isSelectionAtBlockStart,
} from '@udecode/plate-common';
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 { TodoListPlugin } from '@udecode/plate-list/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 { ResetNodePlugin } from '@udecode/plate-reset-node/react';
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/demo/autoformatOptions';
import { aiPlugins } from '@/registry/default/example/ai-plugins';
import { BlockContextMenu } from '@/registry/default/potion-ui/block-context-menu';
import { CommentsPopover } from '@/registry/default/potion-ui/comments-popover';
import {
  DragOverCursorPlugin,
  SelectionOverlayPlugin,
} from '@/registry/default/potion-ui/cursor-overlay';
import { ImageElement } from '@/registry/default/potion-ui/image-element';
import { ImagePreview } from '@/registry/default/potion-ui/image-preview';
import {
  TodoLi,
  TodoMarker,
} from '@/registry/default/potion-ui/indent-todo-marker';
import { LinkFloatingToolbar } from '@/registry/default/potion-ui/link-floating-toolbar';
 
export const commonPlugins = [
  // Nodes
  HeadingPlugin.configure({ options: { levels: 3 } }),
  BlockquotePlugin,
  CodeBlockPlugin.configure({ options: { prism: Prism } }),
  HorizontalRulePlugin,
  LinkPlugin.extend({
    render: { afterEditable: () => <LinkFloatingToolbar /> },
  }),
  ImagePlugin.extend({
    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 } }),
  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.configure({
    options: {
      rules: [
        {
          defaultType: ParagraphPlugin.key,
          hotkey: 'Enter',
          predicate: isBlockAboveEmpty,
          types: [BlockquotePlugin.key, TodoListPlugin.key],
        },
        {
          defaultType: ParagraphPlugin.key,
          hotkey: 'Backspace',
          predicate: isSelectionAtBlockStart,
          types: [BlockquotePlugin.key, TodoListPlugin.key],
        },
        {
          defaultType: ParagraphPlugin.key,
          hotkey: 'Enter',
          predicate: isCodeBlockEmpty,
          types: [CodeBlockPlugin.key],
          onReset: unwrapCodeBlock,
        },
        {
          defaultType: ParagraphPlugin.key,
          hotkey: 'Backspace',
          predicate: isSelectionAtCodeBlockStart,
          types: [CodeBlockPlugin.key],
          onReset: unwrapCodeBlock,
        },
      ],
    },
  }),
  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,
            ],
          },
        },
      ],
    },
  }),
  TrailingBlockPlugin.configure({
    options: { type: ParagraphPlugin.key },
  }),
  DragOverCursorPlugin,
  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,
];
'use client';
 
import { withProps } from '@udecode/cn';
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 { AIPlugin } from '@/registry/default/potion-ui/ai/src/react';
import { AILeaf } from '@/registry/default/potion-ui/ai-leaf';
import { BlockquoteElement } from '@/registry/default/potion-ui/blockquote-element';
import { CalloutElement } from '@/registry/default/potion-ui/callout-element';
import { CodeBlockElement } from '@/registry/default/potion-ui/code-block-element';
import { CodeLeaf } from '@/registry/default/potion-ui/code-leaf';
import { CodeLineElement } from '@/registry/default/potion-ui/code-line-element';
import { CodeSyntaxLeaf } from '@/registry/default/potion-ui/code-syntax-leaf';
import { ColumnElement } from '@/registry/default/potion-ui/column-element';
import { ColumnGroupElement } from '@/registry/default/potion-ui/column-group-element';
import { CommentLeaf } from '@/registry/default/potion-ui/comment-leaf';
import { DateElement } from '@/registry/default/potion-ui/date-element';
import { EmojiInputElement } from '@/registry/default/potion-ui/emoji-input-element';
import { EquationElement } from '@/registry/default/potion-ui/equation-element';
import { HeadingElement } from '@/registry/default/potion-ui/heading-element';
import { HrElement } from '@/registry/default/potion-ui/hr-element';
import { ImageElement } from '@/registry/default/potion-ui/image-element';
import { InlineEquationElement } from '@/registry/default/potion-ui/inline-equation-element';
import { LinkElement } from '@/registry/default/potion-ui/link-element';
import { MediaAudioElement } from '@/registry/default/potion-ui/media-audio-element';
import { MediaEmbedElement } from '@/registry/default/potion-ui/media-embed-element';
import { MediaFileElement } from '@/registry/default/potion-ui/media-file-element';
import { MediaPlaceholderElement } from '@/registry/default/potion-ui/media-placeholder-element';
import { MediaVideoElement } from '@/registry/default/potion-ui/media-video-element';
import { MentionElement } from '@/registry/default/potion-ui/mention-element';
import { MentionInputElement } from '@/registry/default/potion-ui/mention-input-element';
import { ParagraphElement } from '@/registry/default/potion-ui/paragraph-element';
import { withPlaceholders } from '@/registry/default/potion-ui/placeholder';
import { SlashInputElement } from '@/registry/default/potion-ui/slash-input-element';
import {
  TableCellElement,
  TableCellHeaderElement,
} from '@/registry/default/potion-ui/table-cell-element';
import { TableElement } from '@/registry/default/potion-ui/table-element';
import { TableRowElement } from '@/registry/default/potion-ui/table-row-element';
import { TocElement } from '@/registry/default/potion-ui/toc-element';
import { ToggleElement } from '@/registry/default/potion-ui/toggle-element';
import { withDraggables } from '@/registry/default/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 essential plugins for basic editor functionality, while createPotionUI() sets up the default UI components. Remove unused plugins and components.

(Optional) Editor Container

For the full demo experience, you can 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 EditorDemo() {
  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 EditorDemo() {
  // ... (editor setup)
 
  return (
    <DndProvider backend={HTML5Backend}>
      <Plate editor={editor}>{/* ... */}</Plate>
    </DndProvider>
  );
}

Next steps

Congratulations! You've now set up our demo editor. From here, you can start customizing and extending your editor:

  1. 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
  2. Customize the editor's appearance and behavior by modifying the plugins and createPotionUI().

  3. 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.