BizzPocket

Groups: blocks inside a block

On this page

A container block holds other blocks. The seller adds blocks inside it in the page editor, and the container decides where they go: a column, a row, a grid, a panel with a background. A container is a normal block with one extra field, f.blocks().

You can write your own, or install the registry's ready-made one: bizzpocket add group (below).

Contents: A container in 20 lines · The layout JSON · f.blocks() options · What render gets · Data and streaming · Limits · The registry group block · What the editor and the hosts do


A container in 20 lines

Create blocks/panel/panel.tsx:

blocks/panel/panel.tsx
import { block, f } from '@bizzpocket/theme';
import './panel.css';

export default block({
  name: 'Panel',
  category: 'content',
  description: 'A titled box with blocks inside, side by side or stacked.',
  fields: {
    title: f.text({ default: 'Our picks' }),
    layout: f.select(['stacked', 'side-by-side'], { default: 'side-by-side' }),
    colors: f.colorScheme(),
    // The blocks inside. They are not a prop the seller types: the editor offers "add block inside".
    items: f.blocks({ allow: ['hero', 'promo-banner', 'category:products'], max: 4 }),
  },
  render: ({ title, layout, items }) => (
    <div className={`panel panel--${layout}`}>
      <h2>{title}</h2>
      <div className="panel__items">{items}</div>
    </div>
  ),
});

Create blocks/panel/panel.css:

css
[data-theme] .panel {
  padding: 2rem;
  background: var(--bp-color-background, transparent);
  color: var(--bp-color-text, inherit);
}
[data-theme] .panel__items {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}
[data-theme] .panel--side-by-side .panel__items {
  flex-direction: row;
  flex-wrap: wrap;
}
[data-theme] .panel--side-by-side .panel__items > * {
  flex: 1 1 16rem;
  min-width: 0;
}
/* The children must not inherit the panel's colour trait (see "What render gets"). */
[data-theme] .panel__items > * {
  --bp-color-background: initial;
  --bp-color-text: initial;
}

items is an array of the children, already rendered. Put it where the children go and style the direct children of that element (each child is its own <section data-block-type="...">).


The layout JSON

The children are saved in the layout, on the container's node, as children. They are layout nodes like any other block: an id (a UUID), a type, props and visibility. A container node always has children, even when it is empty ("children": []).

layouts/home.json, with the panel holding a hero and a promo banner:

json
{
  "schemaVersion": 1,
  "pageType": "home",
  "pageIdentifier": "home",
  "themeId": "my-theme",
  "blocks": [
    {
      "id": "0b7c5d1e-3f4a-4b5c-8d6e-7f8091a2b3c4",
      "type": "panel",
      "props": { "title": "This week", "layout": "side-by-side" },
      "visibility": { "devices": ["mobile", "desktop"] },
      "children": [
        {
          "id": "1c8d6e2f-4a5b-4c6d-9e7f-8091a2b3c4d5",
          "type": "hero",
          "props": {},
          "visibility": { "devices": ["mobile", "desktop"] }
        },
        {
          "id": "2d9e7f3a-5b6c-4d7e-8f9a-91a2b3c4d5e6",
          "type": "promo-banner",
          "props": {},
          "visibility": { "devices": ["mobile", "desktop"] }
        }
      ]
    }
  ]
}

You do not have to write the ids by hand. add-to-layout adds a container with "children": [], and --parent <id> adds a block inside it:

bash
bizzpocket add-to-layout panel                    # prints: added panel to layouts/home.json (id 0b7c…)
bizzpocket add-to-layout hero --parent 0b7c5d1e-3f4a-4b5c-8d6e-7f8091a2b3c4

--parent refuses an id that is not in the layout, or a block that has no f.blocks() field.


f.blocks() options

Option What it does Default
allow Which blocks may go inside. Each entry is a block type ('hero'), a whole category ('category:media'), or '*' for any block. A category: entry matches a block's category as the block sets it. ['*'] (any block)
max At most this many children. 24
label The name of the "blocks inside" area in the editor. from the key
help Help text under it. none

allow and max are rules for the editor, and the renderer enforces them too: a saved child whose type is not allowed is skipped (and logged), and children past max are not rendered.

The rules are written to dist/blocks.json. The block is marked "container": true, and its propsSchema carries them, in place of a property:

json
"x-bp-children": { "childrenField": "items", "allow": ["hero", "promo-banner", "category:products"], "max": 4 }

What render gets

  • The f.blocks() key is a ReactNode[]: one element per child, in the saved order, each inside its normal block wrapper (<section data-block-type data-block-id>). An empty container gets [].
  • Place the children; do not look inside them. You do not get their props or data. Wrap them, space them, put them in columns; that is all.
  • The container's own fields work as usual, traits included. Trait variables inherit: f.spacing() and f.colorScheme() set --bp-spacing-* / --bp-color-* on the container's wrapper, and a child whose CSS reads var(--bp-spacing-top, 2rem) without a value of its own would get the container's. Reset them on the children with initial (as the panel above and the registry group do); a child's own trait values are on its wrapper's style attribute and still win.
  • f.blocks() is not a prop the seller edits: it has no default, is not in defaultProps, and a saved value under its key is ignored.

The container's wrapper gets data-bp-container (for the renderer's streaming rules, below).


Data and streaming

Each child is a block of its own:

  • Data. A child's data() is resolved exactly like a top-level block's. The server walks the layout's children when it collects a page's needs, so a page with groups is still one data call; a static child stays static. The container itself holds no data (it can have data() like any block, but its children never see it).
  • Streaming. The container does not wait for its children. A child whose data is slow streams on its own: its placeholder sits inside the container, and the rest of the container, and the blocks above, paint at once. As for top-level blocks, nothing below a pending child is displayed until it fills, so nothing jumps (STREAM_ORDER_CSS and CONTAINER_ORDER_CSS in the runtime).
  • Islands and errors. A child's islands hydrate as usual. A child that throws renders as an empty section; its siblings and the container are unaffected. A container whose own render throws renders as an empty section, without its children.
  • LCP. When the first content block of a page is a container, its first children are the candidates for the page's priority (eager, preloaded) image.

Limits

Limit Value What happens past it
f.blocks() fields per block 1 block() throws (a build error naming both fields)
Where f.blocks() may be top level of fields inside an f.list or f.group, block() throws
Nesting depth a block may sit at most 3 levels below a top-level block (a group in a group in a group) deeper children are not rendered; the renderer logs the block id
Children per container max (default 24) the rest are not rendered (logged)

The registry group block

bash
bizzpocket add group

copies blocks/group/group.tsx and group.css into your theme. It is a layout container with no data and no JavaScript (0 KB in the size table's client gz column):

Field Kind What it does
layout select column (stacked), row (side by side, wrapping), grid
columnsMobile / columnsTablet / columnsDesktop number grid columns below 48rem / from 48rem / from 64rem (1, 2, 3 by default)
gap select space between the blocks: none, s, m (your --spacing-block-gap), l
align select stretch, start, center, end across the layout direction
width select full, wide (90rem), content (72rem), narrow (44rem), centred
padding f.spacing() padding from your spacing tokens
colors f.colorScheme() background and text colour from your colour tokens
blocks f.blocks({ max: 12 }) the blocks inside (any block)

Its CSS is scoped [data-theme] .bp-group… and reads tokens with fallbacks, like every registry item. To change what it accepts, edit the copy: blocks: f.blocks({ allow: ['category:content', 'category:media'], max: 6 }).

A layout node for it:

json
{
  "id": "3e0f8a4b-6c7d-4e8f-9a0b-a2b3c4d5e6f7",
  "type": "group",
  "props": { "layout": "grid", "columnsDesktop": 4, "gap": "l" },
  "visibility": { "devices": ["mobile", "desktop"] },
  "children": []
}

What the editor and the hosts do

  • Bun runtime (bizzpocket dev, and the runtime that serves themes): renders children as above.
  • Next host (@bizzpocket/theme-sdk/v3): the adapter passes the React children the host gives a container's component into the f.blocks() key, and containerSpecOf(definition) tells the host which blocks are containers. The host has to render each child through its block pipeline and pass the results; until it does, a container renders empty there (host change list, item 25).
  • Seller editor: adding, reordering and removing blocks inside a container, honouring allow and max from blocks.json, is host change list item 24. Until then, put children in the default layouts by hand (or with add-to-layout --parent).

Next: fields.md for every other field kind.