@rizkyandriawan/foliojs
v0.2.2
Published
DOM-first pagination. Measure, don't simulate.
Downloads
222
Maintainers
Readme
Folio
DOM-first pagination. Measure, don't simulate.
Folio is a JavaScript library that paginates HTML content into discrete pages, perfect for print layouts and PDF generation. Unlike CSS Paged Media polyfills that try to simulate browser behavior, Folio measures the actual rendered DOM and applies smart heuristics for optimal page breaks.
<folio-pages page-size="A4">
<h1>Your Document Title</h1>
<p>Your content here. Folio handles the rest.</p>
</folio-pages>Why Folio?
CSS Paged Media sounds great on paper (pun intended). In practice? Browser support is inconsistent, polyfills are overengineered, and you end up fighting the tool instead of shipping.
Folio takes a different approach:
- Let the browser do its job — render your HTML + CSS normally
- Measure the actual DOM —
offsetHeightdoesn't lie - Apply smart heuristics — battle-tested rules for where to break
- Output pages — done
No CSS @page polyfilling. No abstract syntax tree manipulation. No complex configuration for edge cases.
Installation
npm install @rizkyandriawan/foliojsOr use via CDN:
<script type="module">
import 'https://unpkg.com/@rizkyandriawan/foliojs';
</script>Quick Start
Web Component (Recommended)
The easiest way to use Folio is with the <folio-pages> web component:
<!DOCTYPE html>
<html>
<head>
<script type="module">
import '@rizkyandriawan/foliojs';
</script>
<style>
/* Your document styles */
folio-pages h1 { font-size: 24pt; }
folio-pages p { line-height: 1.6; }
</style>
</head>
<body>
<folio-pages page-size="A4" orientation="portrait">
<h1>My Document</h1>
<p>Content goes here...</p>
<h2>Section Two</h2>
<p>More content...</p>
</folio-pages>
</body>
</html>Folio automatically:
- Measures your content with your CSS applied
- Splits content across pages respecting orphan/widow rules
- Keeps headings with their following content
- Handles tables, code blocks, lists, and images intelligently
JavaScript API
For programmatic control:
import { paginate } from '@rizkyandriawan/foliojs';
const result = await paginate(document.querySelector('#content'), {
pageSize: 'A4',
orientation: 'portrait',
padding: 80,
});
console.log(`Generated ${result.totalPages} pages`);Configuration
Attributes (Web Component)
| Attribute | Default | Description |
|-----------|---------|-------------|
| page-size | A4 | Page size preset: A4, Letter, Legal |
| page-width | 794 | Custom width in pixels (overrides page-size) |
| page-height | 1123 | Custom height in pixels (overrides page-size) |
| orientation | portrait | Page orientation: portrait or landscape |
| padding | 80 | Page padding in pixels (all sides) |
| orphan-lines | 2 | Minimum lines to leave at bottom of page |
| widow-lines | 2 | Minimum lines to keep at top of new page |
| min-content-lines | 3 | Minimum content lines after a heading |
| repeat-table-header | false | Repeat <thead> when tables split across pages |
| algorithm | v2 | Pagination algorithm: v1 (pre-measure) or v2 (fill-overflow) |
JavaScript Options
const options = {
// Page dimensions
pageSize: 'A4', // 'A4' | 'Letter' | 'Legal'
pageWidth: 794, // pixels
pageHeight: 1123, // pixels
orientation: 'portrait', // 'portrait' | 'landscape'
// Padding (number for all sides, or object)
padding: 80,
// padding: { top: 80, right: 60, bottom: 40, left: 60 },
// Typography rules
orphanLines: 2, // min lines at page bottom
widowLines: 2, // min lines at page top
minContentLines: 3, // min content after heading
// Tables
repeatTableHeader: false, // repeat <thead> on continuation
minRowsForSplit: 2, // min rows to allow table split
// Lists
minItemsForSplit: 2, // min items to allow list split
};API Reference
<folio-pages> Element
Methods
const folio = document.querySelector('folio-pages');
// Re-run pagination (e.g., after content or style changes)
folio.paginate();
// Force refresh (re-captures content and paginates)
folio.refresh();
// Generate standalone HTML for PDF conversion
const html = folio.toPrintHTML({
title: 'My Document', // <title> for the HTML
includeStyles: true, // include all CSS (default: true)
});Events
folio.addEventListener('paginated', (event) => {
console.log(`Total pages: ${event.detail.totalPages}`);
console.log('Pages:', event.detail.pages);
console.log('Options used:', event.detail.options);
});
folio.addEventListener('error', (event) => {
console.error('Pagination failed:', event.detail);
});paginate() Function
For direct JavaScript usage without the web component:
import { paginate } from '@rizkyandriawan/foliojs';
const result = await paginate(containerElement, options);
// result.pages - Array of Page objects
// result.totalPages - Number of pages
// result.options - Resolved options usedHow It Works
V2 Algorithm (Default)
Folio 0.2.0 introduces a simpler "fill until overflow" approach:
- Add elements one by one to the current page
- Check overflow after each addition using
scrollHeight - Split or move when overflow is detected
- Recurse into containers (lists, divs) adding children individually
This is more accurate than pre-measuring because it uses the browser's actual layout calculations. For nested lists, Folio only measures after adding atomic content (<li> text), not after adding container elements (<ul>, <ol>).
To use the older pre-measure algorithm: <folio-pages algorithm="v1">.
Element Handlers
Folio categorizes every element and applies appropriate splitting logic:
| Type | Elements | Behavior |
|------|----------|----------|
| Heading | h1-h6 | Never orphaned; keeps min content after |
| Prose | p | Splits respecting orphan/widow rules |
| Code | pre, code | Splits between lines with continuation markers |
| List | ul, ol | Splits between items (min 2 items per page) |
| Table | table | Splits between rows, optionally repeats header |
| Container | blockquote, div, section | Splits between children |
| Atomic | img, hr, svg, video, canvas | Never splits |
Smart Heuristics
Headings + Content: A heading won't be left alone at the bottom of a page. Folio ensures at least minContentLines (or 1/3 of the next block) follows the heading, or moves everything to the next page.
Orphans & Widows: Paragraphs won't leave just 1-2 lines stranded at the bottom (orphan) or top (widow) of a page. Folio adjusts split points to maintain readability.
Tables: Tables split between rows, never mid-row. With repeat-table-header, the <thead> appears on each continuation page.
Code Blocks: Code splits between actual lines (not mid-character), with visual indicators showing continuation.
Images: Paragraphs containing images are treated as atomic—they move to the next page if they don't fit rather than being clipped.
PDF Generation
Folio outputs paginated HTML. To convert to PDF, use the toPrintHTML() method and pass the result to your preferred PDF generator:
Browser Print
const html = folio.toPrintHTML({ title: 'My Document' });
const win = window.open('', '_blank');
win.document.write(html);
win.document.close();
win.print();Download HTML
const html = folio.toPrintHTML({ title: 'My Document' });
const blob = new Blob([html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'document.html';
a.click();
URL.revokeObjectURL(url);Server-Side PDF (Puppeteer)
// Server-side with Puppeteer
const puppeteer = require('puppeteer');
async function htmlToPdf(html) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({
printBackground: true,
preferCSSPageSize: true,
});
await browser.close();
return pdf;
}WeasyPrint (Python)
from weasyprint import HTML
def html_to_pdf(html_string, output_path):
HTML(string=html_string).write_pdf(output_path)CSS Tips
Styling Paginated Content
Target your styles to the folio-pages element:
folio-pages {
font-family: Georgia, serif;
font-size: 11pt;
line-height: 1.6;
color: #333;
}
folio-pages h1 {
font-size: 24pt;
margin-bottom: 0.5em;
}
folio-pages pre {
background: #f5f5f5;
padding: 1em;
overflow-x: auto;
}Force Page Breaks
Use CSS break-before or the data-folio-break-before attribute:
.chapter {
break-before: page;
}<h1 data-folio-break-before>Chapter 2</h1>Prevent Element Splitting
Use CSS break-inside or the data-folio-keep-together attribute:
.keep-together {
break-inside: avoid;
}<figure data-folio-keep-together>
<img src="chart.png">
<figcaption>Important chart</figcaption>
</figure>Page Sizes
| Preset | Width | Height | Common Use |
|--------|-------|--------|------------|
| A4 | 794px | 1123px | International standard |
| Letter | 816px | 1056px | US standard |
| Legal | 816px | 1344px | US legal documents |
All sizes assume 96 DPI. For custom sizes, use page-width and page-height attributes.
Browser Support
Folio works in any browser that supports:
- ES2020 (async/await, optional chaining)
- Custom Elements v1
offsetHeight,getComputedStyle
This includes all modern browsers (Chrome, Firefox, Safari, Edge) and can be used in Node.js with a DOM implementation like JSDOM.
Framework Integration
React
import '@rizkyandriawan/foliojs';
import { useRef, useEffect } from 'react';
function Document({ content }) {
const folioRef = useRef(null);
useEffect(() => {
const folio = folioRef.current;
const handlePaginated = (e) => {
console.log(`Rendered ${e.detail.totalPages} pages`);
};
folio.addEventListener('paginated', handlePaginated);
return () => folio.removeEventListener('paginated', handlePaginated);
}, []);
return (
<folio-pages ref={folioRef} page-size="A4">
<div dangerouslySetInnerHTML={{ __html: content }} />
</folio-pages>
);
}Vue
<template>
<folio-pages page-size="A4" @paginated="onPaginated">
<slot />
</folio-pages>
</template>
<script setup>
import '@rizkyandriawan/foliojs';
const onPaginated = (event) => {
console.log(`Rendered ${event.detail.totalPages} pages`);
};
</script>Comparison
| Feature | Folio | Paged.js | CSS Regions | |---------|-------|----------|-------------| | Approach | DOM measurement | CSS polyfill | CSS polyfill | | Setup complexity | Minimal | Moderate | High | | Custom CSS support | Full | Partial | Partial | | Table splitting | Yes | Limited | No | | Code block splitting | Yes | No | No | | Bundle size | ~15KB | ~100KB | ~80KB |
Credits
Built for Cygnus MD, extracted because others might find it useful.
Heuristics informed by studying:
- WeasyPrint — Python, excellent documentation
- Chrome's LayoutNG — comprehensive fragmentation logic
License
MIT
