Width presets
Every block has a width setting with one of four values, defined in Block::WIDTHS (src/Contracts/Block.php). The three explicit presets, narrow, wide and bleed, are fixed and resolve to the same width on every layout. normal is the only preset a layout may change.
| Preset | Label in the block's Width field | Class emitted | Resolves to |
|---|---|---|---|
| normal | Normal β whatever this page template calls normal | None | --cms-width-normal, declared on .cms-block itself, unless the layout changes it |
| narrow | Narrow β a reading measure, even on a wide template | .cms-block--narrow | --cms-width-narrow, a reading measure, on every layout |
| wide | Wide β roomier, still contained | .cms-block--wide | --cms-width-wide |
| bleed | Full bleed β edge to edge, out of the column entirely | .cms-block--bleed | The full viewport width, outside the content column |
Three layouts change normal: wide changes it to --cms-width-wide, and home and docs remove the cap entirely. On every other layout, normal is --cms-width-normal.
There is no full preset. full was documented as a value but never existed in Block::WIDTHS. Block::cast() converts a stored full to normal when the block is read.
The cascade
One custom property, --cms-block-max, is set in three places in resources/css/cms.css. The browser resolves it by the cascade, not by selector specificity: a value declared on an element always takes precedence over a value the element would otherwise inherit, whatever the weights of the selectors involved.
/* 1. The three measurements. The only place to change what they mean. */
@theme {
--cms-width-narrow: 42rem; /* Tailwind 2xl */
--cms-width-normal: 56rem; /* Tailwind 4xl */
--cms-width-wide: 72rem; /* Tailwind 6xl */
}
/* 2. The block's own default, which is what `normal` resolves to. It comes
first: it has specificity (0,1,0), the same as each preset below, so the
presets take precedence by source order and an explicit narrow stays
narrow. */
.cms-block { --cms-block-max: var(--cms-width-normal); }
/* 3. A preset on the block. The package never emits `--normal`; it is reached
through a block's CSS classes field, to pin a block to the normal
measure on one of the layouts in rule 4. */
.cms-block--narrow { --cms-block-max: var(--cms-width-narrow); }
.cms-block--normal { --cms-block-max: var(--cms-width-normal); }
.cms-block--wide { --cms-block-max: var(--cms-width-wide); }
/* 4. The three layouts that change what `normal` means. These rules have
specificity (0,2,0), which outweighs the block's own default, and they
apply only to blocks left on normal. */
.cms-main--wide .cms-block:not(.cms-block--narrow, .cms-block--normal, .cms-block--wide, .cms-block--bleed, .cms-block--hero) {
--cms-block-max: var(--cms-width-wide);
}
.cms-main--home .cms-block:not(.cms-block--narrow, .cms-block--normal, .cms-block--wide, .cms-block--bleed, .cms-block--hero),
.cms-main--docs .cms-block:not(.cms-block--narrow, .cms-block--normal, .cms-block--wide, .cms-block--bleed, .cms-block--hero) {
--cms-block-max: none;
}
/* Read in one place only. */
.cms-block { max-width: var(--cms-block-max); }
Rule 4 is the reason wide, home and docs change block width and the other layouts do not. It is also the only form of rule that works. Setting --cms-block-max on .cms-main appears equivalent and has no effect, because every .cms-block declares the property on itself, and inheritance never overrides a declared value. The wide layout was originally written that way, and for five releases it rendered blocks at the normal measure, so choosing the wide template produced no visible change. A custom layout that changes what normal means must target the block, never the container.
.cms-block--hero appears in each exclusion list because the hero removes its own cap with a (0,1,0) rule, .cms-block--hero { --cms-block-max: none; }, and rule 4 would otherwise outweigh it. Without the exclusion, a hero on the wide layout would be capped at 72rem.
Measured widths at a 1280px viewport:
| Layout | normal | narrow | wide | bleed |
|---|---|---|---|---|
| default, article (cms-main--reading) | 896px | 672px | 1152px | No cap |
| wide | 1152px | 672px | 1152px | No cap |
| home, docs | No cap | 672px | 1152px | No cap |
Why normal does not inherit from the layout
normal previously inherited its width from the page template, on the basis that the template should define what normal means. That design merged presets: on the reading layout normal resolved to the same width as narrow, and on the wide layout normal resolved to the same width as wide. The editor offered four options that produced three distinct results, so the control misdescribed its own behavior.
The layouts that do need to change normal now declare it on the block, in rule 4 above, and exclude the three explicit presets. narrow is therefore 672px on every layout and wide is 1152px on every layout. Only normal varies, and its label in the block's Width field says so: "Normal β whatever this page template calls normal".
Why normal emits no class
Block::attributesFrom() emits no class for normal. The html and snippet blocks render without a wrapper element unless $cmsAttrs carries something, so a class for normal would have added a <div> wrapper to every raw embed that previously had none. The default is declared on .cms-block in CSS instead, and those two blocks continue to render with no wrapper element unless the editor sets an option explicitly.
Full bleed
.cms-block--bleed {
--cms-block-max: none;
max-width: none;
width: 100vw;
margin-inline: calc(50% - 50vw);
}
:root:has(.cms-block--bleed),
:root:has(.cms-block--section),
.cms-body:has(.cms-block--bleed),
.cms-body:has(.cms-block--section) { overflow-x: clip; }
The section block bleeds with the same geometry, so it is in the same clip list. A new block that bleeds must be added to that list. Three parts of these rules are required:
- The
overflow-x: cliprule.100vwincludes the width of the vertical scrollbar. Without the clip, the page scrolls sideways by the scrollbar's width. Measured at a 1280px viewport, a 1280px-wide bleed block starts at -7px and the whole document scrolls horizontally. clipinstead ofhidden.overflow-x: hiddenmakes the element a scroll container, and a scroll container stops everyposition: stickydescendant from sticking, including the site header.- The
--cms-block-max: nonereset on the bleeding block. The hero resets it for the same reason. Without the reset, a block nested inside the bleeding block (for example, the blocks a snippet embeds) inherits the layout's cap and centers itself, so the wrapper spans the viewport while its visible content stays at the column width.
Changing the measurements
To change a measurement site-wide or for one skin, override the variable:
:root { --cms-width-wide: 80rem; }
[data-cms-skin='press'] { --cms-width-narrow: 38rem; }
To give a custom layout its own value for normal, target the block, as explained under The cascade:
.cms-main--gallery .cms-block:not(.cms-block--narrow, .cms-block--normal, .cms-block--wide, .cms-block--bleed, .cms-block--hero) {
--cms-block-max: var(--cms-width-wide);
}
To pin one block back to the normal measure on such a layout, add cms-block--normal to the block's CSS classes field in the block editor. The stylesheet defines that class for this purpose only; no markup the package renders emits it.
To exempt a single block type from the cap, set the property to none on its class. The hero does this so that it fills the width of its container:
.cms-block--hero { --cms-block-max: none; }
Width in block views
A block view needs only to merge $cmsAttrs into its root element to take part in the width system. Do not set max-width in a block's own CSS rule. If a block needs a different cap, set --cms-block-max and let the .cms-block rule apply it. A block that sets max-width directly is outside the width system, and the editor's Width setting has no effect on it, with no error.
Block::cast() migrates older stored shapes when a block is read, instead of a database migration rewriting them. Block data is JSON stored inside immutable versions, and a migration would have to rewrite every version ever saved. The migrations are:
- A
containerkey is removed. Any value other thandefaultis moved towidth. - A stored
widthoffullbecomesnormal. - A missing or unrecognized
widthbecomesnormal.