Building a block that doesn't exist in Payload CMS? This guide walks through the complete process: defining your custom type, building the component, creating example data, and integrating everything.
This is the full five-step process for creating a block from scratch.
When to Use This Guide
Use this guide when:
Payload CMS doesn't have this block type defined
You're building a completely new block (FeaturedIndustries, Testimonials, etc.)
You want to define the data structure yourself based on Figma design
importtype { FeaturedIndustriesBlock, Industry } from'@/types/blocks';
// Individual industry examplesconstretailIndustry: Industry = {
id: 1,
title: 'Retail',
slug: 'retail',
description: 'Custom signage for retail stores and shopping centers',
icon: 'ShoppingBag',
cta: {
label: 'Explore Retail Solutions',
href: '/industries/retail',
},
};
consthealthcareIndustry: Industry = {
id: 2,
title: 'Healthcare',
slug: 'healthcare',
description: 'Professional signage for hospitals and medical facilities',
icon: 'Heart',
cta: {
label: 'Explore Healthcare Solutions',
href: '/industries/healthcare',
},
};
consthospitality: Industry = {
id: 3,
title: 'Hospitality',
slug: 'hospitality',
description: 'Wayfinding and branding signage for hotels and restaurants',
icon: 'Star',
cta: {
label: 'Explore Hospitality Solutions',
href: '/industries/hospitality',
},
};
// Complete block exampleexportconstfeaturedIndustriesExample: FeaturedIndustriesBlock = {
blockType: 'featuredIndustries',
template: 'default',
tagline: 'Industries We Serve',
title: 'Expertise Across Every Market',
description: 'We work with industries of all sizes. Here are some of our specialties.',
selectedIndustries: [retailIndustry, healthcareIndustry, hospitality],
bgColor: 'light',
itemsPerView: 3,
};
// Array of industries for later useexportconstindustriesData: Industry[] = [
retailIndustry,
healthcareIndustry,
hospitality,
];
This example data:
Shows exactly how to structure the type
Provides sample data for testing
Documents the expected format
Can be copied and modified for other uses
Step 4: Update BlockRenderer
Add a case for your new block type:
File:src/components/block-renderer.tsx
typescript
import { FeaturedIndustriesTemplate1 } from'@/components/blocks/featured-industries';
exportfunctionBlockRenderer({ block }: BlockRendererProps) {
// ... existing blocks ...// NEW: Handle featured-industries blockif (block.blockType === 'featuredIndustries') {
if (block.template === 'default') {
return<FeaturedIndustriesTemplate1data={blockasany} />;
}
// Can add more templates here later// if (block.template === 'carousel') {// return <FeaturedIndustriesCarousel data={block as any} />;// }
}
// Fallback for unknown blocksconsole.warn(`Unknown block: ${block.blockType}/${block.template}`);
returnnull;
}
The BlockRenderer is a router. It checks the blockType and routes to the appropriate component. When you add more template variations later (carousel, list view, etc.), you add more conditions here.
Step 5: Add to Page Data and Test
Add your block to a page and see it render:
File:src/app/data.ts
typescript
importtype { Page } from'@payload-types';
import { featuredIndustriesExample } from'@/components/blocks/featured-industries/featured-industries.example';
exportconsthomePageData: Page = {
id: 'home',
slug: '/',
title: 'Home',
layout: [
{
blockType: 'hero',
// ... hero data ...
},
{
// Add your featured industries block
...featuredIndustriesExample,
// Can override specific fields:title: 'Our Specialties',
} asFeaturedIndustriesBlock,
],
};
Visit your page. You should see the featured industries section rendering with all three example industries.
Creating Multiple Templates
Need carousel and grid versions of the same block? Create separate components:
exportfunctionFeaturedIndustriesCarousel({ data }: FeaturedIndustriesTemplate1Props) {
// Carousel implementation using shadcn/ui Carousel// Same type, different layout
}
Then add to BlockRenderer:
typescript
if (block.blockType === 'featuredIndustries') {
if (block.template === 'default') {
return<FeaturedIndustriesTemplate1data={blockasany} />;
}
if (block.template === 'carousel') {
return<FeaturedIndustriesCarouseldata={blockasany} />;
}
}
Both templates use the same type, so they share all the same fields. Templates just choose different ways to render them.
Customization Checklist
Before considering your block complete:
Component matches Figma design exactly
Colors use Tailwind classes or match Figma specs
Spacing (padding, margins, gaps) matches design
Icons use Lucide React (never SVG imports)
Buttons use shadcn/ui Button component
Container widths and responsive breakpoints match design
Hover states and transitions feel smooth
Empty states are handled gracefully
TypeScript has no errors
Component renders on page without console errors
Key Files Created
When you're done, you should have:
code
src/types/blocks/
└─ featured-industries.ts ← Your custom type
src/components/blocks/featured-industries/
├─ featured-industries-template-1.tsx
├─ featured-industries-carousel.tsx (optional)
└─ featured-industries.example.ts
src/components/
└─ block-renderer.tsx ← Updated with your block case
Moving to Payload Later
When you're ready to connect Payload CMS, your custom type becomes the basis for the Payload collection config. Your code doesn't change—just the data source: