Docs
Installation

Installation

Get started with Plate Plus components in your project.

This guide will help you set up the frontend editor of Potion in your project:

Core Setup

Installation

Follow the Plate installation instructions to get started.

Add the basic editor:

npx shadcx@latest add plate/editor-basic

This will add:

  • tailwind.config.ts: The base tailwind config.
  • src/app/globals.css: The base styles.
  • src/app/editor/page.tsx: The page with the editor.

Configure tailwind.config

Add the following to your tailwind.config file:

// ...
 
import plugin from 'tailwindcss/plugin';
 
const config: Config = {
  // ...
 
  plugins: [
    // ...
    plugin(function ({ addUtilities }) {
      addUtilities({
        '.transition-bg-ease': {
          'transition-duration': '20ms',
          'transition-property': 'background-color',
          'transition-timing-function': 'ease-in',
        },
      });
    }),
  ],
 
  theme: {
    extend: {
      container: {
        screens: {
          '2xl': '1400px',
        },
      },
      
      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',
      },
      
      borderRadius: {
        // ... existing entries ...
        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: {
        // ... existing colors ...
        brand: {
          DEFAULT: 'hsl(var(--brand))',
          active: 'hsl(var(--brand-active))',
          foreground: 'hsl(var(--brand-foreground))',
          hover: 'hsl(var(--brand-hover))',
        },
        'subtle-foreground': 'hsl(var(--subtle-foreground))',
      },
 
      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)' },
        },
      },
    },
  },
};

Full example:

import type { Config } from 'tailwindcss';
 
import plugin from 'tailwindcss/plugin';
 
const config: Config = {
  content: [
    './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
    './src/components/**/*.{js,ts,jsx,tsx,mdx}',
    './src/app/**/*.{js,ts,jsx,tsx,mdx}',
    './src/registry/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  darkMode: ['class'],
  plugins: [
    require('tailwindcss-animate'),
    require('tailwind-scrollbar-hide'),
    plugin(function ({ addUtilities }) {
      addUtilities({
        '.transition-bg-ease': {
          'transition-duration': '20ms',
          'transition-property': 'background-color',
          'transition-timing-function': 'ease-in',
        },
      });
    }),
  ],
  theme: {
    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',
      },
      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: 'hsl(var(--ring))',
        secondary: {
          DEFAULT: 'hsl(var(--secondary))',
          foreground: 'hsl(var(--secondary-foreground))',
        },
        'subtle-foreground': 'hsl(var(--subtle-foreground))',
      },
      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)' },
        },
      },
      screens: {
        'main-hover': {
          raw: '(hover: hover)',
        },
      },
    },
  },
};
export default config;

Configure styles

Add the following to your globals.css file:

/* ... */
 
@layer base {
  /* ... */
  
  :root {
    --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%;
    --brand-foreground: 0 0% 100%;
    --brand-hover: 211 77% 46%;
    --brand-active: 211 77% 41%;
    --highlight: 48 100% 50%;
    --highlight-foreground: 10 105 218;
    --subtle-foreground: 240 5% 34%;
    
    :focus-visible {
      @apply outline-none;
    }
  }
}
 
@layer utilities {
  .focus-ring:focus-visible {
    @apply ring-2 ring-ring ring-offset-2;
  }
 
  .no-focus-ring:focus-visible {
    @apply !ring-0 !ring-offset-0;
  }
}

Full example:

@tailwind base;
@tailwind components;
@tailwind utilities;
 
@layer utilities {
  .text-balance {
    text-wrap: balance;
  }
}
 
@layer base {
  :root {
    --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%;
    --brand-foreground: 0 0% 100%;
    --brand-hover: 211 77% 46%;
    --brand-active: 211 77% 41%;
    --highlight: 48 100% 50%;
    --highlight-foreground: 10 105 218;
    --subtle-foreground: 240 5% 34%;
    
    :focus-visible {
      @apply outline-none;
    }
  }
}
 
@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
  }
}
 
@layer utilities {
  .focus-ring:focus-visible {
    @apply ring-2 ring-ring ring-offset-2;
  }
 
  .no-focus-ring:focus-visible {
    @apply !ring-0 !ring-offset-0;
  }
}

Editor

  1. Get GitHub access with Plate Plus
  2. Copy the registry directory into your project.
  3. Update the import in src/app/editor/page.tsx:
import { PlateEditor } from '@/registry/default/components/editor/plate-editor';
  1. Install the following dependencies:
npm install @udecode/plate-ai @udecode/plate-basic-marks @udecode/plate-block-quote @udecode/plate-callout @udecode/plate-code-block @udecode/plate-comments @udecode/plate-date @udecode/plate-emoji @udecode/plate-heading @udecode/plate-horizontal-rule @udecode/plate-layout @udecode/plate-link @udecode/plate-math @udecode/plate-media @udecode/plate-mention @udecode/plate-slash-command @udecode/plate-table @udecode/plate-toggle @udecode/plate-test-utils @udecode/plate-markdown @faker-js/faker react-markdown @radix-ui/react-hover-card @udecode/plate-docx @udecode/plate-font @udecode/plate-juice @udecode/plate-kbd @udecode/plate-trailing-block prismjs @udecode/plate-selection @udecode/plate-dnd @udecode/plate-indent @udecode/plate-indent-list @radix-ui/react-checkbox @udecode/plate-floating @radix-ui/react-slot @radix-ui/react-tooltip @radix-ui/react-popover @radix-ui/react-separator ai @radix-ui/react-dialog jotai-x vaul cmdk @ariakit/[email protected] @udecode/plate-caption @radix-ui/react-toolbar @radix-ui/react-dropdown-menu @udecode/plate-autoformat date-fns @radix-ui/react-avatar @udecode/plate-select @udecode/plate-node-id @udecode/plate-break @udecode/plate-reset-node @udecode/plate-resizable [email protected] react-dnd react-dnd-html5-backend @udecode/plate-combobox react-lazy-load-image-component react-tweet use-file-picker @uploadthing/[email protected] [email protected] zod sonner @radix-ui/react-tabs react-lite-youtube-embed react-player
  1. Run the development server:
pnpm dev
  1. See the editor in action at localhost:3000/editor

That's it!

You've now set up the Potion frontend editor without any backend features (database, authentication, etc). For a full-stack reference implementation including these features, check out the Potion template.