Styling pages
How CSS attaches to a Layout, how it's scoped, and how to style individual pages.
Each page Format renders is isolated from the others. A Layout's CSS applies only to the pages that Layout produces, Format's base styles load before yours, and every page exposes a host element you target with :host().
How a page is structured
A page is a host element holding that page's content. Format writes data attributes onto the host that describe the page, then fills the page with the Layout's stylesheets and content:
<div class="host" data-layout-id="invoice" data-page-number="1" data-first-page>
#shadow-root (closed)
<!-- the Layout's stylesheets -->
<!-- the Layout's content for this page -->
</div>Target an individual page by pairing the :host() selector with the attributes Format writes per page. The data-layout-id attribute carries the id of the Layout that produced the page, so :host([data-layout-id="invoice"]) styles every page that Layout produces; data-page-number, data-first-page, data-last-page, data-even-page, and data-odd-page narrow to one page or a class of pages. Examples follow below.
Attaching CSS
How CSS gets into the document, and how it's scoped, depends on how you author.
With Studio
You import stylesheets from your document's entry point. Studio bundles them all (CSS, SASS, Linaria, CSS Modules, Tailwind, and the other options under Styling in Studio) and injects the compiled result into every Layout in the document.
import { Document } from '@format.dev/react'
import './styles.css'
export default function InvoiceDocument() {
return (
<Document title="Invoice">
{/* Your content goes here */}
</Document>
)
}<script setup lang="ts">
import { Document } from '@format.dev/vue'
</script>
<style>
@import './styles.css';
</style>
<template>
<Document title="Invoice">
<!-- Your content goes here -->
</Document>
</template>export { default } from './index.html'
import './styles.css'Because the bundle goes into every Layout, your CSS is effectively global across the document: a rule in cover.css applies to every Layout, even if you only intended it for the cover. Class collisions across Layouts are a real concern. Namespace your selectors, use CSS Modules, or scope a rule to one Layout with :host([data-layout-id="invoice"]) .title { ... } where overlap is possible.
Without Studio
When you're authoring the document HTML directly, by hand or from a server template, you control where each stylesheet lands. A <style> block or <link rel="stylesheet"> inside a Layout's <template> applies only to the pages that Layout produces, so per-Layout scoping is automatic. Use <link rel="stylesheet"> when several Layouts share one external file.
<template data-type="layout" data-id="invoice" data-width="793.71px" data-height="1122.52px">
<style>
h1 { font-size: 32px; }
p { line-height: 1.6; }
</style>
<link rel="stylesheet" href="/shared.css" />
<!-- scaffolding... -->
</template>Format resolves a <link rel="stylesheet"> against your assets bundle and inlines its CSS into that Layout's pages. For the wider picture of producing Format documents without Studio, see Building documents.
Document-level global styles
To share styles across every Layout, place a <style> or <link rel="stylesheet"> directly under the <Document>, outside any Layout. Format applies these to every page as global styles, in a cascade layer (@layer format-document) that each Layout's own styles override. They act as document-wide defaults you can still tune per Layout.
<template data-type="document" data-title="Invoice">
<style>
:host { color: #1a1a1a; line-height: 1.5; }
</style>
<template data-type="layout" data-id="cover" data-width="595px" data-height="842px">
<!-- inherits the document-level defaults; can override any of them -->
</template>
<template data-type="layout" data-id="body" data-width="595px" data-height="842px">
<!-- ...same defaults... -->
</template>
</template>Global @font-face
Using @font-face is the one exception to per-Layout scoping. Format applies every font declaration across the document, so a font declared anywhere is available on every page, no matter which Layout rendered it and regardless of how you authored.
The base layer
Before your styles run, every page loads a base layer Format controls. It does roughly what a CSS reset does: normalizes box-sizing, makes images render predictably, and sets sensible typographic defaults on the page. Your own rules sit inside a higher cascade layer (@layer format-layout), so they override the base layer. Document-level global styles sit between the two, in @layer format-document, so a Layout's own styles win over a document-wide default.
The page starts with a white background, an accessible line-height, and Format's font-family variables. Override these in your Layout's CSS for a non-white background or different typography.
For a transparent PDF, set background: transparent on :host.
Targeting a page with :host()
You target the page itself with the :host() functional selector. Pair it with the host's data attributes to style one page differently from the next:
/* applies to every page */
:host { background: lightgrey; }
/* first page only */
:host([data-first-page]) header { display: none; }
/* last page only */
:host([data-last-page]) { background: #fdf8ef; }
/* a single page, by number */
:host([data-page-number="3"]) footer { display: none; }Plain :host is the place for page-level defaults like a background color or a flex layout. Adding a data attribute narrows it: data-first-page for cover-page treatments, data-last-page for finale styling, data-page-number="N" for a single page by number.
Page numbers and page count
Page numbers are the most common per-page need. Place <span data-type="page-number"> and <span data-type="page-count"> in a header or footer, and Format fills them in with the current page and total. All our SDKs provide <PageNumber /> and <PageCount /> components that compile to these. To change the format (Roman numerals, letters, and so on), set --page-counter-style on :host. It accepts any valid CSS <counter-style> name, including custom @counter-style names.
DOM contract
The full set of per-page state Format publishes: the CSS custom properties and data attributes it writes onto each page, plus the base defaults you can override.
CSS custom properties
| Name | Value | Description |
|---|---|---|
| host element of every rendered page | ||
--page-number | unquoted integer, e.g. 1 | The 1-indexed number of the current page. Format's base stylesheet seeds a CSS counter named page-number from this variable. Render the number with <span data-type="page-number"> (or <PageNumber /> in React or Vue). For full control, render the counter yourself with content: counter(page-number) inside a pseudo-element. The counter syntax also supports list-style formatting, e.g. counter(page-number, lower-roman). |
--page-count | unquoted integer, e.g. 12 | The total number of pages in the rendered output. Format's base stylesheet seeds a CSS counter named page-count from this variable. Render the number with <span data-type="page-count"> (or <PageCount /> in React or Vue), typically alongside <PageNumber /> for "Page 3 of 12". For full control, render the counter yourself with content: counter(page-count) inside a pseudo-element. The counter syntax also supports list-style formatting, e.g. counter(page-count, lower-roman). |
Framehost element of every rendered page, once per Frame in the Layout | ||
--frame-<id>-height | CSS px length, e.g. 420px | The measured offsetHeight of the named Frame at the start of page building, before flow filling. Use in Layout CSS that needs to react to a measured dimension, for example sizing a sibling container to match a Frame's height. Published only when the document references a --frame- variable somewhere in its markup or CSS, including externally fetched stylesheets. A document that uses the variable always receives it. Flows that are not inside a Frame overflow against the page itself, whose content area is a fixed size (the page dimensions minus the :host margin), so it has no published variable. |
| host element of every rendered page, once per Flow's overflow Frame | ||
--frame-<id>-sealed-height | CSS px length, e.g. 512px | The measured offsetHeight of a Flow's overflow Frame after flow filling completes. Differs from --frame-<id>-height when the Frame grew to accommodate its Flow's content. Published under the same condition as --frame-<id>-height: the document must reference a --frame- variable. |
Base CSS defaults
Variables Format's base stylesheet initializes with a default. Override on :host from your Layout's CSS to change behavior.
| Name | Value | Description |
|---|---|---|
| host element of every rendered page | ||
--page-counter-style | CSS <counter-style> name, e.g. decimal, lower-roman, upper-alpha | Counter style applied by the built-in [data-type="page-number"] and [data-type="page-count"] selectors (and by the <PageNumber /> and <PageCount /> SDK components). Defaults to decimal. Override on :host for a layout-wide format, or set inline on a single element via the counterStyle prop. Any valid CSS <counter-style> is accepted, including custom @counter-style names defined in the Layout's CSS. |
--font-sans | CSS font-family value, e.g. "Inter", sans-serif | The font family the sans-serif, ui-sans-serif, ui-rounded, and system-ui keywords resolve to. Defaults to Format Sans. Override on :host to change what those keywords resolve to across the Layout. End the value with the generic keyword (e.g. "Inter", sans-serif) to keep Format Sans as a fallback for glyphs the custom family doesn't carry. |
--font-serif | CSS font-family value, e.g. "Besley", serif | The font family the serif, ui-serif, cursive, fantasy, and fangsong keywords resolve to. Defaults to Format Serif. Override on :host to change what those keywords resolve to across the Layout. End the value with the generic keyword to keep Format Serif as a fallback. |
--font-mono | CSS font-family value, e.g. "JetBrains Mono", monospace | The font family the monospace and ui-monospace keywords resolve to, also applied by the base stylesheet to code, kbd, samp, pre, and tt. Defaults to Format Mono. Override on :host to change what those keywords resolve to across the Layout. End the value with the generic keyword to keep Format Mono as a fallback. |
--font-emoji | CSS font-family value, e.g. "Noto Color Emoji" | The font family the emoji keyword resolves to. Defaults to Format Emoji. Format appends this family to every font-family so emoji render in any text without configuration. Override on :host to supply your own emoji font across the Layout. |
--font-math | CSS font-family value, e.g. "STIX Two Math" | The font family the math keyword resolves to, also applied by the base stylesheet to <math> elements. Defaults to Format Math. Override on :host to change the math family across the Layout. |
TableOfContentshost element of every rendered page, or a contents nav | ||
--toc-font-size | CSS length, e.g. 1.125rem | The root size of a contents entry; every other table-of-contents size is relative to it. Defaults to 1.125rem. |
--toc-line-height | CSS line-height, e.g. 1.35 | The line height of every entry, and the height the number reserves so it sits on the title's first line. Defaults to 1.35. |
--toc-title-font-size | CSS length, e.g. 1em | The entry title's size, relative to the entry. The number's reserved box is built from this same value, so the number stays centered on the title's first line at any size. Defaults to 1em. |
--toc-section-number-font-size | CSS length, e.g. 0.8em | The section number's font size, relative to the entry. Defaults to 0.8em. Size the number through this rather than font-size on the number, which would shift it off the title's line. |
--toc-number-align | center or start | How the number sits against the title's first line: center (the default) holds the optical center, start top-aligns it. |
--toc-page-number-font-size | CSS length | The page number's font size, the same at every depth. Defaults to calc(var(--toc-font-size) * 0.92). |
--toc-description-size | CSS length, e.g. 0.82em | The size of an entry's optional description line. Defaults to 0.82em. |
--toc-number-gap | CSS length, e.g. 0.6em | The gap between an entry's number column and its title. Defaults to 0.6em. |
--toc-leader-gap | CSS length, e.g. 0.3em | The gap between an entry's title and the start of its dot leader. Defaults to 0.3em. |
--toc-leader-dot-gap | CSS length, e.g. 0.5em | The space between leader dots. Defaults to 1.8 times --toc-leader-dot-size, resolved per leader so it follows a dot-size override (the dot and the gap stay in step). Set a plain length to pin it. |
--toc-leader-dot-size | CSS length, e.g. calc(var(--toc-font-size) * 0.0625) | The diameter of each leader dot, one size at every depth. Defaults to calc(var(--toc-font-size) * 0.0625). |
--toc-leader-dot-radius | CSS length or percentage, e.g. 50% | The corner radius of each leader dot. Defaults to 50%, a circle; set 0 for a square, or a value between for a rounded square. |
--toc-leader-color | CSS color, e.g. rgb(from currentColor r g b / 0.5) | The color of the leader dots. Defaults to rgb(from currentColor r g b / 0.5), the text color at half strength. The dots inherit it through currentColor, so set it to recolor the dots without touching the surrounding text. |
--toc-page-gap | CSS length | The gap between the leader and the page number. Defaults to calc(var(--toc-font-size) * 0.6). |
--toc-depth-font-size-step | CSS length, e.g. 0.06em | How much smaller each depth's title is than the one above, for the default depth curve. Defaults to 0.06em. |
--toc-depth-font-size-min | CSS length, e.g. 0.7em | The floor the depth curve will not shrink a title below. Defaults to 0.7em. |
--toc-depth-indent-step | CSS length, e.g. 1.3em | How far each depth indents past the one above. Defaults to 1.3em. |
--toc-space-before-root | CSS length, e.g. 0.85em | The space above a top-level entry. Defaults to 0.85em. |
--toc-space-before-nested | CSS length, e.g. 0.3em | The space above a deeper entry, the floor the per-depth easing settles to. Defaults to 0.3em. |
--toc-space-before-step | CSS length, e.g. 0.55em | How much the space above an entry eases off per depth, from root toward nested. Defaults to 0.55em. |
--toc-depth-<n>-font-size | CSS length | Per-depth escape hatches, unset by default so the depth curve applies. --toc-depth-<n>-font-size, --toc-depth-<n>-indent, and --toc-depth-<n>-space-before pin one depth (n is 0 at the top of the outline), overriding the curve for that depth alone. The --toc-depth-<n>-plus-* forms reshape depth n and every depth deeper. |
Data attributes
| Name | Value | Description |
|---|---|---|
Layouthost element of every rendered page | ||
data-layout-id | string matching a Layout's id, e.g. invoice | The id of the Layout that produced this page. Target it from inside the Layout's <style> with :host([data-layout-id="invoice"]) { ... } to style pages differently per Layout. |
| host element of every rendered page | ||
data-page-number | integer string, e.g. 3 | The 1-indexed number of the current page. Target with the functional :host() selector inside the Layout's <style>, for example :host([data-page-number="1"]) { ... } for page-number-specific styling. |
| host element of the first rendered page only | ||
data-first-page | present with no value | Marker present only on the first page. Select with :host([data-first-page]) { ... } for cover-page-style first-page treatments. |
| host element of the last rendered page only | ||
data-last-page | present with no value | Marker present only on the last page. Select with :host([data-last-page]) { ... } for last-page-specific styling. |
| host element of every even-numbered page | ||
data-even-page | present with no value | Marker present on even-numbered pages (2, 4, 6, and so on). Select with :host([data-even-page]) { ... }, for example to mirror binding margins in a two-up document. Pairs with data-odd-page. |
| host element of every odd-numbered page | ||
data-odd-page | present with no value | Marker present on odd-numbered pages (1, 3, 5, and so on). Select with :host([data-odd-page]) { ... }, for example to mirror binding margins in a two-up document. Pairs with data-even-page. |
| each inline number a Counter or a numbering rule printed | ||
data-type="counter" | generated <span> | The baked value of a counter. A numbering rule's number is followed by a sibling [data-type="counter-separator"] for the gap to the content, and both carry data-counter with the counter's name. Style every number with [data-type="counter"], or one counter's numbers with [data-type="counter"][data-counter="figure"]. |
| the gap between a numbering rule's number and the content it numbers | ||
data-type="counter-separator" | generated <span> holding a single space | Separates a rule-inserted number from the content it numbers, such as a heading's text or a figure's caption. The space is real text, not generated content, so it survives copy and PDF extraction. Adjust the gap with margin, change its character with content (for example content: ": "), or remove it with display: none. It carries data-counter with the counter's name (h1, figure, and so on), so you can style one counter's separator on its own, for example with [data-type="counter-separator"][data-counter="figure"]. A number printed inline by a Counter has no separator. |
| each inline cross-reference a Ref printed | ||
data-type="ref" | generated <span> | A resolved reference: a section number, a counter value, or a page. Style with [data-type="ref"]. |
| the inline marker left where a Footnote was written | ||
data-type="footnote-marker" | generated <sup> | The superscript number at the reference point. Style with [data-type="footnote-marker"]. |
| a footnote region, either a per-page footer or an endnotes list | ||
data-type="footnotes" | generated <aside> | Wraps the gathered notes. It is empty on pages with no footnotes, so guard surrounding scaffolding with [data-type="footnotes"]:not(:empty). An inline endnotes list carries data-variant="inline" and a per-page footer has no variant, so you can style the two apart with [data-type="footnotes"][data-variant="inline"] and [data-type="footnotes"]:not([data-variant="inline"]). A class or style set on the footnotes region carries onto this element, so your own CSS can target the footer. |
| the list of gathered notes inside a footnotes region | ||
data-type="footnote-entries" | generated <dl> | The <dl> holding a page's notes as flat data-type="footnote-number" term and data-type="footnote-content" definition pairs. Style it as a grid (a number column and a content column) with [data-type="footnote-entries"]. |
| the number inside the footnote list | ||
data-type="footnote-number" | generated <dt> | The note's number, matching its inline marker. Style with [data-type="footnote-number"]. |
| the note's content inside the footnote list | ||
data-type="footnote-content" | generated <dd> | The note's content, with any nested markup preserved. Style with [data-type="footnote-content"]. |
| the whole generated table of contents | ||
data-type="toc" | generated <nav> | A <nav> landmark wrapping the contents, so it reads as navigation to assistive technology. The entries sit in a toc-entries wrapper inside it, and any leading content you place in the contents renders in the nav above that wrapper. When the document uses <Numbering> it carries data-variant="numbering", so a number-column layout can scope to [data-type="toc"][data-variant="numbering"]; without <Numbering> the attribute is absent. The --toc-* custom properties configure it: set one on :host to style every contents in the document, or on this nav to style one, where a value on the nav overrides the document default inherited from :host. Each knob is documented as its own --toc-* entry. A class or style you set on the <TableOfContents> carries onto this nav. Style with [data-type="toc"]. |
| the entries wrapper in a table of contents | ||
data-type="toc-entries" | generated <div> | Wraps the entry anchors, below any leading content in the nav. It carries the contents type scale, so the --toc-* sizes apply to the entries while leading content above it keeps the document's own font. Style with [data-type="toc-entries"]. |
| each entry in a table of contents | ||
data-type="toc-entry" | generated <a> | One entry: the anchor linking to its heading, holding the number (when numbered) and a toc-row that pairs the title with its page. It carries its outline depth as data-depth (0 at the top of the outline) and a matching --toc-depth custom property, so deeper entries indent and shrink without any nesting. Target one depth with [data-type="toc-entry"][data-depth="2"]. Style with [data-type="toc-entry"]. |
| the number column in a table-of-contents entry | ||
data-type="toc-number" | generated <span> | The number column for an entry, before the title. It appears only on an entry that received a number, so an unnumbered heading's entry has none. The nav itself carries data-variant="numbering" whenever <Numbering> is on, so a number-column layout can target it even where an entry lacks a number. Wraps a toc-number-text. Style with [data-type="toc-number"]. |
| the number's text inside a table-of-contents entry's number column | ||
data-type="toc-number-text" | generated <span> | The section number's text (for example 2.1), wrapped inside toc-number so the text can be styled apart from its column. Style with [data-type="toc-number-text"]. |
| the title-and-page row in a table-of-contents entry | ||
data-type="toc-row" | generated <span> | Pairs the title with its page on one line, so a leader can run between them. It sits after the number (when numbered). Style with [data-type="toc-row"]. |
| the title in a table-of-contents entry | ||
data-type="toc-title" | generated <span> | The heading's text. Style with [data-type="toc-title"]. |
| the dot leader in a table-of-contents entry | ||
data-type="toc-leader" | generated <span> | The dot leader that fills the space between an entry's title and its page number, rendered as an <svg> of tiled dots. Size the dots with --toc-leader-dot-size and space them with --toc-leader-dot-gap. Style with [data-type="toc-leader"]. Author the table of contents with leader="none" to omit it entirely. |
| the point a table-of-contents leader begins | ||
data-type="toc-leader-start" | generated <span> | An empty, aria-hidden anchor just after an entry's title text, marking where the dot leader starts. It appears only when the leader is drawn, so a table of contents authored with leader="none" omits it. Style with [data-type="toc-leader-start"]. |
| the page number in a table-of-contents entry | ||
data-type="toc-page" | generated <span> | The page the heading landed on, resolved after pagination. Style with [data-type="toc-page"]. |
| an entry's one-line description in a table of contents | ||
data-type="toc-description" | generated <span> | A one-line description under the entry, present only when the heading carries data-toc-description. It sits after the row, under the title, so add a summary line to a heading with <h2 data-toc-description="...">. Style with [data-type="toc-description"]. |