275 lines
9.3 KiB
Markdown
275 lines
9.3 KiB
Markdown
---
|
||
title: "Blocks Field"
|
||
aliases: [blocks-field, payload-blocks, layout-builder]
|
||
tags: [payloadcms, fields, blocks, layout-builder, lexical]
|
||
sources: [raw/fields__blocks.md]
|
||
created: 2026-05-15
|
||
updated: 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|Array Field]] (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`)
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
admin: {
|
||
initCollapsed: true, // start collapsed
|
||
isSortable: false, // disable drag reorder
|
||
}
|
||
```
|
||
|
||
### Lexical Custom Rendering
|
||
|
||
When used inside [[wiki/payloadcms/rich-text|Rich Text (Lexical)]], 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`:
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
{
|
||
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
|
||
|
||
```ts
|
||
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|Array Field]]
|
||
- 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
|
||
|
||
- `raw/fields__blocks.md` — [payloadcms.com/docs/fields/blocks](https://payloadcms.com/docs/fields/blocks)
|
||
|
||
## Related
|
||
|
||
- [[wiki/payloadcms/fields-array|Array Field]] — homogeneous repeating rows
|
||
- [[wiki/payloadcms/fields-complex|Fields: Complex]] — all structural fields overview
|
||
- [[wiki/payloadcms/rich-text|Rich Text (Lexical)]] — Blocks feature inside Lexical editor
|
||
- [[wiki/payloadcms/database-indexes|Database Indexes]] — `unique` field gotchas
|
||
- [[wiki/payloadcms/typescript|TypeScript]] — `interfaceName`, type generation
|