Page Builder
<zn-page-builder> | ZnPageBuilder
A config-driven page composer: a palette of predefined section types, a linear canvas of section cards, and an inspector for editing each section’s content.
The Page Builder composes pages from predefined section types the host application
provides. Editors pick sections from the palette, order them on the canvas, and edit each section’s content
in the inspector — the output is a plain JSON config (PageState) the host persists and renders
however it likes.
Section types are declared as <template type slot="config"> children. The template’s
attributes (type, label, icon, icon-library,
color, category, description) define the palette entry; its content
is stamped into the inspector when a section of that type is selected. Controls are bound by their
name attribute: values prefill from the section’s data and write back on change /
zn-change.
<zn-page-builder id="kb-homepage-demo" heading="KB Homepage" subheading="Last updated 7th July 2026" auto-save style="height: 560px"> <zn-button slot="header-left" icon="refresh-cw@lu" panel-bg onclick="this.closest('zn-page-builder').undo()">Undo</zn-button> <zn-button slot="header-right" icon="check@lu" color="primary" onclick="alert(JSON.stringify(this.closest('zn-page-builder').state, null, 2))">Save All Changes</zn-button> <template type="hero" slot="config" label="Hero" icon="star" category="Headers" description="Large banner with optional search"> <zn-input name="title" label="Title"></zn-input> <zn-input name="subtitle" label="Subtitle"></zn-input> <zn-input name="gradient" label="Background gradient" placeholder="linear-gradient(135deg, #00aaff, #aa00ff)" help-text="CSS gradient painted behind the hero. Leave empty for the theme default."></zn-input> <zn-toggle name="showSearch" label="Show search" description="Draws the site search box in the banner."></zn-toggle> </template> <template type="article-list" slot="config" label="Article List" icon="list" category="Content" description="A list of KB articles"> <zn-input name="limit" type="number" label="Max articles"></zn-input> </template> </zn-page-builder>
Listen for zn-page-change to persist the config, or read/write the state property.
Types can also be registered programmatically via sectionTypes /
registerSectionTypes(), including a renderConfig(section, update) callback for
inspector bodies that need real logic.
The inspector
Selecting a section opens the inspector, which heads the panel with the section type’s icon and colour, the section’s name, its type (shown only where a renamed section makes it useful), and a close button that clears the selection. Below that, a Section name field renames the section (the card’s label), followed by the type’s stamped config form. A type whose template has no content gets a hint in place of the form rather than an empty panel.
Because the panel is narrow, the inspector sets its own form-label and help-text typography rather than
inheriting your app’s full-width form styling, and its gap is the single source of spacing
between fields — a control’s own outer margins are zeroed so they can’t stack with it. Stamped
<zn-toggle>s are laid out as a row, with the label and description left and the switch
right; set label-position yourself on a toggle to opt out.
Style the panel through inspector, inspector-header and
inspector-body parts.
JavaScript API
-
state— get/set the currentPageState. The getter returns a deep copy; the setter replaces the state wholesale (like theconfigattribute, it does not emitzn-page-change). -
addSection(type, index?)— insert a new section of a registered type (default: at the end). -
addSectionToSlot(type, containerId, slotIndex)— insert into a container’s empty slot, honouring itsacceptslist. Returns the new section, ornullif not allowed. -
undo()/redo()— step through edit history (bounded at 50 entries). There is no built-in toolbar: wire these to your own header buttons or keyboard shortcuts. -
registerSectionType(type)/registerSectionTypes(types)— programmatic registration, equivalent to slotting templates. Registration is additive — removing an entry fromsectionTypeslater does not unregister it. -
restoreAutoSave()— load the auto-saved draft, if one exists within its 1-day TTL (returnsfalseotherwise). See Saving below.
Saving
Wire your own save action into the header slots (rendered only when filled, as in the flow builder) and read
state — or persist on every zn-page-change:
<zn-page-builder id="kb-home" heading="KB Homepage" auto-save> <zn-button slot="header-right" color="primary" id="save-page">Save All Changes</zn-button> <!-- templates… --> </zn-page-builder> <script> const builder = document.querySelector('#kb-home'); document.querySelector('#save-page').addEventListener('click', () => { fetch('/api/pages/home', {method: 'PUT', body: JSON.stringify(builder.state)}); }); </script>
Add the auto-save attribute to also snapshot the page into localStorage, keyed by the builder’s
id (falling back to its heading), with a 1-day TTL — identical to
the flow builder’s auto-save:
<zn-page-builder id="kb-home" auto-save></zn-page-builder> <!-- every 5 minutes --> <zn-page-builder id="kb-home" auto-save="2"></zn-page-builder> <!-- every 2 minutes -->
Without the attribute nothing is saved, and an empty page is never written. While auto-save is on, a status
pill in the canvas’s bottom-left flashes “Auto-saved” as each snapshot lands and otherwise shows how long
ago the last one happened. When a page is loaded (config / state) that differs
from a fresh auto-saved draft, a banner offers to restore the draft — or call
restoreAutoSave() yourself.
The config
Every edit emits zn-page-change with the full page state (event.detail.state, also
readable via the state property) — plain JSON the host persists and later feeds back in through
the config attribute. Sections appear in page order; container sections carry a
children array sized to their slot count, with null for empty slots:
{ "sections": [ { "id": "s-mc41z-0", "type": "hero", "data": {"title": "Help Centre", "showSearch": true} }, { "id": "s-mc42a-1", "type": "article-grid", "label": "Popular articles", "data": {"title": "Popular"}, "children": [ {"id": "s-mc42h-2", "type": "article-tile", "data": {"article": "art_42"}}, {"id": "s-mc42p-3", "type": "article-tile", "data": {"article": "art_7"}}, null, null, null, null ] }, { "id": "s-mc43b-4", "type": "article-list", "data": {"articles": ["art_1", "art_3"]} } ] }
A required first section
Pages that must always open with a particular section — a hero banner, a masthead — set
required-first to that section type. The builder hoists an existing section of the type to the
top of the page, or inserts an empty one when there is none, and pins it there: it has no remove action,
ignores Delete, can’t be dragged or moved into a container slot, and nothing can be dropped above
it. Its content stays fully editable in the inspector, and the type stays in the palette, so further
sections of it can still be added below.
Which section is pinned is derived from the state — sections[0] when its type matches — so
nothing about the lock is written into the persisted config.
<zn-page-builder heading="KB Homepage" required-first="hero" style="height: 420px" config='{"sections":[{"id":"t1","type":"rich-text","data":{"content":"Welcome"}}]}'> <template type="hero" slot="config" label="Hero" icon="star" category="Headers" description="Banner with a heading and optional search"> <zn-input name="title" label="Title"></zn-input> <zn-toggle name="showSearch" label="Show search"></zn-toggle> </template> <template type="rich-text" slot="config" label="Rich Text" icon="notes" category="Content" description="A block of markdown content"> <zn-input name="content" label="Content"></zn-input> </template> </zn-page-builder>
The config above declares only a rich-text section, so the hero is inserted above it — loading a page that lacks the required section normalises it rather than rejecting it. The guard is client-side, so a host that persists the config should still enforce the same rule on save.
Container tiles
A section type with a slots attribute becomes a full-row container: its card renders a 3-column
grid of that many child slots beneath it. Drag sections from the palette into empty cells, drag children
between cells to reorder, or out onto the page. accepts restricts which types
the slots take. Containers can’t be placed inside other containers, and slot contents persist as
children on the section (empty slots are null).
<zn-page-builder heading="KB Homepage" style="height: 560px" config='{"sections":[{"id":"g1","type":"article-grid","data":{}}]}'> <template type="article-grid" slot="config" label="Article Grid" icon="grid_view" category="Layout" description="A 3x2 grid of article tiles" slots="6" accepts="article-tile"> <zn-input name="title" label="Grid title"></zn-input> </template> <template type="article-tile" slot="config" label="Article" icon="article" category="Content" description="A single article tile"> <zn-input name="article" label="Article id"></zn-input> </template> </zn-page-builder>
List sections
Sections that show a set of existing items (categories, articles, …) reference them by id: bind a
multi-select by name and the chosen ids persist as an array in the section’s data (e.g.
"data": {"articles": ["art_42", "art_7"]}). Options can be inlined as below, or loaded from a
backend with <zn-data-select multiple>.
<zn-page-builder heading="KB Category Page" style="height: 420px"> <template type="article-list" slot="config" label="Article List" icon="list" description="Shows a chosen set of KB articles"> <zn-select multiple name="articles" label="Articles to show"> <zn-option value="art_1">How to reset your password</zn-option> <zn-option value="art_2">Billing FAQ</zn-option> <zn-option value="art_3">Getting started guide</zn-option> </zn-select> </template> </zn-page-builder>
Importing
If you’re using the autoloader or the traditional loader, you can ignore this section. Otherwise, feel free to use any of the following snippets to cherry pick this component.
To import this component from the CDN using a script tag:
<script type="module" src="https://cdn.jsdelivr.net/npm/@kubex/zinc@1.1.98/dist/components/page-builder/page-builder.js"></script>
To import this component from the CDN using a JavaScript import:
import 'https://cdn.jsdelivr.net/npm/@kubex/zinc@1.1.98/dist/components/page-builder/page-builder.js';
To import this component using a bundler:
import '@kubex/zinc/dist/components/page-builder/page-builder.js';
Slots
| Name | Description |
|---|---|
config
|
<template type="…"> declarations; never displayed. Each template’s attributes
(type, label, icon, icon-library, color, category, description, slots, accepts) declare a palette
entry and its content declares the inspector form for that type. slots makes the
section a container with that many child slots; accepts is a comma-separated list of
the type keys its slots allow.
|
header-left
|
Actions shown on the left of the header bar. |
header-right
|
Actions shown on the right of the header bar. |
Learn more about using slots.
Properties
| Name | Description | Reflects | Type | Default |
|---|---|---|---|---|
name
|
The name of the control, submitted as a name/value pair with form data. |
string
|
''
|
|
form
|
Associates the control with a form by id. The form must be in the same document or shadow root. |
|
string
|
''
|
config
|
The page state as a JSON string. Parsed on set; invalid JSON is ignored with a warning. |
string
|
''
|
|
requiredFirst
required-first
|
Section type key that must lead the page. The builder hoists an existing section of that type to the top, or inserts an empty one, and pins it there: it can’t be removed, reordered or dragged into a slot, and nothing can be dropped above it. Its content stays fully editable in the inspector. |
|
string
|
''
|
sectionTypes
|
Section types to make available, registered into the internal registry. |
PageSectionType[]
|
[]
|
|
paletteCollapsed
palette-collapsed
|
Collapses the left palette. Auto-set when the builder becomes narrow. |
|
boolean
|
false
|
inspectorCollapsed
inspector-collapsed
|
Collapses the inspector while a section is selected. |
|
boolean
|
false
|
autoSave
auto-save
|
Auto-save the page to localStorage (1-day TTL). Omit to disable. A bare
auto-save saves every 5 minutes; a numeric value sets the interval in minutes
(auto-save="2"). Restore with restoreAutoSave().
|
number | null
|
null
|
|
state
|
Replaces the page state wholesale (does not emit zn-page-change). |
PageState
|
- | |
value
|
Replaces the page state from a JSON string (does not emit zn-page-change). |
string
|
- | |
defaultValue
|
The serialised state of the last externally loaded page — restored on form reset. | - | - | |
validity
|
Gets the validity state object. |
ValidityState
|
- | |
validationMessage
|
Gets the validation message. |
string
|
- | |
restoreAutoSave
|
Load the auto-saved page, if one exists within the 1-day TTL. | - | - | |
updateComplete |
A read-only promise that resolves when the component has finished updating. |
Learn more about attributes and properties.
Events
| Name | React Event | Description | Event Detail |
|---|---|---|---|
zn-page-change |
|
Emitted whenever the page state changes. event.detail.state is the new PageState.
|
- |
zn-page-selection-change |
|
Emitted when the selected section changes. event.detail.sectionId. |
- |
Learn more about events.
Methods
| Name | Description | Arguments |
|---|---|---|
checkValidity() |
Checks for validity but does not show a validation message. | - |
getForm() |
Gets the associated form, if one exists. | - |
reportValidity() |
Checks for validity and shows the browser’s validation message if the control is invalid. | - |
setCustomValidity() |
Sets a custom validation message. Pass an empty string to restore validity. |
_message:
|
addSection() |
Adds a section of a registered type at index (default: end). Returns null for unknown
types.
|
type: string, index: number
|
addSectionToSlot() |
Adds a new section of a registered type into a container’s slot. Returns null if not allowed. |
type: string, containerId: string, slotIndex: number
|
Learn more about methods.
Parts
| Name | Description |
|---|---|
base |
The grid wrapper. |
header |
The full-width header action bar (only rendered when header slots are filled). |
palette |
The left palette panel. |
canvas |
The centre section-card canvas. |
inspector |
The right panel while a section is selected. |
inspector-header |
The inspector’s fixed header (icon, section name, type, close). |
inspector-body |
The inspector’s scrolling form area. |
Learn more about customizing CSS parts.
Dependencies
This component automatically imports the following dependencies.
<zn-collapsible><zn-example><zn-icon><zn-input><zn-page-palette-item><zn-page-section-card><zn-slash-item><zn-slash-menu><zn-tooltip>