Widgets

Layout Areas

An Area is a small layout container stored inside a zone - it holds widgets in up to four columns, it is not a widget itself, and it cannot hold another Area.

A zone is a list. Lists go downwards. Pages, unfortunately, also go sideways.

An Area is the smallest thing that fixes that: a container stored in the zone, holding a few widgets and one small layout description. It is what you reach for when two widgets belong next to each other, and nothing more than that.

zone
├─ block            a full-width hero
├─ area  2 columns
│  ├─ block         the explanation
│  └─ block         the call to action
└─ block            a closing note

The three-way split

This is the idea the whole feature is built on, and it is worth stating plainly:

Belongs toExample
dataOne block"Ships on Friday", a heading, a link
variantOne block instancegrid, list, compact
Area.layoutThe relationship between blockstwo columns, medium gap, top-aligned

A block never describes the block next to it. That is why columns are not a field, and why Area exists at all.

The stored shape

An Area sits in the zone's array, beside the blocks, and is told apart by its kind:

[
  { "id": "01JC…HD", "type": "core:hero", "data": { "title": "Welcome" } },
  {
    "id": "01JC…KP",
    "kind": "area",
    "layout": { "columns": 2, "gap": "md", "align": "stretch" },
    "children": [
      { "id": "01JC…R2", "type": "core:text", "data": { "heading": "Left" } },
      { "id": "01JC…T7", "type": "core:cta", "data": { "title": "Right" } }
    ]
  }
]
KeyRule
kindAlways "area". It is the only thing that distinguishes an Area from a block, which is why a block may never declare it.
idThe same kind of id a block instance has - stable, unique within the zone, and kept through every move and re-order.
layoutThe tokens below. columns is required; the rest have defaults.
childrenBlocks, in rendered order. Blocks only.

Array order is rendered order, inside an Area exactly as outside one.

The layout tokens

Four closed sets. Not CSS, not class names - tokens VitNode renders, so a stored layout cannot turn into arbitrary styling smuggled through a content column.

TokenValuesDefaultWhat it does
columns1 2 3 42How many columns on a wide screen. Always one column on a phone - see below.
gapnone sm md lgmdThe space between children, in both directions.
alignstart center stretchstretchHow children line up across a row. stretch gives equal-height columns.
justifystart center stretchstretchHow each child sits across its own column. stretch fills it.

Areas are mobile-first: below the md breakpoint every Area is one column, whatever columns says. Two columns on a 360px screen is not a layout, it is a dare.

An Area is not a block

This is the part worth reading twice, because it is what keeps Areas cheap:

  • There is no core:area block. Nothing registers it, nothing can be installed to provide it, and registry.get("core:area") is undefined - on purpose.
  • It needs no entry in allowedBlocks. A zone that allows ["core:text"] still accepts an Area holding core:text. The allowlist governs blocks, and the children inside an Area are checked against the very same list as blocks at the zone's root.
  • It has no data, no fields and no component of its own. There is nothing for a properties form to build, which is why the editor shows four selects instead.

In the editor's block catalogue it therefore lives in its own Layout section above the plugin groups, rather than pretending to be somebody's block.

<ContentZone
  id="settings:sidebar"
  allowedBlocks={['core:text']}
  blocks={content.sidebar}
/>

That zone accepts: core:text at the root, an Area, and core:text inside the Area. It still refuses core:cta in both places.

Areas cannot contain Areas

In Stage 4, an Area's children are blocks. Never another Area.

That is enforced everywhere it can be: the drag-and-drop resolver refuses the drop and says so out loud, the catalogue hides the Layout section while an Area is the insertion target, and the API refuses a nested Area on write. A stored document that somehow contains one is shown as unreadable rather than rendered.

Why one level, when the shape would obviously recurse?

Because every extra level costs more than it looks. Nesting needs depth limits, cycle detection, an answer for what an allowlist means three levels down, a keyboard drag model that can climb out of a container, and a properties panel that can say which area you are editing. One level covers "put these two things side by side", which is what people actually ask for, at a fraction of the surface area.

The shape is ready for it - children is a list of nodes, and a nested area would be one more node kind in it - so the door is closed, not locked.

In the editor

Adding oneThe Layout section of Available Widgets holds a single Area entry. It drops an empty Area into the targeted zone.
Filling itDrag a block onto the Area, or use Add widget on the Area itself - the sidebar then says For: an area in that zone, and the Layout section disappears while it is targeted.
Selecting itClick the Area's own shell (not a child) and the sidebar shows Area properties: Columns, Gap, the space above/below and left/right, Alignment and Distribution. Each control re-lays the page out as you pick it.
UngroupRemoves the Area and leaves its children in the zone, at the position the Area held. Nothing is deleted.
Delete areaRemoves the Area and its children. A non-empty Area asks first, and offers Ungroup, keep widgets in the same breath.

An empty Area renders nothing at all in public - no wrapper, no gap, exactly like an empty zone. In edit mode it is a visible two-column drop target, because now somebody is looking for a place to put something.

Reading a zone that may hold Areas

A zone is a list of content nodes, and a node is a block or an Area. Two helpers keep that from spreading through your code:

import { contentNodeBlocks, isBlockAreaInstance } from '@vitnode/core/widgets'

isBlockAreaInstance(node) // narrows a node to an Area
contentNodeBlocks(nodes) // every block in the zone, areas flattened away

contentNodeBlocks is what you want for anything that cares about blocks and not about arrangement: counting them, indexing their text, checking an allowlist. Rendering is already handled - ContentRenderer walks nodes and lays an Area out for you.

A migration you do not have to run

A zone stored before Areas existed is a list of blocks, and a list of blocks is a list of nodes that never nests. Nothing was rewritten, no column changed, and a page that never uses an Area keeps exactly the markup it had - the Area only exists in the DOM when one is stored.

When an Area is the wrong answer

You wantUse
Two blocks side by sideAn Area. This is the one.
One block that looks differentA variant.
A different arrangement per breakpointNeither, yet. One arrangement, rendered responsively.
A page-wide template - sidebar, footer, headerSeveral zones in your own page markup. An Area arranges content inside a zone; it is not a page layout.
Columns inside columnsNothing. See above, twice.