Page spread in CSS print

CSS Page Spread Module

This document is a draft specification for the declaration of spread and full-page elements in paged media. This specification was written as part of the Pushing Forward for CSS Print project, funded through the NGI0 Commons Fund, established by NLnet. It's a joint initiative from core contributors of Paged.js (@julientaq, @JulieBlanc) and the WeasyPrint team (@grewn0uille, @liZe). The result of this work can be found on CSS Print Lab's GitHub.

This draft addresses:

Introduction

In print publishing, a spread is a fundamental unit of design and refers to two facing pages visible simultaneously when a bound document is open. Publishers place photographs, illustrations, tables, maps, or decorative elements across both pages of a spread, or isolate content on a single dedicated page.

These layouts are commonplace in:

Despite how common these layouts are, CSS has no native mechanism to describe them.

This proposal seeks to fill that gap with a minimal but complete set of CSS mechanisms that integrate cleanly with existing paged media specifications.

Types of spread and full-page layouts

The following table distinguishes the main layout types this specification addresses:

Layout type Flow behavior Page sharing
Full-page element Removed from flow Dedicated page, no other flow content
Full-spread element Removed from flow Dedicated spread (two pages), no other flow content
Spread float Stays in flow; content flows before and after Shares two-page spread with surrounding content
Page float (top/bottom) Stays in flow Shares single page with surrounding content (already in CSS Page Floats 3)

Existing W3C Specifications

CSS Paged Media Module Level 3 (css-page-3)

CSS Paged Media 3 defines the @page rule, named pages (via the page property), and the page box model. The page property allows associating an element with a specific named page type:

figure { page: full-bleed; }
@page full-bleed { margin: 0; }

However, the page property does not remove the element from flow (eventually leaving an unwanted space on the page before the figure), does not isolate it on a dedicated page, and provides no mechanism to span two pages. The concept of a spread is entirely absent from this specification.

CSS Generated Content for Paged Media Module (css-gcpm-3)

css-gcpm-3 introduces position: running(), which removes elements from the flow for placement in page margin boxes (running headers, footers, etc.). This mechanism handles repeated content; it has no concept of placed content on a dedicated page or spread.

CSS Page Floats Module Level 3 (css-page-floats-3)

CSS Page Floats 3 extends the float property with new values (top, bottom, inline-start, inline-end, etc.) and introduces float-reference (inline | column | region | page). This allows elements to float to the top or bottom of a page while remaining in the flow.

This specification is the closest existing mechanism to what we need. However, it does not:

Gap in the specifications

No current W3C specification defines:

  1. The spread as a CSS layout unit;
  2. A mechanism to place an element on a dedicated full page;
  3. A mechanism to spread an element across two facing pages;
  4. Gutter/fold compensation for spread elements.

Terminology

The page-placement property

We propose a new page-placement property to remove an element from the normal flow and place it on a specific page position.

The choice to introduce a dedicated property (rather than extending float) is deliberate. The float property implies that the element remains in the flow and affects surrounding content. A full-page element is fundamentally different: it leaves the flow entirely and occupies its own page. Overloading float with this behavior would conflate two semantically distinct mechanisms.

Syntax

Name:   page-placement
Value:  none | page | left | right | spread | <integer>
Value Description
none Default. The element remains in the normal flow.
page The element is removed from the flow and placed on the next available page. A new page is created for it.
left The element is placed on the next available left page.
right The element is placed on the next available right page.
spread The element is placed across the next available spread. Both pages of the spread are dedicated to this element.
<integer> The element is placed on the page corresponding to the given integer (1-indexed from the beginning of the document).

The element is removed from the principal flow at its source position and reinserted into the page box (or spread) of the target page.

ISSUE (recto/verso vs. left/right values): The current values left and right use physical directions, while this specification adopts recto/verso as its primary terminology. Should the values be renamed to recto and verso?

recto/verso are logically direction-aware terms: in RTL documents (Arabic, Hebrew), the recto page is physically on the left, which the left value does not capture correctly. Using recto/verso would therefore be more correct across writing systems.

However, renaming would create an asymmetry with @page :left and @page :right in css-page-3, which this specification references throughout. Options:

Examples

ISSUE: If page 4 already contains flow content, what happens? too complicated to implement ?

Interaction with the page property

page-placement can be combined with the page property from css-page-3 to associate specific @page rules with the target page:

@page full-bleed {
  margin: 0;
}

figure.full-bleed {
  page: full-bleed;
  page-placement: right;
  width: 100%;
  height: 100%;
}

figure.full-bleed img {
  object-fit: cover;
  width: 100%;
  height: 100%;
}

When page-placement: spread is used together with a named page, the named page rules apply to both pages of the spread.

Interaction with the bleed property

CSS Paged Media 3 defines a bleed property on @page that specifies the extent of the bleed area beyond the page box — the region where content can extend past the trim line for print production:

@page {
  bleed: 3mm;
  marks: crop cross;
}

The bleed area is outside the page box. An element declared at width: 100% fills the page content area exactly up to the trim line; it does not extend into the bleed area automatically. To make an element bleed, the author must explicitly shift it outside the page box using negative relative position and a compensated width.

**ISSUE: Est-ce qu’on pourrait se servir de la nouvelle spécification env() pour le bleed ? https://drafts.csswg.org/css-env-1/ Quelque chose comme width: calc(100% + 2 * env(page-bleed));env(page-bleed) est par défaut la valeur déclarée dans bleed.

Interaction with @page :left and @page :right

In CSS Page 3, @page :left and @page :right allow different styles on verso and recto pages. Spread elements need to be aware of this distinction.

When page-placement: spread is used:

The spread element's width resolves against the full spread content area: the combined width of both page content areas plus the two inner margins (the right margin of the left page and the left margin of the right page). The outer margins (the left margin of the left page and the right margin of the right page) remain outside the spread content area and continue to host page margin boxes such as page numbers and running headers.

Page numbers and generated content apply to both pages of the spread when margins are preserved.

ISSUE: If a named @page rule applies to a spread, should the two pages of the spread be able to have different named page types (e.g., @page left and @page right independently), or does the named page type apply uniformly to both? The first option would require a mechanism to declare spread-specific page pairs, and page number placement.

Accessibility

When an element is moved to a different page by page-placement, its logical position in the document (and therefore in the accessibility tree) should remain at its source position. CSS reading-order hints to preserve the correct sequence for screen readers and other assistive technologies.

The @spread at-rule

When an element spans two pages of a spread, whether as a full-spread element (page-placement: spread) or as a spread float (float-reference: spread), the physical binding of the document introduces a constraint: content near the fold may be hidden, compressed, or distorted. This is especially critical for photographs, maps, and other continuous visual content.

Authors need a way to declare a gutter compensation offset: an amount by which content is shifted outward from the fold on each page to preserve readability. We propose to standardize this through a dedicated @spread at-rule.

The @spread at-rule

The @spread at-rule defines spread-level properties. It can be declared at the stylesheet level or nested within a named @page rule. When nested in @page, it applies only to spreads on pages matching that @page rule.

@page{
  @spread {
  	gutter: 10mm;
	}
}
@page chapter {
  @spread {
    gutter: 8mm;
  }
}

The gutter property

Name:   gutter
Value:  <length>
Initial: 0

The gutter property specifies the additional inward offset applied to the content of spread elements to compensate for the physical binding. A non-zero gutter shifts the visible content area of each page away from the fold by the given amount.

On a verso page, the element's content is shifted to the left by the gutter value. On a recto page, the element's content is shifted to the right by the gutter value.

The effect is that the central 2 × gutter strip of the spread, which corresponds to the fold zone, is free of important content.

This property applies to all elements with page-placement: spread or float-reference: spread within the scope of the @spread rule.

ISSUE: The gutter property shifts content, but the element's physical box dimensions remain those of the full spread. Should gutter behave like padding (reducing the content area) or like a rendering offset (keeping the box dimensions unchanged but offsetting what is visible)? For background images and fills, the distinction matters: should the background color fill the full spread width including the fold zone?

ISSUE: Should gutter be a shorthand for gutter-left and gutter-right to allow asymmetric binding compensation? Different binding methods may hide different amounts of content on each side of the fold.

ISSUE: The @spread at-rule implies that the user agent knows, when laying out a document, which pages will form a spread. This is generally known only after pagination. Should @spread declarations be evaluated during a second layout pass? How does this interact with the fragmentation model in CSS Break 4?

[CSS Page Floats 3] The spread float

Beyond full-page and full-spread elements, there is a common and important layout type where an element spans two pages but does not occupy them entirely. A classic example is a large panoramic photograph in a magazine: text flows above and below the image, which spans both the verso and recto pages of the spread at its position.

This is a spread float: an element that spans both pages of a spread while remaining in the document flow. Content flows before and after the element, but not beside it.

This behavior is a natural extension of CSS Page Floats 3: just as float: top with float-reference: page floats an element to the top of a page while content flows below, a spread float should be declarable by extending float-reference with a spread value. We include spread floats here as well, since their interactions with @page overlap with those of the page-placement property.

Extension of float-reference

We propose adding spread as a new value of the float-reference property:

float-reference: inline | column | region | page | spread

When float-reference: spread is used, the element floats relative to the spread it belongs to, rather than a single page. The element spans the full width of the spread content area (i.e., the combined content areas of both pages and their inner margins).

Combined with the existing float: top or float: bottom values from CSS Page Floats 3:

The width property should be interpreted differently when float-reference: spread is used, compared to float-reference: page. A percentage on float-reference: page resolves against one page content area width; on float-reference: spread it should resolve against the spread content area width. When a spread float specifies width: 100%, this width resolves against the full spread content area (the sum of both page content areas and their inner margins). Alternatively, each half of the spread element could be clipped to its respective page content area.

**ISSUE **: Several questions arise:

This issue is partially addressed by the width property, but it is unclear whether overflow is possible in both directions. If an image originally on the right page overflows onto the left page, the layout of the left page must be reconsidered. The fragmentation algorithms must account for the spread as a whole. Does this conflict with the fragmentation algorithms defined in the CSS Breaks module?

Examples

Interaction with the page property → see spread specification

Gutter → see spread specification

Open questions

ISSUE (Module): Should these specifications form a standalone module, or should they be distributed across existing modules?

Risk of conflict if this is an independent module?

ISSUE (Multi-column interaction): How does page-placement interact with multi-column layouts? If a figure inside a multi-column container has page-placement: spread, does it leave the column flow entirely and occupy a full spread? Or should page-placement be restricted to elements in the page-level flow? We think it should be the first option.

ISSUE (Overflow of placed element): What happens when the content of a full-page or full-spread element overflows its target page or spread? Options: (a) the overflow is clipped; (b) the overflow continues onto the next page or spread; (c) a user-agent warning is raised. For photographs, option (a) is usually appropriate; for text-heavy content, option (b) may be preferable. [→trop spécifique ?]