Container Blocks
Container blocks hold other blocks inside them — sliders with slides, grids with columns, accordions with panels. Define them in your blockSchema using blocks_layout or object_list widgets.
blocks_layout: Typed Child Blocks
Each child has its own @type and schema (from blocks). Children are stored in a shared blocks dict on the parent, with the field holding { items: [...] } for ordering:
// Schema definition
slides: {
title: 'Slides',
widget: 'blocks_layout',
allowedBlocks: ['slide', 'image'],
defaultBlockType: 'slide',
maxLength: 10,
}
// Resulting data
{
"@type": "slider",
"blocks": {
"slide-1": { "@type": "slide", "title": "First" },
"slide-2": { "@type": "image", "url": "..." }
},
"slides": { "items": ["slide-1", "slide-2"] }
}All blocks_layout fields on the same block share the same blocks dict. So a block can have multiple container fields (e.g., header_blocks and footer_blocks) whose children all live in the parent's blocks.
object_list: Items Sharing One Schema
All items share one inline schema, stored as an array with an ID field. Use dataPath when the data is nested within the block:
// Schema
slides: {
title: 'Slides',
widget: 'object_list',
idField: '@id',
dataPath: ['data', 'rows'], // optional path when data is nested
schema: {
properties: {
title: { title: 'Title' },
image: { title: 'Image', widget: 'image' },
description: { title: 'Description', widget: 'slate' },
}
}
}
// Resulting data (note: nested under dataPath)
{
"@type": "slider",
"data": {
"slides": [
{ "@id": "slide-1", "title": "First", "image": "..." },
{ "@id": "slide-2", "title": "Second", "image": "..." }
]
}
}object_list with allowedBlocks: Typed Items
When allowedBlocks is set on an object_list, items can have different types (like blocks_layout) but are still stored as an array. Each item's type is stored in the field specified by typeField (defaults to '@type') and its schema is looked up from blocks:
facets: {
title: 'Facets',
widget: 'object_list',
allowedBlocks: ['checkboxFacet', 'selectFacet'],
typeField: 'type',
defaultBlockType: 'checkboxFacet',
}
// Resulting data
{
"@type": "search",
"facets": [
{ "@id": "facet-1", "type": "checkboxFacet",
"title": "Content Type", "field": "portal_type" },
{ "@id": "facet-2", "type": "selectFacet",
"title": "Subject", "field": "Subject" }
]
}Both blocks_layout and object_list look the same in the editing UI and blocks can be dragged between them — data is automatically adapted when moving between formats (ID fields added/stripped, type fields set appropriately).
Rendering Containers in Your Frontend
Add data-block-uid to each child element. You don't need to mark the container element itself:
<div class="slider" data-block-uid="slider-1">
<div class="slide" data-block-uid="slide-1"
data-block-add="right">
<img src="/news.jpg"/>
<h2>Big News</h2>
</div>
<div class="slide" data-block-uid="slide-2"
data-block-add="right">
...
</div>
<a data-block-selector="-1">Prev</a>
<a data-block-selector="+1">Next</a>
</div>- data-block-add="bottom|right" — Controls where the '+' button appears. By default it will be the opposite of its parent. Use "bottom" for vertical stacking, "right" for horizontal.
- data-block-selector="-1|+1|blockId" — Tag paging buttons so sidebar selection can navigate paged containers
Table Mode
Set addMode: 'table' for table-like structures (rows containing cells). This lets users add and remove columns as easily as rows:
rows: {
widget: 'object_list',
idField: 'key',
addMode: 'table',
dataPath: ['table', 'rows'],
schema: {
properties: {
cells: {
widget: 'object_list',
idField: 'key',
schema: {
properties: {
value: { title: 'Content',
widget: 'slate' }
}
}
}
}
}
}Empty Blocks
A container can never be empty. When the last child is deleted, either the defaultBlockType is added, or a special block with @type: "empty" is inserted. Empty blocks are stripped before saving. Render them as empty space — Hydra puts a '+' button in the middle for the user to replace it.
You can override the look of the '+' button by rendering something inside the empty block and adding data-block-add="button" to it.
Synchronised Block Types in a Container
You might want one container type that holds different block types but constrains them to all be the same type with synchronised settings. A field on the parent lets the editor select the type and all blocks get converted using fieldMappings:
blocks: {
gridBlock: {
allowedBlocks: ['teaser', 'image'],
schemaEnhancer: {
inheritSchemaFrom: {
typeField: 'variation',
},
},
},
teaser: {
schemaEnhancer: {
childBlockConfig: {
editableFields: ['href', 'title', 'description'],
},
},
fieldMappings: {
default: { '@id': 'href', 'title': 'title', 'image': 'preview_image' },
},
},
}- inheritSchemaFrom: Parent inherits schema from selected child type. When the type field changes, child blocks sync to new type.
- typeField: Field name for selecting child type (e.g., 'variation')
- defaultsField: Field name for storing inherited defaults (e.g., 'itemDefaults')
- blocksField: Which blocks field the sub-blocks are in. Set to ".." to use the parent's allowedBlocks.
- filterConvertibleFrom: Only allow selecting a block type which can convert from the specified type.
- childBlockConfig: Child hides fields except editableFields when inside a parent with inheritSchemaFrom.