Width & Breakout
"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.
| Token | Resolves to | Use it for |
|---|---|---|
article / prose | prose track (reading measure) | body copy, the reading column |
content | content band | full inner content width |
breakout-md | breakout-md track | a touch wider than reading |
breakout-lg | breakout-lg track | content width, wider band |
full-bleed | 1 / -1 | edge-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.
| Value | Meaning | Placement |
|---|---|---|
1 | Full bleed | 1 / -1 (static) |
2 | Content band | 2 / -2 (static) |
3–7 | Wide → reading-column inset | responsive; the chosen inset at lg+, collapses to the content band below lg |
marginalia-left / -right | Margin tracks | responsive 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 toarticle).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
columnSpangrid-column string, or lean on the site'sdefaultColumnSpan. Don't reach for the breakout vocabulary you won't use. - Editor should choose a semantic width → give the block a
breakoutWidthselect with the named tokens and wrap it inwithBlockPlacement. Authors pick "reading column" or "full bleed," not a number. - Block needs raw, granular inset control → give the
breakoutWidthselect the ladder options instead. - A band that must always be edge-to-edge → set
pinnedBandin 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
contentWidthaxis; it only works inside the passthrough.
Rules to respect
- Blocks own no outer width. Let
withBlockPlacement(or the top-levelcolumnSpan) decide placement. A block that sets its ownmax-widthor 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.