Visual page builder for Payload CMS with thumbnail block picker
Features • Installation • Usage • Blocks • Custom Blocks • Configuration
- Visual Block Picker — Browse and select blocks with thumbnail previews
- Category-Based Organization — Blocks organized by type: Heroes, Features, CTAs, Testimonials, Pricing, and more
- Pre-built Modern UI Blocks — Production-ready components styled with shadcn/ui design patterns
- Error Isolation — Graceful fallbacks for all operations
- Tailwind CSS v4 — Modern styling with CSS variables for easy theming
- WCAG 2.1 AA Accessible — Semantic HTML, ARIA labels, and keyboard navigation
Run our automated installation script to install the package, configure Payload, set up Tailwind CSS, and automatically integrate the frontend renderer.
The script will:
- Install the package
- Add the plugin to
payload.config.ts - Add the plugin paths to
tailwind.config.js - Auto-Integrate:
- Updates
src/components/RenderBlocks.tsx(or similar) to use theVisualBlocksRendererwith Hybrid Rendering (Server Components for your blocks, Client Components for visual blocks). - Injects the necessary import into your
page.tsx.
- Updates
Mac/Linux:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Exudius/payload-plugin-visual-blocks/main/bin/install.sh)"Windows (PowerShell):
irm https://raw.githubusercontent.com/Exudius/payload-plugin-visual-blocks/main/bin/install.ps1 | iex# npm
npm install payload-plugin-visual-blocksThe plugin uses Tailwind CSS. You need to add the plugin's paths to your tailwind.config.js so the styles are generated correctly.
// tailwind.config.js
module.exports = {
content: [
// ... your existing content paths
'./node_modules/payload-plugin-visual-blocks/dist/**/*.{js,jsx,ts,tsx}', // Add this line
],
// ...
}Import and add the plugin to your payload.config.ts.
// payload.config.ts
import { buildConfig } from 'payload'
import { visualBlocks } from 'payload-plugin-visual-blocks'
export default buildConfig({
// ...
plugins: [
visualBlocks({
// Apply to specific collections (optional, defaults to all with blocks fields)
collections: ['pages'],
}),
],
})To render the blocks on your frontend (Next.js App Router), you need to use the VisualBlocksRenderer component.
If you used the Zero-Config Install script, your src/components/RenderBlocks.tsx should already be updated to use the plugin with Hybrid Rendering. The script creates a backup (.bak) before making changes.
It also attempts to inject the component into your page.tsx. If that step was skipped (for safety), you just need to add <RenderBlocks blocks={layout} /> to your page JSX.
If the automated script couldn't update your file, or if you prefer to do it manually, follow these steps:
-
Open your
RenderBlocks.tsx(usually insrc/components/). -
Import the renderer:
import { VisualBlocksRenderer } from 'payload-plugin-visual-blocks/client'
-
Update the component to use Hybrid Rendering (Server Components for your blocks, Client Components for visual blocks):
export const RenderBlocks: React.FC<any> = (props) => { const { blocks } = props if (!blocks || !Array.isArray(blocks) || blocks.length === 0) { return null } return ( <> {blocks.map((block, index) => { const { blockType } = block // 1. Render your existing Server Components directly if (blockType && blockType in blockComponents) { const Block = blockComponents[blockType] return <Block key={index} {...block} /> } // 2. Delegate to VisualBlocksRenderer for plugin blocks (Client Component) return ( <VisualBlocksRenderer key={index} blocks={[block]} /> ) })} </> ) }
-
Render Blocks in Page:
Finally, import and use your updated
RenderBlockscomponent in yourpage.tsx.// src/app/(frontend)/[slug]/page.tsx import { RenderBlocks } from '@/components/RenderBlocks' export default async function Page({ params }) { // ... fetch your page data return ( <main> {/* Pass the layout/blocks field to the renderer */} <RenderBlocks blocks={page.layout} /> </main> ) }
The VisualBlocksRenderer accepts additional props for customization:
<VisualBlocksRenderer
blocks={pageData.layout}
// Add your own custom blocks
customBlocks={{
'my-custom-block': MyCustomBlockComponent,
}}
// Custom class for the wrapper
className="my-page-content"
// Custom class for each block wrapper
blockClassName="my-12"
/>The plugin ships with production-ready blocks organized into categories:
| Slug | Description |
|---|---|
hero-01 |
Centered layout with badge, headline, dual CTAs, and stats row |
hero-02 |
50/50 split layout with text on left, image on right |
hero-03 |
Full-width background image with gradient overlay |
| Slug | Description |
|---|---|
features-01 |
3-column grid with icons and descriptions |
features-02 |
Modern bento grid layout with varied card sizes |
features-03 |
Alternating left/right image-text sections |
| Slug | Description |
|---|---|
cta-01 |
Compact gradient banner with inline form |
cta-02 |
Card-style CTA with supporting image |
| Slug | Description |
|---|---|
testimonials-01 |
Single featured quote with star rating and avatar |
testimonials-02 |
Multiple testimonials with carousel navigation |
| Slug | Description |
|---|---|
pricing-01 |
3-tier pricing table with monthly/yearly toggle |
You can easily create your own blocks that integrate with the visual picker using the createBlock helper.
// src/blocks/MyCustomBlock.tsx
import React from 'react'
import { createBlock } from 'payload-plugin-visual-blocks'
interface MyBlockProps {
title: string
content: string
}
export const MyCustomBlock = createBlock<MyBlockProps>({
config: {
slug: 'my-custom-block',
interfaceName: 'MyCustomBlock',
labels: { singular: 'My Block', plural: 'My Blocks' },
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'content', type: 'textarea' },
],
},
Component: ({ title, content }) => (
<div className="p-8 bg-white rounded-lg shadow">
<h2 className="text-2xl font-bold">{title}</h2>
<p className="mt-2 text-gray-600">{content}</p>
</div>
),
})// payload.config.ts
import { visualBlocks } from 'payload-plugin-visual-blocks'
import { MyCustomBlock } from './blocks/MyCustomBlock'
export default buildConfig({
plugins: [
visualBlocks({
additionalBlocks: [MyCustomBlock.config],
}),
],
})Add it to your VisualBlocksRenderer via the customBlocks prop.
<VisualBlocksRenderer
blocks={blocks}
customBlocks={{
'my-custom-block': MyCustomBlock.Component
}}
/>interface VisualBlocksOptions {
/**
* Collections to apply the visual block picker to.
* @default [] (Applies to all collections with blocks fields)
*/
collections?: string[]
/**
* Include built-in shadcn blocks automatically.
* @default true
*/
includeBuiltInBlocks?: boolean
/**
* Additional custom blocks to inject.
* @default []
*/
additionalBlocks?: Block[]
}We welcome contributions! Please see our Contributing Guide for details on how to:
- Set up your development environment
- Add new blocks
- Run tests
- Publish updates (for maintainers)
MIT © Innoveapp.com