Tutorial: Using the Page Builder
The Page Builder is a full-screen visual layout editor that lets you compose page content using containers and widgets, nested at any depth. It runs inside the WordPress post/page edit screen and saves everything to the sanilwb_data post meta field.
This tutorial walks through building a page from scratch — adding containers, placing widgets, configuring them, and saving. It also explains how drag-and-drop reordering works and how to use the history panel for undo/redo.
Opening the Page Builder
Navigate to any WordPress post or page in the admin. Below the Gutenberg editor, find the Sanil Page Builder meta box. Click Open Page Builder to launch the full-screen editor.
Understanding the UI
The editor is divided into three areas:
| Area | Location | Purpose |
|---|---|---|
| Topbar | Top bar across the full width | Menu (☰: Show Layers, Show History, Exit) + post title on the left; device toggle in the center; Canvas Settings, Preview, and Save buttons on the right |
| Layers Panel | Left sidebar | All create, delete, reorder, and settings actions |
| Canvas | Right side, fills remaining space | Live visual preview of the current layout |
The canvas is read-only for editing. Clicking a widget in the canvas highlights its bar in the Layers Panel and scrolls to it, but all actual editing happens in the Layers Panel. This separation keeps the canvas a clean preview, not an interactive editor.
The Layer Hierarchy
There is only one structural building block: the Container. A container can hold widgets, further containers, or a mix of both, in any order, at any depth — there is no fixed number of levels and no separate row/column concept.
Container
└── Widget (a content block)
OR
└── Container (another container nested inside, holding more widgets/containers)
A container with no children shows an empty state inviting you to add its first item. A container's children can be reordered freely, and widgets and nested containers can be mixed in any sequence.
Color coding in the Layers Panel
| Kind | Bar color |
|---|---|
| Container | Violet |
| Widget | Amber |
Every container — root-level or nested any number of levels deep — shares the same violet color; only the leaf widget kind gets its own (amber).
Step 1 — Add Your First Container
When the builder opens on an empty page, the Layers Panel shows a placeholder ("No containers yet — use the canvas to add your first container"). The canvas itself always has an Add Container button pinned at the bottom. Click it to add the first container.
The container's bar (violet) immediately appears in the Layers Panel. The canvas updates to show a blank container outline.
To add more root-level containers later: use the same Add Container button at the bottom of the canvas, or right-click (or use the ⋮ menu on) an existing container's bar in the Layers Panel and choose Add Container / Add Container Below.
Step 2 — Configure the Container's Layout
Click a container's bar to open its settings panel. The Style tab's Layout section controls how its children are arranged — this replaces the old fixed column-split templates with plain flexbox controls:
| Field | What it controls |
|---|---|
| Layout | Flex, Block, Inline Block, Inline, or the default |
| Direction | Horizontal (row) or Vertical (column) |
| Align Items | Cross-axis alignment (Start / Center / End / Stretch) |
| Justify Content | Main-axis alignment (Start / Center / End / Space Between / Space Around) |
| Flex Wrap | Whether children wrap onto new lines |
| Align Content | Alignment of wrapped lines |
| Gap | Spacing between children |
These fields are responsive (desktop/tablet/mobile), so a container can lay its children out horizontally on desktop and stack them vertically on mobile.
Step 3 — Add a Widget to a Container
Hover over a container's contents in the canvas and click the + button that appears (or use the Add Widget action on the container's bar in the Layers Panel). Both open the same Widget Picker dialog, which lists a Container card first, followed by every widget type.
Available widget types:
| Widget | Label | What it does |
|---|---|---|
button |
Button | A call-to-action button with an optional URL |
heading |
Heading | A heading text block |
paragraph |
Paragraph | A rich-text paragraph (WYSIWYG editor) |
image |
Image | A single image with an optional link |
icon |
Icon | A Lucide icon, with an optional link URL |
post_title |
Post Title | The current post's title (reflects whichever post is in context) |
archive_title |
Archive Title | The current archive's title |
featured_image |
Featured Image | The current post's featured image |
post_content |
Post Content | The current post's main content |
excerpt |
Excerpt | The current post's excerpt |
shortcodes |
Shortcodes | Raw shortcode/embed code passthrough |
template_reference |
Dynamic Template | A live pointer to a saved template — see Step 12 below |
Click the widget type to add it. The widget is inserted into the container and its Settings Dialog opens immediately so you can configure it before returning to the canvas.
Adding a container instead. Picking the Container card from the same dialog inserts a new (empty) container at that position instead of a widget — its own settings panel does not open automatically.
Inserting an item between two existing widgets. Hovering over any widget in the canvas preview reveals its own + button at that widget's bottom edge. Clicking it opens the same Widget Picker dialog, but the new widget or container is inserted directly after the one you hovered — instead of at the bottom of the container.
Step 4 — Configure a Widget
Every widget dialog follows the same overall shape — a Content tab specific to that widget type, then a shared Style tab, then a shared Advanced tab. Adding a Heading widget (from Step 3's Widget Picker) opens a dialog with these tabs:
Content tab
| Field | What it controls |
|---|---|
| Tag | Which HTML heading tag to render (h1–h6) |
| Text | The heading's own text |
| Link URL | Optional — wraps the heading in a link |
A dynamic widget's Content tab looks different — Post Title, for example, has a Tag and a link-to-post toggle but no manual Text field, since its content always comes from the current post. See the Built-in widget types table for what each widget's own Content tab actually contains.
Style tab
| Field | What it controls |
|---|---|
| Layout, Size, Typography, Appearance, Spacing, Position | The shared style sections every widget gets — see SHARED_STYLE_FIELDS for the full field list |
Advanced tab
| Field | What it controls |
|---|---|
| Show on Desktop | Toggle visibility at desktop viewport |
| Show on Tablet | Toggle visibility at tablet viewport |
| Show on Mobile | Toggle visibility at mobile viewport |
| Admin Label | Custom name shown on this widget's layer bar (overrides "Heading 1") |
| CSS Class | Custom class added to the widget's wrapper element |
Click Save when done. The canvas updates to show the rendered widget.
Step 5 — Add More Widgets and Containers
To add more items to a container, hover in the canvas and use the + button, or click the container's bar in the Layers Panel and use its Add Widget / Add Container actions. New items are appended at the bottom of the container's children unless you used a widget's own trailing + button, in which case they land right after it. Drag any item's bar to reorder it.
Step 6 — Nesting Containers
Because a container can hold further containers at any depth, there is no separate "nested row" concept — building a more complex layout inside another container is done the same way as adding your first one.
To nest a container:
- Open the Widget Picker for the outer container (via the
+button in the canvas, or its Add Widget action) and pick the Container card, or use the Add Container action directly on the outer container's bar. - The new container appears as a child, empty, ready for its own widgets or further containers.
- Configure its Layout section (Step 2) to control how its own children are arranged.
There is no depth limit. Containers can be nested inside containers inside containers, as deep as the layout requires.
Widgets and containers can be freely mixed in any order, in any container. Use the drag handle on any item bar to reorder them.
Step 7 — Configure Container Settings
Click a container's bar in the Layers Panel to open its settings panel directly — there is no separate gear/settings button.
| Tab | Fields |
|---|---|
| Style | Layout, Size, Appearance, Spacing, Position (the same shared style sections every widget gets, minus Typography — a container never renders text of its own) |
| Loop | Repeat this container once per matched post/term/user — see below. Only shown on a regular page, not while editing a template. |
| Advanced | Visibility toggles (desktop/tablet/mobile), Admin Label, CSS Class |
Container settings are useful for adding background colors, borders, or spacing to a section of the page, on top of controlling how its children lay out.
Loop — repeat a container for every matching post, term, or user
Turning on Enable Loop turns the container into a repeating unit: instead of showing once, it repeats once per item matched by a query you configure — each repeat is a full, independent, editable copy of everything inside the container (any widgets, and any further nested containers).
| Field | What it does |
|---|---|
| Context | New Query — build your own query below. Current Query — reuse this page's own main query (e.g. the posts an archive page is already showing) instead of a separate one. |
| Query Type | (New Query only) What a "match" is: Post Type, Terms (categories/tags), or Users. |
| Post Type | (Post Type queries) Which post type to pull matches from. |
| Terms | (Post Type queries: filters to posts having any of the picked terms. Terms queries: the terms themselves are the loop items.) |
| Meta Query | (Post Type queries) Filter matches by custom field value. |
| Role | (Users queries) Filter matches by user role. |
| Total Items | Maximum number of repeats. |
| Offset | Skip this many matches from the start before repeating. |
| Order | Sort direction for the matches. |
| Exclude Current Post | (Post Type queries) Leave out the post currently being viewed/edited, so a "related posts" loop doesn't show the same post you're already on. |
Things to know:
- A loop with zero matches (or still loading) shows one dimmed placeholder copy in the editor so you can still edit its widgets — it disappears once real matches exist on the live page.
- Loops can't be nested. A container that's already inside a Loop-enabled container never shows its own Loop tab.
- Loop is only available on regular pages — not while editing a template, since a template is reused across many different pages and a query saved into one wouldn't fit every page it's used on.
Step 8 — Reorder Elements by Dragging
Both containers and widgets can be reordered by dragging their layer bars. To start a drag:
- Click and hold on any layer bar.
- Move the cursor at least 6 pixels before the drag activates. This small threshold prevents accidental drags when you intend to click.
- A Drag Overlay (a lightweight clone of the bar) follows the cursor. The original bar dims in place.
- Drop onto the target position.
What can be dragged where: any container or widget can be moved to become a child of any other container — or of the page root — at any depth, reparenting freely between siblings, into a different container, or out to the root. The one restriction: you cannot drop a container inside itself or inside one of its own descendants. Attempting that shows a brief rejection message in the Layers Panel and the drop is ignored.
Step 9 — Undo and Redo
Every action that changes the layout (add, remove, move, save widget/container settings) creates an undo snapshot before applying the change. The undo stack holds up to 50 snapshots.
| Keyboard shortcut | Action |
|---|---|
Ctrl+Z (or Cmd+Z) |
Undo the last action |
Ctrl+Y (or Cmd+Y) |
Redo (restore the undone action) |
Ctrl+Shift+Z (or Cmd+Shift+Z) |
Redo (alternative shortcut) |
Shortcuts are ignored when your cursor is inside a text input, textarea, select element, or any contentEditable area — so you can type normally in widget dialogs without accidentally triggering undo.
History Panel
Click the History button in the Layers Panel header to switch from the Layers Panel to the History Panel. The History Panel shows a flat timeline of all actions with the most recent at the top.
- The blue highlight shows the current state.
- Click any entry to jump directly to that point in history (
jumpToHistory()runs undo or redo in a loop to reach the target index). - Entries above the highlight are redoable (future actions you have undone). Entries below are the recorded past.
- The timeline never reorders — undo/redo only moves the highlight cursor through the fixed list.
Step 10 — Device Preview
The Device Toggle in the topbar switches the canvas between three simulated viewport widths.
| Device | Canvas width |
|---|---|
| Desktop | Full width (100%) |
| Tablet | 768 px |
| Mobile | 375 px |
Width transitions animate with a 0.25-second ease. On tablet and mobile, the canvas is horizontally centered.
The device state is shared between the topbar toggle and the widget/container settings dialog's device toggle. Switching one switches both — this is what drives which responsive value (desktop/tablet/mobile) a field's editor shows and saves to.
Step 11 — Save
Click Save in the topbar. The button has three visible states:
| State | What it means |
|---|---|
| "Save" | Idle — ready to save. The button stays enabled even with no unsaved changes; clicking it simply re-saves the current state |
| "Saving…" | AJAX request is in flight — a mutex prevents a second click from firing, and the button is disabled |
| "Saved" (1.5 seconds) | Request succeeded — the button is briefly disabled, then returns to "Save" |
Closing without saving: If you choose Exit from the topbar's menu (☰) — or navigate away — while there are unsaved changes, the builder shows a confirmation dialog before leaving. Confirming discards all unsaved changes.
Step 12 — Reusing a Container as a Dynamic Template
If you've built a container you want to reuse elsewhere — a card layout, a call-to-out section, a repeating item inside a Loop — you don't have to rebuild it by hand each time. Right-click (or use the ⋮ menu on) that container's bar in the Layers Panel and choose Save as Template.
A panel opens with:
| Field | What it does |
|---|---|
| Name | The new template's name |
| Category | Which category to file it under |
| Replace this container with a reference to the new template | Checked by default |
Clicking Save creates a brand-new template from that container's own contents. With the checkbox left checked (the default), the container on your page is immediately swapped for a Dynamic Template widget pointing at the template you just created — the page stays live-linked to it, so editing the template later updates every place it's used, including this one. Unchecking the box just saves a copy of the template without touching your page, for the rarer case where you want a one-off starting point rather than a live link.
Editing the template's design afterward. A Dynamic Template widget's Content tab has a Template section with the picker and an Edit Template link that appears once a template is picked — clicking it opens that template in its own separate editor, in a new browser tab. There is no inline editing of the template's layout/design inside the page's own editor session (colors, spacing, structure); you edit those in the template's own tab, save it there, and the page (and every other place that references it) picks up the change automatically the next time it's viewed — no re-save needed on the page itself.
Overriding just the text per placement. Below the Template section, an Overrides section lists every Heading, Paragraph, and Button found inside the picked template, each with its own Text field (and a URL field, for Heading/Button) — this is where per-instance editing does happen, right in the page's own editor session. Leave a field blank and it keeps showing whatever the template itself has; type something in and it overrides that one field for this placement only, without touching the template or any other placement of it. This is the piece that makes reusing one designed block practical — e.g. a template used for 10 different homepage sections, each with its own heading and "read more" link, all sharing the exact same look, with the look itself still fully editable in one place (the template) whenever it needs to change.
Adding a Dynamic Template by hand. You don't need to go through "Save as Template" every time — the Widget Picker's Dynamic Template card (in the Dynamic group) lets you add one pointing at any existing template directly, the same way you'd add any other widget.
Common Mistakes
The canvas looks empty after adding a Dynamic Template widget
Open the widget settings and select a template in the Content tab's Template picker. Without a template picked, there is nothing to reference and the canvas shows nothing.
I want to add a widget above or below a nested container
Use the container's own + button or Add Widget action — the widget is appended to the end of that container's children. To reorder it above or below a nested container, drag its layer bar to the desired position.
I can't drag a container into a different parent container
Make sure the target container is open (expanded) in the Layers Panel so its drop zone is visible. If the drag is silently rejected, you are likely trying to drop a container inside itself or inside one of its own descendants — that's the one move the builder blocks.
The canvas doesn't update after I change widget settings
The canvas reflects the last saved state of the widget. Click Save inside the widget settings dialog. The canvas then re-renders with the new values.
Widget layer bars show names like "Heading 1", "Heading 2"
These are auto-generated labels based on the widget type and count. To give a widget a meaningful name, open its settings dialog, go to the Advanced tab, and fill in the Admin Label field.
A Dynamic Template's Overrides list doesn't show a widget I just added to the template
The Overrides section fetches the template's structure once and keeps it for the rest of the editing session. If you add/rename/remove a Heading, Paragraph, or Button via the Edit Template link and come back to this same page without reloading it, the Overrides list still reflects the template as it was before that edit. Reload the page to see the current list — this is a known rough edge, not something you're doing wrong.