Complete Guide: Use Existing Payload CMS Block Types
Use @payload-types to build Hero, Services and CTA blocks with shadcn/ui + Tailwind for type-safe components

📚 Comprehensive Payload CMS Guides
Detailed Payload guides with field configuration examples, custom components, and workflow optimization tips to speed up your CMS development process.
Part 2 of the Design to Code series — Following Design-Driven Development
You have a Payload CMS setup with block types already defined: Hero, Services, FeaturedProjects, CTA, and more. When you encounter one of these existing types, don't create a custom type. Use the one Payload already provides and focus on building the component.
This guide shows you the path for existing Payload blocks.
When to Use This Guide
Use this guide when:
- Payload CMS already has this block type defined
- You're building a component for Hero, Services, FeaturedProjects, CTA, or other existing types
- You want to use
@payload-typesdirectly
Skip this guide if you're creating a new block type (use Creating Custom Block Types instead).
The Process: Three Steps
Building a component from an existing Payload type is straightforward. Here's the complete flow:
- Import the type from Payload
- Build the component
- Add dummy data to your page
That's it. No type definition needed.
Step 1: Import from @payload-types
First, check what Payload has defined. Open @payload-types (the auto-generated types from your Payload schema):
// From @payload-types
import type { HeroBlock } from '@payload-types';
This is the source of truth. Payload has already defined all the fields this block type should have. Your job is just to implement a component that uses them.
Step 2: Build the Component
Create your component file and implement the block using the Payload type.
File: src/components/blocks/hero/hero-template-1.tsx
'use client';
import type { HeroBlock } from '@payload-types';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
interface HeroTemplate1Props {
data: HeroBlock;
}
export function HeroTemplate1({ data }: HeroTemplate1Props) {
const { title, subtitle, backgroundImage, cta } = data;
return (
<div
className="relative h-screen flex items-center justify-center"
style={{
backgroundImage: `url(${backgroundImage?.url})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
>
{/* Dark overlay */}
<div className="absolute inset-0 bg-black/50" />
{/* Content */}
<div className="relative z-10 text-center text-white max-w-2xl">
<h1 className="text-6xl font-bold mb-4">{title}</h1>
{subtitle && <p className="text-xl mb-8">{subtitle}</p>}
{cta && (
<Button asChild className="bg-white text-black hover:bg-gray-100">
<a href={cta.href}>{cta.label}</a>
</Button>
)}
</div>
</div>
);
}
What's happening here:
The component receives data typed as HeroBlock (from Payload). It destructures the fields it needs and renders them. The Payload type tells you exactly what fields are available, so TypeScript will error if you reference a field that doesn't exist. No guessing, no surprises.
Notice we use shadcn/ui components (Button, Card) and Tailwind for styling. We customize the appearance to match Figma through className overrides, not by building custom components.
Step 3: Add to BlockRenderer
Update your block renderer to handle this block type:
File: src/components/block-renderer.tsx
import { HeroTemplate1 } from '@/components/blocks/hero';
export function BlockRenderer({ block }: BlockRendererProps) {
// Check if it's a hero block
if (block.blockType === 'hero') {
if (block.template === 'template-1') {
return <HeroTemplate1 data={block} />;
}
if (block.template === 'template-2') {
return <HeroTemplate2 data={block} />;
}
}
// ... other block types ...
}
The BlockRenderer acts as a router. It checks the blockType and template fields from Payload and renders the appropriate component. If you need multiple templates for the same block type (template-1, template-2, etc.), add more conditions.
Step 4: Add Dummy Data to Page
Before Payload feeds you data, you need example data for development. Create it directly in your page data file:
File: src/app/data.ts
import type { Page } from '@payload-types';
export const homePageData: Page = {
id: 'home',
slug: '/',
title: 'Home',
layout: [
{
blockType: 'hero',
template: 'template-1',
title: 'Design. Build. Illuminate.',
subtitle: 'Custom signage solutions that define how brands are seen.',
backgroundImage: {
url: 'https://example.com/hero-bg.jpg',
alt: 'Hero background',
},
cta: {
label: 'Explore Our Work',
href: '/portfolio',
},
} as HeroBlock,
],
};
Notice the as HeroBlock at the end. This tells TypeScript to treat this object as a HeroBlock. TypeScript will error if you're missing required fields or using wrong field names. This is your safety net before Payload is connected.
The data structure matches exactly what Payload would provide. When you switch to Payload later, you just replace this dummy data with real data from the CMS—no component changes needed.
Using Multiple Templates
Often, a block type has multiple template variations. For example, Hero might have:
template-1: Full-screen image backgroundtemplate-2: Split layout (text left, image right)template-3: Text-only minimal design
All three use the same HeroBlock type, but they render differently:
// Same type, different templates
{
blockType: 'hero',
template: 'template-1',
title: '...',
// ... fields ...
} as HeroBlock
{
blockType: 'hero',
template: 'template-2',
title: '...',
// ... same fields ...
} as HeroBlock
Create a component for each template:
export function HeroTemplate1({ data }: { data: HeroBlock }) { /* ... */ }
export function HeroTemplate2({ data }: { data: HeroBlock }) { /* ... */ }
export function HeroTemplate3({ data }: { data: HeroBlock }) { /* ... */ }
Then route to them in BlockRenderer:
if (block.blockType === 'hero') {
if (block.template === 'template-1') return <HeroTemplate1 data={block} />;
if (block.template === 'template-2') return <HeroTemplate2 data={block} />;
if (block.template === 'template-3') return <HeroTemplate3 data={block} />;
}
All templates share the same type, so they all have the same fields available. Templates just choose which fields to display and how.
Common Payload Block Types in This Project
Here are the existing Payload block types you'll encounter:
| Block Type | Use Case | Template | Example |
|---|---|---|---|
hero | Page hero section | template-1, template-2 | Full-screen banner with CTA |
services | Service cards | default | Grid of services |
featured-projects | Project showcase | carousel, grid | Projects with images |
cta | Call-to-action section | default | "Get started" section |
Check your Payload schema to see what's defined.
Customizing to Match Figma
Your component needs to match the Figma design exactly. Use shadcn/ui components as the base and customize with Tailwind:
// If Figma shows:
// - Blue background (#0066ff)
// - Rounded corners (12px)
// - Shadow effect
<Card className="bg-blue-600 rounded-xl shadow-lg p-6">
{/* content */}
</Card>
// Or with inline styles for custom values:
<div
className="bg-blue-600 rounded-xl shadow-lg p-6"
style={{
borderRadius: '12px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
}}
>
{/* content */}
</div>
Never create custom components for standard UI elements. Use shadcn/ui, customize with className and style overrides. This keeps your codebase consistent and reduces maintenance burden.
Key Differences: Existing vs Custom Types
To help you decide which path to take:
| Aspect | Existing Payload Type | Custom Type |
|---|---|---|
| Type Definition | Use @payload-types | Create in src/types/blocks/ |
| When to Use | Block already defined in Payload | New block type you're creating |
| Type File Needed | No | Yes |
| Effort | ~30 minutes (just component) | ~1-2 hours (type + component + examples) |
| File Count | Component + BlockRenderer update | Type + Component + Examples + BlockRenderer |
Testing Your Implementation
Before moving on, make sure:
- Component imports from
@payload-types - BlockRenderer has the correct conditions
- Dummy data in page file matches the type exactly
- Component renders on your page
- Styling matches Figma design
- All interactive elements (buttons, links) work
Quick test: add the block to src/app/data.ts and visit your page. You should see it rendered immediately.
Moving to Payload CMS Later
When you connect to real Payload CMS data, your code doesn't change:
// Before: example data
import homePageData from '@/app/data';
// After: Payload data
async function HomePage() {
const homePageData = await getPage('home');
// ... rest of code unchanged
}
Your component, BlockRenderer, and types all work exactly the same. You're just swapping the data source.
Design-Driven Development: Build Types from Figma Quickly
Turn Figma mocks into TypeScript types and components, reduce refactors, and simplify Payload CMS integration.
Create Custom Block Types in Payload CMS — 5-Step Guide
Step-by-step TypeScript guide to define block types, build shadcn/ui components, map Lucide icons, and integrate with…