Basic · 3 of 5

Custom Theme

The directory layout BuilderJS reads. Fork themes/default/, change the brand block, edit one or two templates — every page across your product picks it up.

Walkthrough

  1. The directory layout

    Each theme is a flat folder under themes/. index.json registers which templates exist; the matching <Name>.template.html files render them. Sample pages live under master/sample/<category>/ — the gallery and the in-builder Change Theme modal both read from there.

    Tree
    themes/example-mini/
    ├── index.json                       # 1 — config + brand variables
    ├── Block.template.html              # 2 — block frame (the file most buyers edit)
    ├── Page.template.html               # 3 — page wrapper
    ├── master/
    │   └── sample/
    │       └── email/
    │           └── HelloThere.json      # 4 — a minimal sample page
    └── README.md                        # not read by the builder; for the buyer
    
    # Production themes also ship Grid · Cell · Heading · P · Image · Button
    # · Alert · Menu · Divider · etc. — copy themes/default/ for the full set.
  2. Define brand variables in index.json

    A _brand block is a buyer convention — not part of the BuilderJS schema. Templates read it via the EJS render context (configData._brand) and weave the values into the inline style string. Change one line here, every block on every page reflavors.

    JSON
    {
      "name": "example-mini",
      "title": "Example Brand",
      "_brand": {
        "primary":    "#5b21b6",
        "primary_fg": "#ffffff",
        "accent":     "#a855f7"
      },
      "pages":     ["Page"],
      "templates": ["Block", "Grid", "Cell", "Heading", "P", "Image", "Button"]
    }
  3. Edit Block.template.html — the highest-leverage file

    EJS server-side. formatter.toStyleStringAll() emits the format properties bound by the controls panel; you can append your own CSS to apply brand-wide styling. Below: a 4 px brand-primary stripe on every block — one of the smallest visible diffs against the default theme.

    EJS
    <%
    var brand = (typeof configData !== 'undefined' && configData && configData._brand) || {};
    var brandStripe = brand.primary ? ('border-left:4px solid ' + brand.primary + ';') : '';
    %>
    <div style="<%- formatter.toStyleStringAll() %><%- brandStripe %>">
        <%- elements %>
    </div>
  4. Register the new theme in ThemeRegistry

    Open demo/backend/_lib/ThemeRegistry.php and add a row to the SEED constant under the matching category (standard / extended / page). The gallery + Change Theme modal pick it up on next reload — no DB, no rebuild.

    PHP
    private const SEED = [
        'standard' => [
            // …existing rows…
            ['dir' => 'example-mini', 'name' => 'Hello There', 'sample' => 'master/sample/email/HelloThere'],
        ],
        // 'extended', 'page' — same shape
    ];

Live demo

demo-mini-builder--rich-3col-header
Live demo loads themes/default — the walkthrough teaches forking themes/example-mini

The whole snippet

Click Copy on any file to grab it byte-identical, or Expand to read inline (capped at ~520 px so the page stays scannable). Multi-file snippets surface a side-by-side grid — copy only the files you need.

{
  "name": "page",
  "template": "Page",
  "formats": { "padding_top": "32", "padding_bottom": "32" },
  "page_title": "Hello there",
  "blocks": [
    {
      "name": "block",
      "template": "Block",
      "formats": {
        "padding_top": "24", "padding_bottom": "24",
        "padding_left": "16", "padding_right": "16",
        "background_color": "#f5f3ff"
      },
      "elements": [
        {
          "name": "heading",
          "template": "Heading",
          "level": "h1",
          "formats": { "color": "#5b21b6", "font_size": "32", "font_weight": "700" },
          "content": "Hello there 👋"
        }
      ]
    }
  ]
}

Notes

Why does the live demo above load themes/default instead of themes/example-mini? Because themes/example-mini/ ships only Page + Block templates + one sample — it's an intentional teaching artefact, not a bootable theme. Production themes need the full template set (Heading, P, Image, Button, Grid, Cell, Menu, Divider, Alert, …). To see your forked theme rendered live, copy themes/default/ wholesale, edit incrementally, and the same mini-builder code in the steps above renders it.

Real-world starting point: copy themes/default/ to themes/your-brand/ wholesale, then customise. The default ships ~35 templates covering every element type the builder supports — far simpler than building from zero.

Where templates run: EJS templates render server-side at canvas paint time and again at HTML export. Both contexts get the same formatter + configData + elements locals — what you see on the canvas is what you get in the exported HTML.

Sample JSON shape: a tree of nested objects, each carrying {name, template, formats, …} plus element-specific fields. See the dev docs for the full schema. The bidirectional contract is documented there too — builder.getData() emits exactly this shape, and builder.load(...) consumes it.

Hardened invariant: don't rename a template that already has consumer pages — buyers loading a saved JSON with the old template name will see a "missing template" error. Add a new template instead, migrate samples gradually, then deprecate. (See docs/BUILDER.md § "Hardened Rules" → "Before adding a new SAMPLE JSON".)