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
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):
typescript
// From @payload-typesimporttype { 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.
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
typescript
import { HeroTemplate1 } from'@/components/blocks/hero';
exportfunctionBlockRenderer({ block }: BlockRendererProps) {
// Check if it's a hero blockif (block.blockType === 'hero') {
if (block.template === 'template-1') {
return<HeroTemplate1data={block} />;
}
if (block.template === 'template-2') {
return<HeroTemplate2data={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:
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 background
template-2: Split layout (text left, image right)
template-3: Text-only minimal design
All three use the same HeroBlock type, but they render differently:
if (block.blockType === 'hero') {
if (block.template === 'template-1') return<HeroTemplate1data={block} />;
if (block.template === 'template-2') return<HeroTemplate2data={block} />;
if (block.template === 'template-3') return<HeroTemplate3data={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:
typescript
// 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:<divclassName="bg-blue-600 rounded-xl shadow-lg p-6"style={{borderRadius: '12px',
boxShadow: '04px12pxrgba(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: