ChaiBuilder Logo

Blocks

Everything an editor puts on a page is a block. A page is not a document with regions; it is a tree of blocks, and that tree is the page's content.

A block has two halves, and holding them apart explains most of the product:

  • A React component - what renders, on the server for visitors and in the canvas for editors.
  • A props schema - what an editor can change. The builder generates the settings panel from this schema; nobody hand-builds a form.

Write a component and describe its props, and the block becomes something a non-technical editor can drop on a page, configure, and style.

A page is a tree

Every block in the tree carries an id and a type string, plus its own props and styles. Blocks nest: a section holds a container, which holds a heading and a button. Some blocks exist purely to hold others.

Page
└── Section
    └── Container
        ├── Heading      "Pricing"
        ├── Text         "Simple, flat pricing"
        └── Repeater     over the plans collection
            └── Card
                ├── Text     {{ plan.name }}
                └── Button   {{ plan.ctaLabel }}

The tree is stored as JSON on the page row. It is data, not code, which is why a page can be diffed, versioned, restored, and copied between environments.

What a block declares

A block definition is mostly metadata. Grouped by what it is for:

Group Declares Why it matters
Identity A type string, a label, a group, an icon, an optional description The type string is the permanent identity; the rest is how the block appears in the library
Children Whether it wraps other blocks, what children it starts with, what it will accept, whether it can be nested Turns loose blocks into a structure editors cannot easily break
Props A schema plus UI hints Becomes the settings panel
Special props Which props are translatable, AI-editable, or editable inline on the canvas The same prop can be plain text to one system and a translation target to another
Data An optional data provider, and what it depends on Lets a block fetch its own server data - see Data Providers
Scope Which page types the block is allowed on Keeps a blog-only block out of the landing-page library
Guardrails Whether it can be deleted, moved, or duplicated Editor ergonomics, not security. Anything expressed here is advisory

The last row is worth reading twice. Block guardrails shape what the builder UI offers; they are not an authorization boundary. Access control lives in roles and permissions.

What a block receives when it renders

Beyond its own configured props, every block is handed the same render context:

It receives Use it for
lang The current language
draft Whether this is a draft render
inBuilder Whether it is rendering in the canvas rather than for a visitor
pageProps Facts about the page: slug, page type, page id, language
pageData The resolved data for this render, which is what bindings read from

inBuilder is the escape hatch that keeps the canvas usable. A carousel that autoplays for visitors can hold still while it is being edited; a block with no content yet can render a placeholder instead of collapsing to nothing.

Props, styles, and bindings are three different things

  • Props are the block's configured values: text, a link target, a toggle, an image URL.
  • Styles are Tailwind classes stored per block. Design tokens are named bundles of those classes, applied as a unit.
  • Bindings are props whose value is a {{ }} data path, resolved on the server at render time. See Data Binding.

A prop holding {{ blog.title }} is still just a prop. Nothing about the block changes; the value arrives resolved.

One consequence catches people out: blocks store values, not references. An image block holds a URL, not a pointer to an asset record, so replacing the asset in the media library does not update pages that already use it.

Three families of block

Family Where it comes from Notes
Shipped blocks The builder's own library Structural blocks, text, media, forms, repeaters
Your blocks Registered by your project Ordinary React components; this is the main extension point
Global blocks Built by editors in the builder A reusable subtree referenced from many pages: header, footer, CTA band

A global block is not really a block type. It is a page whose block tree is referenced from other pages, so editing it once updates every page that references it. What sits in the consuming page is a lightweight reference that the server replaces with the real tree before rendering. See Global Blocks.

Type strings are effectively permanent

The stored page JSON refers to block types by string. When a page renders, each block is looked up in the registry by that string, and a block whose type is not registered renders nothing - no error, no placeholder, no visible gap in the page.

That single behaviour drives a few rules:

  • Do not rename a block type once pages use it. Change the label, not the type.
  • Removing a block type silently empties every page that used it. Search for usage first.
  • Renaming a prop leaves the old value stranded in stored JSON. Read the old name as a fallback, or migrate the data.

The upside of the same design: a block that throws while rendering can be contained. When the app provides an error boundary to the renderer, a failing custom block degrades to nothing instead of taking the whole page down with it.

Failure modes worth knowing

Symptom Usual cause
A block vanished from the published page Its type is not registered in the render path, or a condition on it resolved to false
A block renders in the canvas but not live Registration happens in the editor entry but not the public route
Editor's text is ignored A data provider on that block returns the same prop name; provider output wins
Empty text where a binding was The bound value was missing, and a missing binding resolves to an empty string
Styling disappeared after a token change Deleting a design token strips the classes it carried

© ChaiBuilder. All rights reserved.