Width & Breakout

  1. Why this page exists
  2. One grid, named lines
  3. The base: columnSpan
  4. Two width surfaces
  5. Full-bleed & contained bands
  6. The central applier
  7. Which control, when
  8. Rules to respect

"How wide is this block?" is the most-touched decision in a Tome page, and it is deliberately one system. That system grew by absorbing three earlier lineages into a single resolver, so it carries more vocabulary than any one site needs. This page is the map: what the vocabulary is, which part to reach for, and the standing rule that keeps it from growing again.

The load-bearing policy: the width surface is frozen. No new width vocabulary gets added without deprecating something old. If a block seems to need a new width word, it almost certainly maps onto one of the values below.

One grid, named lines

Every top-level block is placed on the page grid by its grid-column. The grid exposes both raw line numbers and named lines (prose-start / prose-end, content-start / content-end,breakout-md-start / breakout-md-end, and so on). Named lines are responsive and route-configurable — the same token resolves to a different span at different breakpoints, driven by the grid's custom properties. Nothing here is a pixel cap; widths are always positions on this grid.

The base: columnSpan

At the simplest level, RenderBlocks places each top-level block using a raw columnSpan — a literal CSS grid-column string on the block, falling back to the site's defaultColumnSpan when the block sets none:

<RenderBlocks blocks={page.layout} components={blockComponents} defaultColumnSpan="2 / -2" />

Common raw values:

  • 2 / -2 — respects the site gutter columns (the usual default).
  • 1 / -1 — full bleed, covers the gutters, edge to edge.
  • 3 / 11 — magazine asymmetry (half-width, left-aligned).
  • 11 / -3 — magazine asymmetry (half-width, right-aligned).

This is enough for a plain content site. The richer vocabulary below exists for blocks that want an editor-facing width control instead of a raw grid string.

Two width surfaces

The breakout resolver in @wabbit/tome-ui accepts two coexisting vocabularies on the same grid. Neither is an alias of the other — they resolve independently and are both first-class, so nothing has to migrate when a site prefers one over the other.

Named tokens — opinionated, semantic. Pick these when you mean "the reading column" or "edge to edge," not a specific inset number.

TokenResolves toUse it for
article / proseprose track (reading measure)body copy, the reading column
contentcontent bandfull inner content width
breakout-mdbreakout-md tracka touch wider than reading
breakout-lgbreakout-lg trackcontent width, wider band
full-bleed1 / -1edge-to-edge bands

The numeric ladder — unopinionated, granular. Symmetric insets with no editorial meaning: 4 is "symmetric inset 4," nothing more. Pick these when a block wants raw, predictable grid control.

ValueMeaningPlacement
1Full bleed1 / -1 (static)
2Content band2 / -2 (static)
3–7Wide → reading-column insetresponsive; the chosen inset at lg+, collapses to the content band below lg
marginalia-left / -rightMargin tracksresponsive marginalia placement

The two surfaces are allowed to differ. At lg, the named breakout-md resolves to roughly 5 / -5 (the reading track), while the ladder's 4 resolves to 4 / -4. That's intentional: names track the semantic grid, numbers track raw insets.

Full-bleed & contained bands

A contained band sits inside the gutters — anything from the prose track out to the content band. A full-bleed band (full-bleed / ladder 1 / 1 / -1) covers the whole viewport width.

Full-bleed is special: the resolver turns a full-bleed block into a subgrid passthrough, re-exposing the page's named lines to the block's own inner content. Only then does the inner content width axis become meaningful — a separate control (contentWidth) that places the inner content on the inset lines via--tome-content-cols. Outside a full-bleed passthrough those inset lines don't exist, so the inner axis is inert. Inner content never touches the true edge; the narrowest inner value clamps to the content band.

The central applier

Blocks don't set their own outer width. Placement is decided in one place — the withBlockPlacement helper in @wabbit/tome-ui — which wraps a block component and reads the editor-set chrome values (breakoutWidth, contentWidth, padding, text size), resolves them against the block's policy, and emits the grid placement:

import { withBlockPlacement } from '@wabbit/tome-ui'

// in your RenderBlocks component map:
someBlock: withBlockPlacement(SomeBlock, { defaultBreakout: 'content' })

The policy the consumer passes carries three levers:

  • defaultBreakout — the fallback width when the editor set none (defaults to article).
  • pinnedBand — force full-bleed regardless of the stored value; a pinned band can't be narrowed into collapsing its subgrid root.
  • aliasMode: 'ladder' — collapse named tokens to their ladder equivalents before resolving. This exists only for a site whose stored data predates the union (so its named tokens should render the old numeric way). Leave it off unless you are that site — the default keeps names as first-class named lines with zero migration.

Which control, when

  • Plain content site, one-off placement → set the raw columnSpan grid-column string, or lean on the site's defaultColumnSpan. Don't reach for the breakout vocabulary you won't use.
  • Editor should choose a semantic width → give the block a breakoutWidth select with the named tokens and wrap it in withBlockPlacement. Authors pick "reading column" or "full bleed," not a number.
  • Block needs raw, granular inset control → give the breakoutWidth select the ladder options instead.
  • A band that must always be edge-to-edge → set pinnedBand in the block's policy; don't rely on the editor leaving it on full-bleed.
  • Placing content inside a full-bleed band → use the inner contentWidth axis; it only works inside the passthrough.

Rules to respect

  • Blocks own no outer width. Let withBlockPlacement (or the top-level columnSpan) decide placement. A block that sets its own max-width or margins fights the grid.
  • Don't invent new width words. The surface is frozen. A new need maps onto an existing token or ladder value; if it genuinely doesn't, that's a platform conversation about deprecating something, not a local addition.
  • Pick a lane per block, not per instance. Named tokens or the ladder — choose one vocabulary for a given block's control so authors aren't choosing between two overlapping scales.
  • Never migrate stored values. Names and numbers coexist by design; changing a block's option list doesn't require rewriting stored content. If you must reproduce a pre-union site's old rendering, that's exactly what aliasMode: 'ladder' is for.

New to the block pipeline itself? Start with Get Started.