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

275 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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