Documentation
Layout
Every layout is built from a handful of primitives. Column and Row stack their children, Spacer
and Expanded distribute the space between them, and Box and Padding frame and inset content.
Column and Row
Column stacks its children top to bottom, Row left to right. Both take the same options.
Column({ gap: 12 }, [Text("First"), Text("Second")]);
Row({ gap: 8, justify: "between", align: "center" }, [
Text("Total", { size: 16, bold: true }),
Text("100.00 EUR"),
]);
| Prop | Type | What it does |
|---|---|---|
gap | number | space inserted between children, in points |
justify | "start" | "center" | "end" | "between" | "around" | distribution along the stacking axis (like CSS justify-content) |
align | "start" | "center" | "end" | "stretch" | position across the axis (like CSS align-items) |
wrap | boolean | let children flow onto further lines when they do not fit |
alignContent | "start" | "center" | "end" | "between" | "around" | "stretch" | how the WRAPPED lines sit as a block |
reverse | boolean | lay the children out in the opposite order |
width height | number | string | fix a dimension, in points or as "50%" of the offered space |
justify: "between" pushes the first and last child to the edges. align: "center" lines children up
on the cross axis. Both default to start.
Wrapping, and per-child control
With wrap, children that do not fit start a new line, and alignContent places those lines as a
block. Individual children get three props of their own, on any element:
Row({ wrap: true, gap: 8, alignContent: "between" }, [
Box({ width: "33%" }, [Text("one")]),
Box({ width: "33%" }, [Text("two")]),
Box({ width: "33%", order: -1 }, [Text("drawn first")]),
Box({ flexShrink: 0 }, [Text("keeps its width, and overflows")]),
Expanded({ flex: 1, flexBasis: 120 }, Box([Text("starts at 120pt, then grows")])),
Box({ alignSelf: "end" }, [Text("overrides the row's align")]),
]);
flexShrink is 1, as in CSS: when a line overflows, its children give space back in proportion to
their own width until the line fits. 0 opts a child out, so it keeps its width and runs past the edge.
Nothing is squeezed past what it can actually hold. A child's floor is its min-content extent - for
a text, its longest word - which is the same rule a browser applies through the automatic min-width of
a flex item. Once a child sits on its floor it stops giving, and the share it could not absorb goes to
the ones that still can.
A percentage width inside a flex line resolves against the line minus its gaps, so N columns at
(100/N)% fit exactly - CSS makes you subtract the gaps yourself.
A layout's
alignuses the cross-axis vocabularystart / center / end(it is direction-agnostic - for aRowthe cross axis is vertical, soleft/rightwould not make sense). That differs from aText's ownalign, which isleft / center / right.
Spacer
Spacer is flexible empty space. Drop one between two items and it pushes them apart.
Row([Text("Subtotal"), Spacer(), Text("100.00 EUR")]);
Pass a number to weight several spacers against each other: Spacer(2) takes twice the space of
Spacer(1).
Expanded
Expanded makes a child grow to fill the remaining space. Give each one a flex weight to share the
space in proportion.
Row({ gap: 12 }, [
Expanded({ flex: 1 }, Box({ border: "steelblue", padding: 12, radius: 6 }, [Text("flex 1")])),
Expanded(
{ flex: 2 },
Box({ bg: "#1450aa11", padding: 12, radius: 6 }, [Text("flex 2, twice as wide")]),
),
]);
| Prop | Type | What it does |
|---|---|---|
flex | number | the child's share of the remaining space (default 1) |
Expanded takes a single child, not an array. In a Row it fills width, in a Column it fills height.
Box
Box is the styled container: a background, a border, rounded corners, padding and an optional fixed
size. By default it fills the width it is offered (like a block element) and shrink-wraps its height to
its content. Set width or height to fix a dimension. (As a fixed child in a Row it shrink-wraps
its width too, since there it is not offered a width to fill.)
Box({ bg: "#1450aa11", border: "steelblue", borderWidth: 1, radius: 8, padding: 16 }, [
Text("A framed, padded, rounded box."),
]);
| Prop | Type | What it does |
|---|---|---|
bg | ColorInput | fill color |
border | ColorInput | border color (all sides) |
borderTop borderRight borderBottom borderLeft | ColorInput | color a single side |
borderWidth | number | border thickness (default 1 when a border is set) |
radius | number | Corners | corner radius - one number, or per corner |
padding | Insets | inner spacing (see below) |
width height | number | string | a fixed size in points, or "50%" of the offered space |
minWidth maxWidth minHeight maxHeight | number | string | clamp a dimension |
aspectRatio | number | derive the axis you did not pin (16 / 9) |
overflow | "hidden" | crop the children to the box, rounded corners and all |
relative | boolean | make this box the frame for Positioned children |
Sizing, in the order it happens
The order is the contract, and it follows CSS: relative sizing → the aspect ratio fills whichever
axis was left open → min/max clamp. So an explicit bound beats the ratio, the way min-height beats
aspect-ratio in a browser.
Box({ width: "50%", maxWidth: 100 }, []); // 50% of a 400pt region is 200, clamped to 100
Box({ width: 240, aspectRatio: 16 / 9 }, []); // height derived: 135
Percentages on padding and margin resolve against the width on all four sides, top and bottom
included. That is the CSS quirk, kept on purpose so a layout ported from the web behaves the same.
Gradients
bg takes a gradient as readily as a colour. It is written box-relative - an angle and stops - and
resolved against the box when it is drawn:
import { linearGradient, radialGradient } from "@jasy/pdf";
Box({ bg: linearGradient({ angle: 135, stops: ["#1450aa", "#7aa5e8"] }), padding: 20 }, [...]);
Box({ bg: radialGradient({ stops: ["#ffffff", "#e2483d"] }), width: 80, height: 80 }, []);
Angles follow CSS: 0 points up, and it turns clockwise.
Corners, one at a time
radius also takes a corner object. Two radii that share an edge and would overlap are scaled down
together, so the shape stays smooth - react-pdf clamps each corner on its own, which pinches.
Box({ radius: { topLeft: 16, bottomRight: 16 }, bg: "#eef1f5", padding: 16 }, [...]);
Padding
Padding insets a single child without drawing anything. It is the spacing-only cousin of Box.
Padding({ top: 16, bottom: 8 }, Text("Breathing room above and below."));
Anywhere a spacing value is taken (padding on a Box, the Padding element), you can write it four
ways:
16 // all four sides
{ x: 16, y: 8 } // horizontal / vertical
{ top: 16, left: 8 } // per side, any subset
[16, 8, 16, 8] // [top, right, bottom, left]
Divider
Divider draws a thin horizontal rule across the full width of its parent - a clean separator between
sections, totals or list items.
Column({ gap: 4 }, [Text("Subtotal"), Divider(), Text("Total", { bold: true })]);
| Prop | Type | What it does |
|---|---|---|
color | ColorInput | line color (default a light gray) |
thickness | number | line thickness in points (default 1) |
margin | Insets | space above / below the rule (default a small gap) |
Divider({ color: "#1450aa", thickness: 2, margin: { y: 12 } });
Colors
Wherever a color is taken (color, bg, border), jasy accepts several forms. rgb and rgba are
small helpers you import from the package.
import { rgb, rgba } from "@jasy/pdf";
// every entry below is a valid color value:
const colors = [
"steelblue", // any CSS color name
"#1450aa", // hex
"#1450aacc", // hex with alpha
0xff1450aa, // ARGB number, alpha first
rgb(20, 80, 170), // channels 0 to 255
rgba(20, 80, 170, 0.5), // ... with an alpha of 0 to 1
"rgb(20, 80, 170)", // the CSS function, as a string
"rgb(20 80 170 / 50%)", // the modern space form, with slash-alpha
"hsl(214, 79%, 37%)", // and hsl(), the same sRGB family
];
A wide-gamut function such as color(display-p3 …) or light-dark() is named and refused rather
than squashed into sRGB - the result would be a colour nobody chose.
See it all in one file
Here is every primitive on this page in a single document. A Document can hold more than one Page,
so each page below demonstrates one idea. Copy it, run it once, and flip through the result.
import { writeFileSync } from "node:fs";
import {
Document,
Page,
Column,
Row,
Box,
Padding,
Text,
Spacer,
Expanded,
Divider,
renderToBytes,
} from "@jasy/pdf";
const heading = (t: string) => Text(t, { size: 22, bold: true, color: "#1450aa" });
const sub = (t: string) => Text(t, { size: 12, color: "gray" });
async function build() {
const doc = Document([
Page({ size: "A4", margin: 56 }, [
Column({ gap: 16 }, [
heading("Column and Row"),
sub("Column stacks top to bottom, Row stacks left to right."),
Divider({ color: "steelblue" }),
Row({ justify: "between", align: "center" }, [
Text("Left", { bold: true }),
Text("Right", { bold: true }),
]),
]),
]),
Page({ size: "A4", margin: 56 }, [
Column({ gap: 16 }, [
heading("Spacer"),
sub("Flexible empty space that pushes items apart."),
Divider({ color: "steelblue" }),
Row([Text("Subtotal"), Spacer(), Text("100.00 EUR")]),
]),
]),
Page({ size: "A4", margin: 56 }, [
Column({ gap: 16 }, [
heading("Expanded"),
sub("Children grow to share the remaining space by their flex weight."),
Divider({ color: "steelblue" }),
Row({ gap: 12 }, [
Expanded(
{ flex: 1 },
Box({ border: "steelblue", padding: 14, radius: 6 }, [Text("flex 1")]),
),
Expanded({ flex: 2 }, Box({ bg: "#1450aa11", padding: 14, radius: 6 }, [Text("flex 2")])),
]),
]),
]),
Page({ size: "A4", margin: 56 }, [
Column({ gap: 16 }, [
heading("Box"),
sub("A background, a border, rounded corners and padding."),
Divider({ color: "steelblue" }),
Box({ bg: "#1450aa11", border: "steelblue", borderWidth: 1, radius: 8, padding: 18 }, [
Text("A framed, padded, rounded box."),
]),
]),
]),
Page({ size: "A4", margin: 56 }, [
Column({ gap: 16 }, [
heading("Padding"),
sub("Insets a single child without drawing anything."),
Divider({ color: "steelblue" }),
Box({ border: "steelblue", radius: 6 }, [
Padding(
{ x: 20, y: 14 },
Text("This text sits inside 20pt horizontal, 14pt vertical padding."),
),
]),
]),
]),
]);
writeFileSync("layout-tour.pdf", await renderToBytes(doc));
}
build();
Run it, open layout-tour.pdf, and page through Column and Row, Spacer, Expanded, Box and Padding.