Bolt CMS Docs
Sign in

Rendering Content

Blocks

Self-contained partials that bundle their own CSS, markup, and JavaScript — each rendered with its styles, element ids, and script scoped to a single instance.

Overview

A block is a partial that carries its own styling and behavior in one file. Where a component is purely presentational markup driven by a data array, a block bundles three things together — a <style>, the markup, and a <script> — and has all three scoped to a single instance. Two blocks of the same kind can sit on one page with separate styles and separate state, and neither one touches the other.

Reach for a block when a partial needs its own JavaScript or instance-scoped CSS — a counter, a toggle, a tabbed panel, a copy-to-clipboard card. For static, presentational markup, a plain component is lighter.

A block has no fixed size — it can be as small as a single button or as large as a full page layout, depending on how much its template renders. Blocks also nest: because each instance is scoped on its own, a block's template can echo block(...) to render another block inside itself. The inner block is fully resolved — its styles, element ids, and script scoped to a distinct id — before the outer block is scoped around it, so the two never collide. A page-sized layout block is a natural place to compose smaller blocks this way.

The mechanism is borrowed from Paradigm CMS: inside a block file you write the literal token $block wherever a name must be unique to the instance. At render time the engine rewrites every $block to a generated id like block_a1b2c3d4e5_1, so ids, class names, and the JavaScript namespace can never collide.

Rendering a Block

Blocks live in blocks/. The block() function — loaded for every page by the front controller — takes the block name and a data array, and returns the rendered HTML. Echo it wherever you want the block to appear:

<?php echo block('counter', ['label' => 'Visitors', 'start' => 5]); ?>

The first argument resolves to a file under blocks/. The .php extension is optional, a leading blocks/ is ignored, and sub-directories work — so all of these are valid:

Call File
block('counter') blocks/counter.php
block('counter.php') blocks/counter.php
block('cards/stat') blocks/cards/stat.php

The name is confined to the blocks directory: a traversal attempt (..) or a missing file renders nothing visible — just an HTML comment — instead of including an arbitrary file or throwing.

A bare name resolves in core only. To load a block from one of your extensions, name it explicitly with a slug: prefix — so block('billing:invoice') renders extensions/billing/blocks/invoice.php. A name core doesn't have, with no slug, renders nothing (an HTML comment) rather than reaching into an extension.

Anatomy of a Block File

A block is one PHP file with three regions. Below is the shipped blocks/counter.php, with its styling abbreviated — notice how the $block token threads through the style, the markup, and the script:

<?php
// blocks/counter.php
$data   = is_array($data ?? null) ? $data : [];
$label  = htmlspecialchars($data['label'] ?? 'Count', ENT_QUOTES);
$start  = (int) ($data['start'] ?? 0);
$step   = (int) ($data['step'] ?? 1);
$accent = htmlspecialchars($data['accent'] ?? '#3b82f6', ENT_QUOTES);
?>
<style>
    #$block-value {
        font-size: 2.25rem;
        font-weight: 700;
        color: <?php echo $accent; ?>;
    }
    #$block-card .$block-btn {
        color: <?php echo $accent; ?>;
        cursor: pointer;
    }
    /* ...full card + control styles in the shipped file... */
</style>

<div id="$block-card">
    <span id="$block-label"><?php echo $label; ?></span>
    <span id="$block-value"><?php echo $start; ?></span>
    <button class="$block-btn" type="button" onclick="$block.add(-1)">&minus;</button>
    <button class="$block-btn" type="button" onclick="$block.add(1)">&plus;</button>
</div>

<script>
    $block = {
        value: <?php echo $start; ?>,
        step: <?php echo $step; ?>
    };

    $block.add = function (direction) {
        $block.value += direction * $block.step;
        document.getElementById('$block-value').textContent = $block.value;
    };
</script>
  • The <style> targets $block-prefixed selectors, so its rules only ever match this instance's elements.
  • The markup builds every id and class from $block, and wires inline handlers to $block.method().
  • The <script> defines a $block object holding this instance's state. The data lives in the object literal; methods are attached afterward as $block.method = function () { ... }.

How Scoping Works

Each call to block() generates one id — block_ followed by a short hash and a per-request counter — and rewrites every unescaped $block token in that block's output to it. The counter guarantees a fresh id per call, so repeated instances are always distinct. The rewrite is purely textual and applies only to that one block's HTML; the rest of the page is untouched.

So the markup you authored as:

<div id="$block-card">
    <button class="$block-btn" onclick="$block.add(1)">&plus;</button>
</div>

reaches the browser as:

<div id="block_a1b2c3d4e5_1-card">
    <button class="block_a1b2c3d4e5_1-btn" onclick="block_a1b2c3d4e5_1.add(1)">&plus;</button>
</div>

Render the same block again and it becomes block_…_2. The <style> rules, element ids, and the global $block object are renamed in lockstep, so the second instance is fully independent of the first.

Passing Data

Whatever array you pass as the second argument is available inside the template as $data. The template's own variables stay local to the render — they never leak back into the page — so there is nothing to unset() afterward.

A well-behaved block guards its input and escapes anything dynamic it prints:

$data  = is_array($data ?? null) ? $data : [];
$label = htmlspecialchars($data['label'] ?? 'Count', ENT_QUOTES);

Two things help in the corners where the bare $block token is awkward:

  • $blockId — the resolved instance id, available as a PHP variable. Use it inside a double-quoted PHP string, where writing $block would make PHP try to interpolate it: echo "<label for='{$blockId}-name'>".
  • \$block — a backslash escapes the token, printing a literal, un-scoped $block. This is how this very page shows the token in its examples.

Live Demo

These three counters are real, rendered by the calls shown below. Each keeps its own count and its own accent color — click the buttons and they move independently, because every instance got a distinct scoped id:

echo block('counter', ['label' => 'Apples',  'start' => 3, 'accent' => '#22c55e']);
echo block('counter', ['label' => 'Oranges', 'start' => 7, 'accent' => '#f97316']);
echo block('counter', ['label' => 'Lemons',  'start' => 0, 'accent' => '#eab308']);
Apples 3
Oranges 7
Lemons 0

Reaching a Parent Block

Data flows down into a block through $data. To go the other way — let a nested block call a method on the block it sits inside — use the $parent token. It is the counterpart of $block: where $block resolves to this instance's id, $parent resolves to the id of the block one level out. Because it is simply the parent's $block id, it works anywhere $block does — as the parent's JavaScript object ($parent.method()), as an id prefix ($parent-card), or in a selector.

So a child wires an inline handler straight to a method on its parent —

<!-- inside the child block -->
<button onclick="$parent.add(1)">Add one</button>

— and the parent defines that method on its own $block:

<!-- inside the parent block -->
<script>
    $block = { total: 0 };
    $block.add = function (n) {
        $block.total += n;
        document.getElementById('$block-total').textContent = $block.total;
    };
</script>

At render time the child's $parent.add(1) is rewritten to a direct call on the parent's scoped object — block_a1b2c3d4e5_1.add(1). There is no DOM walking and no instance registry to consult: the reference is resolved to a plain global while the page is assembled, so it costs nothing at runtime. For the rare spot where the bare token is awkward — inside a double-quoted PHP string, say — the resolved parent id is also exposed as the PHP variable $parentId, exactly as $blockId is for $block.

The one-backslash escape that prints a literal \$block does double duty once a block is nested: it defers resolution one level outward, so the same escape climbs the ancestry. A bare $parent is the parent; \$parent resolves in the grandparent's scope, \\$parent in the great-grandparent's — one backslash per extra level:

<!-- inside a nested child -->
<button onclick="$parent.refresh()">parent</button>
<button onclick="\$parent.refresh()">grandparent</button>

A token that runs out of ancestors — $parent in a top-level block, or one backslash too many — is left as a literal rather than resolved, so the mistake surfaces instead of silently pointing somewhere wrong.

The panel below is live. Its three buttons are separate tally-button child blocks; each calls $parent.add(...) on click, and the parent — a single tally block — owns the running total and the “last” label they all update:

echo block('tally', ['title' => 'Fruit picked']);
Fruit picked 0
last: —

Timing: When a Child Can Call Up

A child block's <script> runs before its parent's, because the child is rendered into the parent's markup above the point where the parent's own script sits. Calling $parent from an event handler — like the onclick in the demo — is always safe: by the time the button is pressed every block on the page has initialized. The same holds for any call you defer to a ready callback.

Only one case needs care: a child that must call its parent synchronously, as it loads. For that, move the parent's <script> above its markup and keep it definition-only — assign $block and its methods, but touch no DOM there, since the parent's own elements aren't parsed yet. The parent object then exists before any child script runs:

<!-- parent block: define first, render children second -->
<script>
    $block = { total: 0 };
    $block.add = function (n) { /* ... */ };
</script>

<div id="$block-panel">
    <?php echo block('child'); ?>   <!-- may now call $parent.add() as it loads -->
</div>

The rule of thumb is define early, act late: declare state and methods up top so descendants can find them, and do anything that reads the DOM or reaches back into children from an event or a ready callback.

Conventions

  • One block, one file in blocks/, named for what it renders: counter.php, tabs.php, copy-code.php.
  • Use $block for every instance-unique name — element ids, class names, and the JavaScript object. Prefix, don't free-float: write $block-card, not card.
  • Guard and escape the input. Open with $data = is_array($data ?? null) ? $data : []; and wrap printed values in htmlspecialchars().
  • Keep methods off the object literal. Put state in $block = { ... }, then attach behavior as $block.method = function () { ... }.
  • Wire events inline with onclick="$block.method()"; the handler resolves to this instance's object.
  • Echo the result. block() returns a string — echo block(...) to place it, or keep the string to compose it elsewhere.
  • Reach upward with $parent, not globals. A nested block calls its parent through $parent.method() (and an ancestor above that with \$parent). Make those calls on events or in a ready callback — a child's script runs before its parent's.
Component patterns Data store