Skip to content

Blocks System

Blocks are reusable content modules that editors can add to pages in any order. This guide walks through creating a new block from scratch.

In a typical setup, content structure and rendering are tangled in the same file. In this boilerplate they are kept separate — each block is split across two files:

FileRole
src/blocks/[name]/config.tsDefines the fields Payload shows in the admin panel
src/blocks/[name]/[name]-block.tsxThe React component that renders those fields on the frontend

The config tells Payload what data to store. The component decides how to display it. They connect through RenderBlocks — a switch that maps each block’s blockType slug to its component.

  1. Create the config file

    src/blocks/quote/config.ts
    import type { Block } from 'payload';
    export const quote: Block = {
    slug: 'quote',
    interfaceName: 'QuoteBlock',
    fields: [
    {
    name: 'content',
    type: 'textarea',
    required: true,
    },
    {
    name: 'author',
    type: 'text',
    },
    ],
    };

    slug is the block’s identifier — it becomes the blockType value stored in the database and used in the frontend switch. interfaceName gives the generated TypeScript type a readable name.

  2. Create the component

    src/blocks/quote/quote-block.tsx
    interface Props {
    content: string;
    author?: string;
    }
    export default function QuoteBlock({ content, author }: Props) {
    return (
    <blockquote className="quote-block">
    <p>"{content}"</p>
    {author && <cite>{author}</cite>}
    </blockquote>
    );
    }

    The component receives the block’s fields directly as props. author is optional because it was not marked required in the config. The && guard prevents rendering an empty <cite> when the editor leaves it blank.

  3. Register in RenderBlocks

    src/components/render-blocks/render-blocks.tsx
    import QuoteBlock from './quote/quote-block';
    function getBlock(block: Blocks) {
    switch (block.blockType) {
    case 'text':
    return <TextBlock {...block} />;
    case 'image':
    return <ImageBlock {...block} />;
    case 'quote':
    return <QuoteBlock {...block} />;
    default:
    return null;
    }
    }

    block.blockType matches the slug from the config. Any block not listed in this switch will silently render nothing — always add a case when you create a new block.

  4. Add to a collection

    src/collections/pages.ts
    import { quote } from '@/blocks/quote/config';
    export const pages: CollectionConfig = {
    fields: [
    {
    name: 'blocks',
    type: 'blocks',
    blocks: [image, text, quote],
    },
    ],
    };

    Adding the config to the blocks array makes it available in the admin panel for that collection. Editors will see it in the block picker when editing a page.

The boilerplate includes these blocks out of the box:

BlockPurposeKey Fields
textRich text contentcontent (Lexical)
imageImage with captionimage, caption
  • Use descriptive slugs (quote not q)
  • Define interfaceName for type safety
  • Keep components focused on rendering
  • Move data transformation to hooks or utilities