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
- Get GitHub access with Plate Plus
- Copy the registry directory into your project.
- Update the import in
src/app/editor/page.tsx
:
import { PlateEditor } from '@/registry/default/components/editor/plate-editor';
- 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
- Run the development server:
pnpm dev
- 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.