Will my template work?
vellora renders the HTML and CSS that documents use: text, tables, page rules, images and fonts. When a template uses something outside that, it doesn't guess. It stops and tells you where.
Works
- Works:Text, headings, lists and tables
- Works:Tables over several pages, with the header row repeated
- Works:Page size and margins from @page
- Works:Page X of Y in headers and footers
- Works:Page breaks before, after or inside blocks and table rows
- Works:CSS in a style tag or a style attribute
- Works:Images and fonts you pass in as bytes, or images as data: URLs
- Works:@media print rules
- Works:PDF/A-2b for archiving
Works with limits
- Works with limits:Flexbox: common layouts work; tables are the safer choice
- Works with limits:Floats and multi-column text, with a few edge cases
- Works with limits:Headers and footers only in the top-center and bottom-center slots
- Works with limits:Inline SVG draws, but the page’s CSS doesn’t style the SVG
Not supported
- Not supported:JavaScript: script tags are rejected, the page never runs
- Not supported:CSS grid, transform, animation, transition and filter
- Not supported:External stylesheets (link rel=stylesheet, @import): rejected with the line and column
- Not supported:Remote images and @font-face URLs: nothing is fetched
- Not supported:Forms, iframes, canvas, video and audio
- Not supported:Windows and Alpine Linux
A few rarer limits, and the details for each item, are in The full list below.
When something is unsupported, you get the line and column
Say your template has <p style="transform: rotate(1deg)">. renderPdf rejects it before making a PDF:
VelloraUnsupportedError: Unsupported construct "css:transform" at line 1, column 11: this CSS feature is outside vellora's document subset; run `vellora fix`The error also carries the same details as properties, so your code or your CI can read them:
import { renderPdf, VelloraUnsupportedError } from "vellora";
try {
await renderPdf('<p style="transform: rotate(1deg)">Hi</p>');
} catch (err) {
if (err instanceof VelloraUnsupportedError) {
console.log(err.feature, err.line, err.col); // css:transform 1 11
}
}You find out on your machine, when you render the template, not from a customer who got a broken PDF.
Where your CSS, images and fonts come from
vellora loads nothing from the network or the disk on its own. Everything the PDF needs is in the HTML string or in the render options:
- CSS goes in a
<style>tag or astyle=""attribute. A<link rel="stylesheet">or@import url(...)is rejected ascss:external-stylesheet, with the line and column, instead of printing the PDF unstyled. - Images are
data:URLs, or bytes you pass in theimagesoption, keyed by thesrcin your HTML. See Logos & images. - Fonts are bytes you pass in the
fontsoption, used by the family name inside the font file. vellora also bundles its own fonts and never uses the fonts installed on the machine. See Fonts.
import { readFileSync } from "node:fs";
import { renderPdf } from "vellora";
const logoUrl = "https://cdn.example.com/logo.png";
const html = `<style>body { font-family: "Inter"; }</style>
<img src="${logoUrl}" width="120" height="40">
<h1>Invoice</h1>`;
const pdf = await renderPdf(html, {}, {
images: { [logoUrl]: new Uint8Array(await (await fetch(logoUrl)).arrayBuffer()) },
fonts: [readFileSync("Inter-Regular.ttf")],
});My template uses X. What now?
| Your template has | What to do |
|---|---|
| CSS grid | Rewrite that part with a table (or flexbox for simple rows). For display: grid or display: flex on a table cell, vellora fix (or strict: false) rewrites it into a nested table for you. |
transform, animations, transitions, filters | Remove them. Animations and transitions do nothing on paper anyway. For a rotated stamp or a filtered logo, use an image of it. |
| JavaScript that fills in values or draws a chart | Compute the values in Node and pass them as data. Draw charts as inline SVG or pass them as images. |
| A remote image URL | Fetch it yourself and pass the bytes in images, keyed by that URL. |
@font-face with a URL | Pass the font file's bytes in fonts and use the family name inside the file. |
| A CDN stylesheet (Tailwind, Bootstrap) | Inline the built CSS in a <style> tag. |
HTML already rendered by another engine, with literal {{ or {% in it | Pass { template: false } (CLI: --no-template) so the HTML goes to the renderer untouched, or wrap that text in {% raw %}…{% endraw %}. Without either, it is read as template syntax. |
A header or footer in a corner slot (@top-left, @bottom-right, …) | Move it to @top-center or @bottom-center. |
| Something that really needs a browser | Send that one template to Chrome with the optional Chromium engine. Keep the rest native. |
Fix it automatically, or check it in CI
The error hint says run vellora fix. The fixer handles a few patterns: grid or flex on a table cell, images without a size, inline SVG it can turn into an image, and broken markup. Most other rejections, like transform, you remove by hand.
strict: falseruns the same fixers on the HTML before rendering. The default,strict: true, never changes your HTML.jsconst pdf = await renderPdf(html, data, { strict: false });vellora lintandvellora fixfrom@vellora/cli(0.3.2 or newer) check or rewrite template files in CI. The lint does not catch everything the renderer rejects (an inlinetransform, for example), so the surest CI check is a strictrenderPdfof each template with sample data.
The full list
A few more limits that only some templates hit:
- Page breaks:
break-before,break-afterandbreak-inside: avoidwork on blocks, wrappers and table rows.orphansandwidowsare ignored. @page :first,:leftand:rightwork if they don’t change the left or right margin.- Named pages (
page: name) start a new page, but their own@pagerules are ignored. - A block taller than a page splits across pages, mostly as Chrome does.
- Inline SVG: shapes, paths, gradients and text draw; the page’s own CSS doesn’t style SVG elements.
Every element and CSS feature, with its exact status and notes, is in COMPATIBILITY.md. It is generated from the renderer's source and checked in CI, so it matches what the code accepts.
Next: Check one of your templates with the migration script →