obsidian/wiki/payloadcms/fields-blocks.md
2026-05-15 15:23:29 +01:00

9.3 KiB
Raw Permalink Blame History

title aliases tags sources created updated
Blocks Field
blocks-field
payload-blocks
layout-builder
payloadcms
fields
blocks
layout-builder
lexical
raw/fields__blocks.md
2026-05-15 2026-05-15

Overview

The Blocks Field stores an array of objects where each object is a "block" with its own schema. Unlike wiki/payloadcms/fields-array (every item looks the same), blocks let you mix and match different content types in any order.

Primary use-cases:

  • Page builder (Quote, CallToAction, Slider, Gallery)
  • Form builder (Text, Select, Checkbox)
  • Event agenda (Break, Presentation, BreakoutSession)
import type { Field } from 'payload'

export const MyBlocksField: Field = {
  name: 'layout',
  type: 'blocks',
  blocks: [QuoteBlock, CTABlock],
}

Block Field Config Options

Option Required Description
name Property name in DB
blocks Array of block configs available to this field
minRows Minimum block count during validation
maxRows Maximum block count during validation
filterOptions Function returning allowed block slugs based on context
defaultValue Array of block data for initial state
localized Localizes all nested data without per-field localized: true
validate Custom validation function (runs client + server)
saveToJWT Include in user JWT if top-level auth field
hooks Field-level hooks
access Field-level access control
labels Customize block row labels in Admin
unique DB-level unique index (see gotcha below)
hidden Hide from APIs but still save to DB
virtual Disable DB storage or link via string path
typescriptSchema Override generated TS type with JSON schema

⚠️ unique: true gotcha — creates a collection-wide DB unique index, not per-document. No two documents can share the same value at that nested path. On MongoDB, documents without the block collide on null. Use a custom validate function instead for within-document uniqueness.


Block Field Admin Options

admin: {
  initCollapsed: true,   // start collapsed
  isSortable: false,     // disable drag reorder
}

Lexical Custom Rendering

When used inside wiki/payloadcms/rich-text, you can customize block display:

  • admin.components.Label — custom React component for the label
  • admin.components.Block — completely replace the block rendering in Lexical

Import utility components from @payloadcms/richtext-lexical/client:

import {
  InlineBlockEditButton, BlockEditButton,
  InlineBlockRemoveButton, BlockRemoveButton,
  InlineBlockLabel, InlineBlockContainer,
  BlockCollapsible,
} from '@payloadcms/richtext-lexical/client'

Block Config Options (per block)

Each block is its own config object:

Option Required Description
slug Identifies block type; saved as blockType on each block
fields Array of fields for this block
labels Admin label (auto-generated from slug if omitted)
interfaceName Creates reusable TypeScript interface + GraphQL type
dbName Custom SQL table name (Postgres)
graphQL.singularName GraphQL schema name (deprecated; prefer interfaceName)
custom Plugin extension point

Block Admin Options

Option Description
components.Block Replace entire block including header
components.Label Replace block label only
disableBlockName Hide the blockName field (true to hide)
group Group blocks in the selection drawer
images.icon 20×20px icon for Lexical menus (SVG preferred)
images.thumbnail 3:2 ratio thumbnail for block selection drawer

Block Image Guidelines

const QuoteBlock: Block = {
  slug: 'quote',
  admin: {
    images: {
      icon: { url: 'https://example.com/icons/quote-20x20.svg', alt: 'Quote' },
      thumbnail: { url: 'https://example.com/thumbnails/quote-480x320.jpg', alt: 'Quote block' },
    },
  },
  fields: [{ name: 'quoteText', type: 'text', required: true }],
}
  • icon: 1:1 aspect ratio, 20×20px display — use SVG
  • thumbnail: 3:2 aspect ratio (480×320, 600×400, 900×600) — use centered important content

blockType, blockName, block.label

Property Scope Source Editable
blockType Each block Block's slug No — auto-set
blockName Each block Editor input Yes (hide with disableBlockName)
block.label Block type label in block config or slug fallback No — config only

blockName is the per-instance label editors give to distinguish identical block types (e.g., two Quote blocks with different names). block.label is shared by all blocks of that type.


Copy / Paste

Built-in clipboard support via the ... row action menu:

Action Scope Behavior
Copy Row Single block Paste into any position in any compatible field
Paste Row Single block Replaces a specific row
Copy Fields Entire field All blocks at once
Paste Fields Entire field Replaces ALL blocks in target field
  • Clipboard stored in localStorage under _payloadClipboard
  • Persists across tabs (same origin), not across different Payload origins
  • Validates target field's blocks config before allowing paste
  • Block IDs automatically regenerated on paste to prevent duplicates

⚠️ Paste Fields replaces everything — even if you copied a single row, "Paste Fields" replaces all blocks. To add a copied row to an existing field: add an empty block first, then use Paste Row on that empty block.


Block References (Performance Optimization)

Define a block once in root blocks config and reference it by slug to avoid sending the full schema to the client multiple times:

const config = buildConfig({
  blocks: [
    {
      slug: 'TextBlock',
      fields: [{ name: 'text', type: 'text' }],
    },
  ],
  collections: [
    {
      slug: 'collection1',
      fields: [{
        name: 'content',
        type: 'blocks',
        blockReferences: ['TextBlock'],
        blocks: [], // must be empty when using blockReferences
      }],
    },
  ],
})

⚠️ Block references are isolated — the block config cannot be modified per collection; access control runs without collection data.


Conditional Blocks (filterOptions)

Dynamically restrict which blocks are available based on sibling data:

{
  name: 'layout',
  type: 'blocks',
  filterOptions: ({ siblingData }) => {
    return siblingData?.enabledBlocks?.length
      ? [siblingData.enabledBlocks]
      : true // allow all
  },
  blocks: [block1, block2, block3],
}
  • Re-evaluated on every form state update
  • Blocks present in data but disallowed by filterOptions → validation error on save

Best Practices

  • Define each block config in its own file for reusability across collections
  • Use blockReferences when the same block appears in 3+ places
  • Use interfaceName on blocks for clean TypeScript interfaces
  • Prefer admin.disableBlockName for programmatic blocks where editors don't need to name rows
  • For within-document uniqueness, use validate — not unique: true

Example: Full Collection with Blocks

import { Block, CollectionConfig } from 'payload'

const QuoteBlock: Block = {
  slug: 'Quote',
  interfaceName: 'QuoteBlock',
  fields: [
    { name: 'quoteHeader', type: 'text', required: true },
    { name: 'quoteText', type: 'text' },
  ],
}

export const ExampleCollection: CollectionConfig = {
  slug: 'example-collection',
  fields: [{
    name: 'layout',
    type: 'blocks',
    minRows: 1,
    maxRows: 20,
    blocks: [QuoteBlock],
  }],
}

Key Takeaways

  • Blocks Field = heterogeneous array — each item can be a different schema, unlike wiki/payloadcms/fields-array
  • Each block is identified by slug (stored as blockType); editors can add a blockName per instance
  • filterOptions enables conditional availability based on form state
  • Copy/paste is cross-document; Duplicate is in-place only — do not confuse them
  • blockReferences optimizes performance when blocks are reused across many collections/lexical editors
  • unique: true on a nested block field is collection-wide, not per-document — use validate instead
  • Block images (icon + thumbnail) improve the editor experience in Lexical and the block selection drawer
  • Full TypeScript support: import type { Block } from 'payload'

Sources