@satzstrom/primitives
Primitives
React components for pages, flowing content, and document structure. Use ordinary HTML and CSS inside them.
npm install @satzstrom/primitivesDocument
Root of a Satzstrom document. Contains fixed Page components, flowing PageMaster components, or both. Defines language, reading direction and PDF metadata.
function Document(props: DocumentProps): ReactNode| Name | Type | Description |
|---|---|---|
children | ReactNode | Direct children must resolve to Page or PageMaster components. |
title? | string | PDF title; required for PDF/UA-1. |
author? | string | PDF author. |
subject? | string | PDF subject. |
keywords? | string[] | Keywords for PDF metadata. |
lang? | string | Document language; de by default. |
dir? | "ltr" | "rtl" | Reading direction; ltr by default. |
bookmarks? | SequenceDefinition<boolean> | Sequence used to create PDF bookmarks. |
labels? | DocumentLabels | Overrides localized accessible labels. |
className? | string | CSS class of the document root element. |
import { Document, Page } from "@satzstrom/primitives";export default function Invoice() { return ( <Document title="Invoice 2026-0718" author="Brückner Studio" lang="en"> <Page size="A4">…</Page> </Document> );}Page
Creates exactly one freely composed physical page. Children can be static or receive page values through a render function. For custom sizes, landscape puts the longer dimension horizontally and portrait vertically; omitted orientation preserves width and height.
function Page(props: PageProps): ReactNode| Name | Type | Description |
|---|---|---|
children | ReactNode | ((PageContext) => ReactNode) | Page content or a render function with page and pages. |
size? | PageSizeName | { width; height } | Paper size; A4 by default. Custom dimensions use millimeters. |
orientation? | "portrait" | "landscape" | Page orientation. |
bleed? | number | Non-negative bleed in millimeters. |
cropMarks? | boolean | Enables crop marks. |
className? | string | CSS class of the physical page. |
<Page size="A4" orientation="landscape" bleed={3} cropMarks> {({ page, pages }) => <footer>{page} / {pages}</footer>}</Page>PageMaster
Flows its children through as many pages as needed. A custom layout places Flow between repeating regions, such as headers and footers. Without a layout, content uses the whole page.
function PageMaster(props: PageMasterProps): ReactNode| Name | Type | Description |
|---|---|---|
children | ReactNode | Flowing content to paginate. |
layout? | ComponentType<PageLayoutProps> | Page frame with page, pages, and exactly one Flow. |
size? | PageSettings["size"] | Paper size for every page in this master. |
orientation? | PageSettings["orientation"] | Shared page orientation. |
bleed? | number | Bleed in millimeters. |
cropMarks? | boolean | Enables crop marks. |
className? | string | CSS class of the generated pages. |
import { Document, Flow, PageMaster, type PageLayoutProps } from "@satzstrom/primitives";function ReportPage({ page, pages }: PageLayoutProps) { return ( <div style={{ display: "flex", flexDirection: "column", height: "100%" }}> <header>Annual report</header> <Flow style={{ flex: 1 }} /> <footer>{page} / {pages}</footer> </div> );}export default function Report() { return ( <Document title="Annual report"> <PageMaster layout={ReportPage} size="A4"> <h1>Introduction</h1> <p>This content flows across all required pages.</p> </PageMaster> </Document> );}Flow
Marks the area where PageMaster content continues. Keep Flow empty and give it space through CSS, for example with flex: 1. Standard div attributes are supported.
function Flow(props: FlowProps): ReactNode<Flow style={{ flex: 1 }} />PageBreak
Forces a new page in the flowing content of a PageMaster.
function PageBreak(): ReactNode<PageMaster layout={ReportPage}> <ChapterOne /> <PageBreak /> <ChapterTwo /></PageMaster>RepeatBox
Marks a box so its border, background, and padding repeat on every fragment.
function RepeatBox<E extends ElementType = "div">(props: RepeatBoxProps<E>): ReactNode| Name | Type | Description |
|---|---|---|
as? | ElementType | Rendered HTML element; div by default. |
…props | ComponentPropsWithoutRef<E> | All props of the selected element. |
<RepeatBox as="aside" className="notice"> A long note that may continue across a page boundary.</RepeatBox>defineSequence
Defines an immutable, independent numbering system for chapters, figures, or tables.
function defineSequence(config: { name: string; titleRequired?: boolean;}): SequenceDefinitionconst chapters = defineSequence({ name: "chapters", titleRequired: true,});<Sequence sequence={chapters} title="Results"> {({ number, title }) => <h2>{number} {title}</h2>}</Sequence>Sequence
Registers an entry in a Sequence and provides its computed number, title, and nesting depth.
function Sequence(props: SequenceProps): ReactNode| Name | Type | Description |
|---|---|---|
sequence | SequenceDefinition | Definition created with defineSequence. |
id? | string | Explicit HTML target for references. |
title? | string | Entry title; required when titleRequired is set. |
numberLabel? | string | Accessible label for the rendered number. |
children | (value) => ReactNode | Render function with number, title, and depth. |
Contents
Creates a table of contents with final numbers and pages from a Sequence, or passes entries to a custom component.
function Contents(props: ContentsProps): ReactNode| Name | Type | Description |
|---|---|---|
sequence | SequenceDefinition | Source of the table-of-contents entries. |
className? | string | Class of the default table of contents. |
asChild? | true | Passes entries to exactly one custom React element. |
import { Contents, type SequenceEntry } from "@satzstrom/primitives";function CustomContents({ entries = [] }: { entries?: SequenceEntry[] }) { return <ol>{entries.map(entry => ( <li key={entry.id} style={{ marginLeft: entry.depth * 12 }}> <a href={"#" + entry.id}>{entry.number} {entry.title}</a> — {entry.page} </li> ))}</ol>;}// SequenceEntry = { id: string; number: string; title?: string;// page: number; depth: number; parentId?: string }<Contents sequence={chapters} asChild><CustomContents /></Contents>Ref
Links to an HTML ID and displays its computed number, title, or final page.
function Ref(props: RefProps): ReactNode| Name | Type | Description |
|---|---|---|
target | string | HTML ID of the referenced target. |
value? | "number" | "title" | "page" | Output value; number by default. |
className? | string | CSS class of the link. |
See <Ref target="results" value="number" /> on page <Ref target="results" value="page" />.Footnote
Places a numbered call in the text. Its content appears in the authored Footnotes region, with shared Markdown/JSX numbering and backlinks.
function Footnote(props: { children: ReactNode; label?: string }): ReactNodeThis statement needs a source<Footnote>Source: Annual report 2025.</Footnote>.Footnotes
An empty placeholder in a PageMaster layout, outside Flow, or inside a fixed Page. Style and limit its height with CSS. It is hidden when empty and adds no separator. Long notes continue in the same PageMaster; a fixed Page reports overflow.
function Footnotes(props: FootnotesProps): ReactNodeExisting documents with footnotes need one empty Footnotes component outside Flow in each PageMaster layout that uses notes. Copy the complete document.tsx example to get started. For a fixed Page, place Footnotes inside that Page.
The full-height frame gives the percentage limit a definite height. Footnotes can use up to 20% here; Flow receives the remaining space. Change the limit, font and spacing to suit your layout.
import { Document, PageMaster, Flow, Footnote, Footnotes } from "@satzstrom/primitives";function PageLayout() { return ( <div style={{ height: "100%", padding: "15mm", display: "flex", flexDirection: "column" }}> <Flow style={{ flex: 1, minHeight: 0 }} /> <Footnotes style={{ maxHeight: "20%", flexShrink: 0, fontSize: "10pt" }} /> </div> );}export default function Report() { return ( <Document title="Report with footnotes" lang="en"> <PageMaster size="A4" layout={PageLayout}> <h1>A report with footnotes</h1> <p>A statement with a source.<Footnote>Source: Annual report 2025.</Footnote></p> </PageMaster> </Document> );}Math
Renders KaTeX as visible HTML plus accessible MathML, inline or as a separate display equation.
function Math(props: { children: string; display?: boolean; label?: string;}): ReactNode<Math>{String.raw`E = mc^2`}</Math><Math display label="Quadratic formula"> {String.raw`x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}`}</Math>Markdown
Renders GitHub-Flavored Markdown with tables, footnotes, and mathematics. Raw HTML is deliberately skipped. Use single dollars for inline math, double dollars for display math, and escape literal dollar signs.
function Markdown(props: { children: string; labels?: MarkdownLabels;}): ReactNode<Markdown>{`## Results| Quarter | Revenue || --- | ---: || Q1 | 42.000 € |A reference with a footnote.[^1][^1]: Internal analysis.`}</Markdown>useRenderReady
Delays measurement and rendering while asynchronous browser content is not ready.
function useRenderReady(ready: boolean): voidfunction RemoteChart() { const { data, isLoading } = useReportData(); useRenderReady(!isLoading); return data ? <Chart data={data} /> : null;}