Fluxo UIFluxo UIv0.5.0

PDF Toolkit

Create PDF documents from code, or from a table already on the page. The document and table engines run unchanged in the browser and in Node.

The toolkit is split into two entry points. fluxo-ui/pdf holds the document model, the drawing primitives, the table engine and the layout helpers; it depends on nothing outside the language, so the same code that builds a file in the browser builds it on a server. fluxo-ui/pdf/html adds the one piece that genuinely needs a DOM: reading a table that is already rendered on the page.

Nothing is fetched or evaluated at runtime. The fourteen standard typefaces are described by embedded metrics rather than downloaded font files, and a link whose scheme is not http, https, mailto or tel is discarded when the document is written.

Runs Anywhere

The document and table engines touch no browser API, so the same code produces files in the browser and on a Node server.

Typed Document API

Pages, text, images, vector paths, links and bookmarks behind a fully typed surface with no plugin registration step.

Table Engine

Column width solving, word wrapping, row and column spans, repeating headers and automatic pagination.

Live Table Export

Read a table already rendered on the page, optionally carrying its computed CSS into the document.

No Remote Code

Nothing is fetched or evaluated at runtime. Fonts are embedded metrics, and link schemes are checked before they are written.

Tree Shakes Cleanly

Every module resolves to its own chunk, so importing the colour helpers alone never pulls in the table or image code.

Getting Started

# The toolkit ships with the library
npm install fluxo-ui
import { PdfDocument, drawGrid, savePdf } from 'fluxo-ui/pdf';

const doc = new PdfDocument({
  format: 'a4',
  margin: 40,
  metadata: { title: 'Monthly summary' },
});

doc.drawText('Monthly summary', 40, 50, { fontSize: 20, bold: true });

drawGrid(doc, {
  startY: 90,
  preset: 'ruled',
  columns: [
    { key: 'region', header: 'Region' },
    { key: 'revenue', header: 'Revenue', width: 90, style: { align: 'right' } },
  ],
  body: [
    { region: 'Nordics', revenue: '$42,257' },
    { region: 'East Asia', revenue: '$66,014' },
  ],
});

savePdf(doc.build(), 'summary.pdf');

What comes from where

// Document, drawing, tables, layout helpers and output — isomorphic
import {
  PdfDocument,
  drawGrid,
  createFlow,
  applyHeaderBand,
  applyFooterBand,
  applyWatermark,
  stampPageNumbers,
  savePdf,
  toBlob,
  toDataUrl,
} from 'fluxo-ui/pdf';

// Reading tables out of the DOM — browser only
import { extractTable, buildPdfFromTable, exportTableToPdf } from 'fluxo-ui/pdf/html';

Invoice

Invoice Document

A complete billing document: accent band, sender and recipient blocks, a ruled line-item table, computed tax summary, and payment footer.

1 page · 3.0 KB

Inline preview is unavailable in this browser. Use Download PDF to open the file.

import { PdfDocument, drawGrid } from 'fluxo-ui/pdf';
import { savePdf } from 'fluxo-ui/pdf';

const doc = new PdfDocument({
  format: 'a4',
  margin: 44,
  metadata: { title: 'Invoice INV-2026-0418', author: 'Northwind Studio Ltd.' },
});

const right = doc.pageWidth - doc.margin.right;

doc.drawRectangle(0, 0, doc.pageWidth, 8, { fillColor: '#4f46e5' });
doc.drawText('Northwind Studio Ltd.', doc.margin.left, 56, { fontSize: 17, bold: true });
doc.drawText('INVOICE', right, 56, {
  fontSize: 26,
  bold: true,
  color: '#4f46e5',
  align: 'right',
  anchor: 'point',
});

const table = drawGrid(doc, {
  startY: 258,
  preset: 'ruled',
  columns: [
    { key: 0, header: 'Description' },
    { key: 1, header: 'Qty', width: 44, style: { align: 'right' } },
    { key: 2, header: 'Unit price', width: 78, style: { align: 'right' } },
    { key: 3, header: 'Tax', width: 46, style: { align: 'right' } },
    { key: 4, header: 'Amount', width: 86, style: { align: 'right' } },
  ],
  body: lineItems,
});

doc.drawText('Total due', right, table.finalY + 46, { bold: true, align: 'right', anchor: 'point' });

savePdf(doc.build(), 'invoice.pdf');

Account Statement

Account Statement with Summary Cards

Tinted summary tiles drawn from primitives above a running-balance ledger, with debits and credits coloured by sign and a movement footer.

1 page · 3.7 KB

Inline preview is unavailable in this browser. Use Download PDF to open the file.

import { PdfDocument, drawGrid, stampPageNumbers } from 'fluxo-ui/pdf';

const doc = new PdfDocument({ format: 'a4', margin: 40 });

// Summary cards drawn with primitives
cards.forEach((card, index) => {
  const x = 40 + index * 132;
  doc.drawRectangle(x, 100, 122, 62, { fillColor: card.tint, radius: 8 });
  doc.drawText(card.label, x + 12, 114, { fontSize: 8, color: card.ink });
  doc.drawText(card.value, x + 12, 132, { fontSize: 16, bold: true, color: card.ink });
});

drawGrid(doc, {
  startY: 190,
  preset: 'ruled',
  columns: [
    { key: 'date', header: 'Date', width: 74 },
    { key: 'description', header: 'Description' },
    { key: 'debit', header: 'Debit', width: 78, style: { align: 'right' } },
    { key: 'credit', header: 'Credit', width: 78, style: { align: 'right' } },
    { key: 'balance', header: 'Balance', width: 84, style: { align: 'right' } },
  ],
  body: entries,
});

stampPageNumbers(doc);

Text and Shapes

Text, Alignment and Vector Shapes

Fonts, wrapping, character spacing, decorations, alignment anchors, rounded rectangles, ellipses, polygons, dashed rules and bezier paths.

1 page · 3.0 KB

Inline preview is unavailable in this browser. Use Download PDF to open the file.

import { PdfDocument } from 'fluxo-ui/pdf';

const doc = new PdfDocument({ format: 'a4', margin: 40 });

doc.drawText('Typography', 40, 60, { fontSize: 22, bold: true });
doc.drawText('Helvetica, Times and Courier are always available.', 40, 88, {
  fontSize: 11,
  font: 'Times',
  italic: true,
  color: '#475569',
});

doc.drawText('Wrapped paragraph text flows inside a fixed measure.', 40, 120, {
  maxWidth: 240,
  lineHeightFactor: 1.4,
});

doc.drawText('Centred', 297, 200, { align: 'center', anchor: 'point', fontSize: 14 });
doc.drawText('Right edge', 555, 200, { align: 'right', anchor: 'point', fontSize: 14 });

doc.drawRectangle(40, 240, 140, 70, { fillColor: '#6366f1', radius: 10 });
doc.drawCircle(250, 275, 35, { fillColor: '#f97316', strokeColor: '#7c2d12', lineWidth: 2 });
doc.drawPolygon(
  [{ x: 330, y: 240 }, { x: 400, y: 310 }, { x: 330, y: 310 }],
  { fillColor: '#10b981', opacity: 0.75 },
);
doc.drawLine(40, 340, 555, 340, { strokeColor: '#cbd5e1', dashPattern: [5, 3] });

doc.drawPath((path) => {
  path.moveTo(40, 380).curveTo(140, 350, 240, 430, 340, 380);
}, { strokeColor: '#db2777', lineWidth: 2.5 });

Table Presets

Table Presets

Every built-in table preset rendered from the same data: lined, ruled, zebra, minimal and boxed.

1 page · 3.7 KB

Inline preview is unavailable in this browser. Use Download PDF to open the file.

import { PdfDocument, drawGrid } from 'fluxo-ui/pdf';

const doc = new PdfDocument({ format: 'a4' });

// Five built-in presets: lined, ruled, zebra, minimal, boxed
let y = 60;
for (const preset of ['lined', 'ruled', 'zebra', 'minimal', 'boxed'] as const) {
  const result = drawGrid(doc, {
    startY: y,
    preset,
    columns: [
      { key: 'sku', header: 'SKU', width: 70 },
      { key: 'product', header: 'Product' },
      { key: 'stock', header: 'Stock', width: 54, style: { align: 'right' } },
      { key: 'price', header: 'Price', width: 62, style: { align: 'right' } },
    ],
    body: rows,
  });
  y = result.finalY + 26;
}

Spanning Cells

Spanning Cells and Per-Cell Styling

Two-level merged headers using rowSpan and colSpan, a totals footer, and individual cells carrying their own background, colour and weight.

1 page · 3.4 KB

Inline preview is unavailable in this browser. Use Download PDF to open the file.

import { PdfDocument, drawGrid } from 'fluxo-ui/pdf';

drawGrid(doc, {
  startY: 60,
  preset: 'boxed',
  head: [
    [
      { value: 'Team', rowSpan: 2 },
      { value: 'First half', colSpan: 2, style: { align: 'center' } },
      { value: 'Second half', colSpan: 2, style: { align: 'center' } },
      { value: 'Total', rowSpan: 2, style: { align: 'right' } },
    ],
    ['Q1', 'Q2', 'Q3', 'Q4'],
  ],
  body: [
    ['Platform', '18', '24', '31', '29', '102'],
    ['Design', '12', '9', '15', '18', '54'],
  ],
  foot: [[{ value: 'All teams', colSpan: 5, style: { align: 'right' } }, '156']],
});

Paginated Report

Paginated Landscape Report

Forty-eight rows flowing across pages with a repeating header row, a totals footer, running header and footer bands, and conditional cell colouring.

3 pages · 7.9 KB

Inline preview is unavailable in this browser. Use Download PDF to open the file.

import { PdfDocument, drawGrid, applyHeaderBand, applyFooterBand } from 'fluxo-ui/pdf';

const doc = new PdfDocument({ format: 'a4', orientation: 'landscape', margin: 40 });

drawGrid(doc, {
  startY: 80,
  preset: 'zebra',
  repeat: { header: 'everyPage', footer: 'lastPage' },
  columns: [
    { key: 'region', header: 'Region' },
    { key: 'representative', header: 'Representative' },
    { key: 'units', header: 'Units', style: { align: 'right' } },
    { key: 'revenue', header: 'Revenue', style: { align: 'right' } },
    { key: 'growth', header: 'Growth', style: { align: 'right' } },
    { key: 'status', header: 'Status', style: { align: 'center' } },
  ],
  body: salesRows,
  foot: [[{ value: 'Total', colSpan: 3, style: { align: 'right' } }, total, '', '']],
  onCellBeforeDraw: ({ cell }) => {
    if (cell.section === 'body' && cell.columnIndex === 5) {
      cell.style.textColor = statusColors[cell.text[0]] ?? '#334155';
    }
  },
});

applyHeaderBand(doc, { slots: [{ text: 'Regional Performance' }, { text: 'FY2026', align: 'right' }] });
applyFooterBand(doc, {
  slots: [{ text: (page, total) => `Page ${page} of ${total}`, align: 'right' }],
  ruleColor: '#e2e8f0',
  ruleWidth: 0.5,
});

Flowing Document

Flowing Multi-Section Document

Headings, paragraphs, rules and tables stacked in order. The composer tracks the cursor and starts a new page whenever the next block will not fit.

2 pages · 4.5 KB

Inline preview is unavailable in this browser. Use Download PDF to open the file.

import { PdfDocument, createFlow, stampPageNumbers } from 'fluxo-ui/pdf';

const doc = new PdfDocument({ format: 'a4', margin: 48 });
const flow = createFlow(doc, { startY: 60, gap: 10 });

flow.heading('Annual Review', { fontSize: 22 });
flow.paragraph(summary, { fontSize: 10.5, lineHeightFactor: 1.5 });
flow.rule();

flow.heading('Territory breakdown', { fontSize: 14 });
flow.grid({
  preset: 'ruled',
  columns: [...],
  body: rows,
});

flow.heading('Outlook', { fontSize: 14 });
flow.paragraph(outlook);

stampPageNumbers(doc);

Certificate

Certificate

A landscape page with zero margins, decorative borders and corner rules. Type a recipient name and upload a seal image to see both placed in the document.

Drag & drop files here, or browse to chooseAccepted: image/png,image/jpeg • Up to 1 file
No seal chosen — a drawn placeholder is used.
1 page · 2.2 KB

Inline preview is unavailable in this browser. Use Download PDF to open the file.

import { PdfDocument } from 'fluxo-ui/pdf';

const doc = new PdfDocument({ format: 'a4', orientation: 'landscape', margin: 0 });

const width = doc.pageWidth;
const centre = width / 2;

doc.drawRectangle(0, 0, width, doc.pageHeight, { fillColor: '#fffbeb' });
doc.drawRectangle(26, 26, width - 52, doc.pageHeight - 52, { strokeColor: '#b45309', lineWidth: 2.5 });

doc.drawText('CERTIFICATE OF COMPLETION', centre, 104, {
  align: 'center',
  anchor: 'point',
  fontSize: 25,
  bold: true,
  color: '#78350f',
  characterSpacing: 1.5,
});

doc.drawText(recipientName, centre, 200, {
  align: 'center',
  anchor: 'point',
  fontSize: 34,
  font: 'Times',
  italic: true,
});

// A seal supplied as a PNG or JPEG, sized and centred on the page
if (sealImage) {
  doc.drawImage(sealImage, { x: centre - 34, y: 318, width: 68, height: 68 });
} else {
  doc.drawCircle(centre, 352, 34, { fillColor: '#fef3c7', strokeColor: '#b45309', lineWidth: 1.5 });
  doc.drawText('SEAL', centre, 350, { align: 'center', anchor: 'point', fontSize: 10, bold: true });
}

Pages and Units

Units, Page Sizes and Watermarks

A document measured in millimetres where each page chooses its own size, orientation and margin, finished with a watermark applied across every page.

4 pages · 3.9 KB

Inline preview is unavailable in this browser. Use Download PDF to open the file.

import { PdfDocument, applyWatermark } from 'fluxo-ui/pdf';

// Work in millimetres instead of points
const doc = new PdfDocument({ unit: 'mm', format: 'a5', margin: 12 });

doc.drawText('A5 measured in millimetres', 12, 20, { fontSize: 14, bold: true });
doc.drawRectangle(12, 30, 60, 25, { fillColor: '#0ea5e9' });

// Each page can take its own size and orientation
doc.addPage({ format: 'letter', orientation: 'landscape' });
doc.drawText('Letter, landscape', 12, 20, { fontSize: 14, bold: true });

doc.addPage({ format: [120, 80] });
doc.drawText('Custom 120 x 80 mm', 12, 20, { fontSize: 12, bold: true });

applyWatermark(doc, { text: 'SPECIMEN', opacity: 0.12 });

HTML Table Export

Export an HTML Table

Reads a table that is already on the page — text, colspans and optionally the computed CSS — and turns it into a PDF.

SKUProductCategoryStockPrice
FRN-1042Oak reading chairFurniture24$489
FRN-1088Walnut side tableFurniture8$265
LGT-2210Brass floor lampLighting41$178
LGT-2255Linen pendant shadeLightingOut of stock$96
TXT-3301Wool throw blanketTextiles132$74
TXT-3348Cotton cushion coverTextiles87$32
DEC-4419Ceramic vase, tallDecor15$58
DEC-4460Stoneware bowl setDecor3$112
Catalog total310$1,304
import { buildPdfFromTable, exportTableToPdf } from 'fluxo-ui/pdf/html';

// One call: read a live table from the DOM and save it
exportTableToPdf('#catalog-table', {
  title: 'Product Catalog',
  fileName: 'catalog.pdf',
  // Keep the spacing, alignment and borders, but print in the preset's colours
  extract: { inheritStyles: true, ignoreColors: true },
  grid: { preset: 'ruled' },
});

// Or take the document first to keep editing it
const { doc } = buildPdfFromTable('#catalog-table', {
  extract: { inheritStyles: true, includeHidden: false },
  grid: { preset: 'zebra' },
});
doc.drawText('Reviewed by operations', 40, doc.pageHeight - 60, { fontSize: 9 });
savePdf(doc.build(), 'catalog.pdf');

Interactive Builder

Build a Document from Form Input

Edit the title, page setup, table preset and line items. The preview regenerates from the same code a consumer would write.

Total $16,900.00
1 page · 1.7 KB

Inline preview is unavailable in this browser. Use Download PDF to open the file.

import { PdfDocument, drawGrid } from 'fluxo-ui/pdf';

const doc = new PdfDocument({ format, orientation, margin: 40 });

doc.drawText(title, 40, 50, { fontSize: 18, bold: true });

drawGrid(doc, {
  startY: 84,
  preset,          // lined | ruled | zebra | minimal | boxed
  columns: [
    { key: 'item', header: 'Item' },
    { key: 'qty', header: 'Qty', width: 56, style: { align: 'right' } },
    { key: 'rate', header: 'Rate', width: 80, style: { align: 'right' } },
    { key: 'amount', header: 'Amount', width: 90, style: { align: 'right' } },
  ],
  body: rows,
  foot: [[{ value: 'Total', colSpan: 3, style: { align: 'right' } }, total]],
});

Server Side

build() returns a Uint8Array, so a document can be written straight to disk, streamed in a response or attached to a message. Only the helpers that reach for Blob, URL or document — savePdf, printPdf, openPdfInNewTab — require a browser.

import { writeFileSync } from 'node:fs';
import { PdfDocument, drawGrid } from 'fluxo-ui/pdf';

// No DOM is involved, so this runs unchanged on a server
const doc = new PdfDocument({ format: 'a4', margin: 40 });

doc.drawText('Nightly batch', 40, 50, { fontSize: 18, bold: true });

drawGrid(doc, {
  startY: 84,
  preset: 'zebra',
  columns: [
    { key: 'account', header: 'Account' },
    { key: 'balance', header: 'Balance', width: 90, style: { align: 'right' } },
  ],
  body: await loadAccountRows(),
});

writeFileSync('statement.pdf', doc.build());

Document Options

unit
'pt' | 'mm' | 'cm' | 'in' | 'px' | 'pc'"'pt'"

Measurement unit for every coordinate and size passed to the document

format
string | [number, number] | { width: number; height: number }"'a4'"

Page size preset name (a0-a8, b3-b6, c3-c6, letter, legal, tabloid, ledger, executive, folio, statement, creditcard) or explicit dimensions

orientation
'portrait' | 'landscape'"'portrait'"

Page orientation applied to the default page size

margin
number | number[] | { top, right, bottom, left }"40pt"

Page margin used as the default content area for every page

metadata
DocumentMetadata

Title, author, subject, keywords, creator and dates written to the document information dictionary

compress
boolean"true"

Compress page content streams

defaultFont
string"'Helvetica'"

Font family used until setFont is called

defaultFontSize
number"11"

Font size used until setFontSize is called

lineHeightFactor
number"1.15"

Multiplier applied to the font size to derive line height

language
string

Document language tag exposed to assistive technology

Text Options

font
string

Font family for this call, falling back to the document font

fontSize
number

Font size in document units

bold
boolean"false"

Select the bold face of the family

italic
boolean"false"

Select the italic or oblique face of the family

color
ColorInput

Text colour as a hex string, CSS colour function, named colour, packed integer or channel array

align
'left' | 'center' | 'right' | 'justify'"'left'"

Horizontal alignment within the measure defined by maxWidth

anchor
'box' | 'point'"'box'"

Whether x is the left edge of an alignment box or the alignment point itself

maxWidth
number

Wrap the text to this measure

maxLines
number

Truncate after this many lines and append the ellipsis marker

ellipsis
string"'…'"

Marker appended when text is truncated

lineHeightFactor
number

Line height multiplier for this call

characterSpacing
number"0"

Extra space inserted between characters

wordSpacing
number"0"

Extra space inserted at each space character

rotation
number"0"

Rotation in degrees about the text origin

baseline
'alphabetic' | 'top' | 'middle' | 'bottom'"'alphabetic'"

Which part of the glyph box the y coordinate refers to

underline
boolean"false"

Draw a rule beneath each line

strikeThrough
boolean"false"

Draw a rule through each line

opacity
number"1"

Text opacity between 0 and 1

Shape Options

fillColor
ColorInput

Interior colour; omit to leave the shape unfilled

strokeColor
ColorInput

Outline colour; omit to leave the shape unstroked

lineWidth
number

Outline width in document units

lineCap
'butt' | 'round' | 'square'"'butt'"

How open path ends are drawn

lineJoin
'miter' | 'round' | 'bevel'"'miter'"

How corners between segments are drawn

dashPattern
number[]

Alternating dash and gap lengths

opacity
number"1"

Opacity applied to both fill and stroke

radius
number"0"

Corner radius, accepted by drawRectangle

Table Options

columns
ColumnDefinition[]

Column keys, headers, widths and per-column styles; inferred from the data when omitted

head
RowDefinition[]

Header rows; derived from the column headers when omitted

body
RowDefinition[]

Body rows as arrays or as objects keyed by column key

foot
RowDefinition[]

Footer rows; derived from the column footers when omitted

preset
'lined' | 'ruled' | 'zebra' | 'minimal' | 'boxed'"'lined'"

Built-in visual treatment for the table

palette
GridPalette

Colour overrides layered on top of the chosen preset

styles
SectionStyles

Style overrides for all cells, a section, the stripe rows, or named columns

startY
number

Vertical position where the table begins; defaults to the top margin

margin
number | number[] | EdgeInsets

Margin used to compute the available width and page break threshold

width
number | 'full' | 'auto'"'full'"

Target table width

align
'left' | 'center' | 'right'"'left'"

Horizontal placement of the table within the content area

repeat
{ header?: 'everyPage' | 'firstPage' | 'never'; footer?: 'everyPage' | 'lastPage' | 'never' }"{ header: 'everyPage', footer: 'lastPage' }"

Whether header and footer rows repeat when the table spans pages

minRowHeight
number"0"

Minimum height applied to every row

outerBorderWidth
number"0"

Width of a border drawn around the whole table

onCellBeforeDraw
(context: CellRenderContext) => boolean | void

Called before each cell is painted; mutate the style or return false to skip the cell

onCellAfterDraw
(context: CellRenderContext) => void

Called after each cell is painted, for drawing decorations over it

onPageStart
(context: PageRenderContext) => void

Called when the table starts laying out on a page

onPageEnd
(context: PageRenderContext) => void

Called when the table finishes on a page

Column Options

A column’s style applies to its header, body and footer cells together, so setting align once lines a column’s heading up with its values. headerStyle and footerStyle layer on top of it when one section needs to differ.

key
string | number

Identifies the column; object rows are read by this key and array rows by position

header
CellValue

Header cell content when no explicit head rows are supplied

footer
CellValue

Footer cell content when no explicit foot rows are supplied

width
number | 'auto' | 'shrink'"'auto'"

Fixed width in document units, or let the solver size the column from its content

minWidth
number

Lower bound the solver may not shrink past

maxWidth
number

Upper bound the solver may not grow past

style
Partial<CellStyle>

Styles for the whole column — header, body and footer cells alike. Set align here to align a column's header with its values

headerStyle
Partial<CellStyle>

Overrides layered on top of style for this column's header cells only

footerStyle
Partial<CellStyle>

Overrides layered on top of style for this column's footer cells only

format
(value: CellValue, rowIndex: number) => string

Formats each body value before it is measured and drawn

Extract Options

includeHidden
boolean"false"

Include rows and cells whose computed style hides them

inheritStyles
boolean"false"

Carry the table's computed CSS into the generated document

ignoreColors
boolean"false"

When inheriting styles, keep the spacing, alignment and borders but leave the page's colours behind so the table prints in the preset's palette

view
Window

Window used to resolve computed styles; defaults to the table's own view

skipColumns
number[]

Column indices to leave out of the export

includeRow
(row: HTMLTableRowElement) => boolean

Return false to leave a row out of the export

includeCell
(cell: HTMLTableCellElement) => boolean

Return false to leave a cell out of the export

transformText
(text: string, element: HTMLTableCellElement) => string

Rewrite each cell's text before it is measured

Features

Runs Anywhere

The document and table engines touch no browser API, so the same code produces files in the browser and on a Node server.

Typed Document API

Pages, text, images, vector paths, links and bookmarks behind a fully typed surface with no plugin registration step.

Table Engine

Column width solving, word wrapping, row and column spans, repeating headers and automatic pagination.

Live Table Export

Read a table already rendered on the page, optionally carrying its computed CSS into the document.

No Remote Code

Nothing is fetched or evaluated at runtime. Fonts are embedded metrics, and link schemes are checked before they are written.

Tree Shakes Cleanly

Every module resolves to its own chunk, so importing the colour helpers alone never pulls in the table or image code.