Takumi
Render OG images and paged PDFs from JSX, HTML, and CSS. No headless browser.
Takumi renders images in Node.js, Cloudflare Workers, browsers, and Rust applications. The PDF package runs in Node.js, Bun, and Cloudflare Workers.
Quick Start
bun i takumi-js # PNG, JPEG, WebP, SVG, animations
bun i takumi-pdf # paged PDF
Image
import { render } from "takumi-js";
import { writeFile } from "node:fs/promises";
const image = await render(
<div tw="w-full h-full flex items-center justify-center bg-gradient-to-b from-blue-100 to-red-50">
<h1 tw="text-6xl font-bold">Hello from Takumi</h1>
</div>,
{ width: 1200, height: 630 },
);
await writeFile("./output.png", image);
import { render } from "takumi-pdf";
import { PageNumber, TotalPages } from "takumi-pdf/primitives";
import { writeFile } from "node:fs/promises";
const pdf = await render(<Invoice data={data} />, {
size: "a4",
footer: (
<div tw="flex w-full justify-center text-[10px] text-gray-500">
Page <PageNumber /> of <TotalPages />
</div>
),
});
await writeFile("invoice.pdf", pdf);
Features
Takumi is a Rust rendering engine for markup and CSS. It handles layout, text shaping, compositing, and encoding without launching a browser. One component tree renders as an image, an animation, or a paged PDF.
Images
text-fitgrows or shrinks a headline to fill its line box, no measuring loop.text-wrap: balanceevens multi-line headlines.prettyspares the orphan word.- Animations sample the tree across time.
@keyframesandanimate-spinbecome WebP, APNG, GIF, or video frames. - Stylesheets apply as written, with complex selectors,
var(),calc(), and media queries. - CSS Grid, block, inline, and float handle layout.
langpicks each language's own Han glyphs for the same code points.- Text on a path follows
offset-path.background-clip: textand conic gradients paint it. - Masks,
clip-path,backdrop-filter, and blend modes composite the way browsers do. - SVG filters run through
filter: url(...),feTurbulenceandfeDisplacementMapincluded. corner-shapeswaps round corners forsquircle,bevel,scoop,notch, orsuperellipse(n).- Tailwind v4 utilities apply directly, arbitrary values included.
- Page breaks honor
break-before: page,break-after: page, andbreak-inside: avoid. - Widows and orphans default to 2, keeping lone lines away from page breaks.
- Headers and footers repeat on every page.
<PageNumber />and<TotalPages />count in CSS counter styles,trad-chinese-informalincluded. - Tables share column widths across pages and repeat their
<thead>on every page. - Text stays selectable and searchable. Fonts embed as subsets.
- Links and metadata carry into the output.
outline: truebuilds bookmarks from headings. - Attachments embed files, including Factur-X e-invoice XML.
- Tagged PDF is on by default. PDF/A-2, A-3, A-4, and PDF/UA-1 pass veraPDF.
- Arabic, bidi text, CJK, and emoji shape correctly.
- 1.5 MB of gzip wasm fits the Cloudflare Workers free plan.
Runtimes
- Node.js and Bun load a native binding, prebuilt for macOS, Linux (glibc and musl), and Windows on x64 and ARM64.
- Cloudflare Workers and browsers load the WebAssembly build.
- Rust applications embed the
takumicrate.
Coming From Something Else
| You are using | What changes |
|---|---|
satori | Replace satori() with renderSvg(), or call render() for encoded image bytes. Compare the renderers. |
next/og | Swap the ImageResponse import. Existing Satori-compatible templates keep their explicit Flexbox styles. Read the migration guide. |
| Puppeteer or Playwright for PDFs | Replace page.pdf() with render(). You must preload remote assets, and Takumi supports less CSS than Chrome. Read the migration guide. |
@react-pdf/renderer | Replace Document, View, and Text with HTML elements and CSS. Browser viewers and some text controls have no equivalent. Read the migration guide. |
pdfkit | Use JSX or HTML instead of positioning each line. Keep pdfkit when you need low-level drawing control. |
More Output
Fonts
Only a last-resort Latin font ships built in. Load the rest through fonts: a URL, raw bytes, or googleFonts. A weight range or an axes entry loads the variable font, so font-variation-settings drives its axes:
import { render } from "takumi-js";
import { googleFonts } from "takumi-js/helpers";
const image = await render(
<div
tw="w-full h-full flex items-center justify-center"
style={{
fontSize: 72,
fontFamily: "Fraunces",
fontVariationSettings: "'opsz' 72, 'wght' 700",
}}
>
Hello from Takumi
</div>,
{
width: 1200,
height: 630,
fonts: googleFonts([{ name: "Fraunces", weight: "100..900", axes: { opsz: "9..144" } }]),
},
);
Rendering many images? Register the fonts once on a Renderer and reuse it. See Typography & Fonts.
API route (next/og-compatible)
import { ImageResponse } from "takumi-js/response";
export function GET() {
return new ImageResponse(
<div tw="w-full h-full flex items-center justify-center bg-gradient-to-b from-blue-100 to-red-50">
<h1 tw="text-6xl font-bold">Hello from Takumi</h1>
</div>,
{ width: 1200, height: 630 },
);
}
Animated WebP
import { renderAnimation } from "takumi-js";
import { writeFile } from "node:fs/promises";
const animation = await renderAnimation({
width: 400,
height: 400,
fps: 30,
format: "webp",
scenes: [
{
durationMs: 1000,
node: (
<div tw="w-full h-full flex items-center justify-center">
<div tw="w-32 h-32 bg-blue-500 animate-spin rounded-lg" />
</div>
),
},
],
});
await writeFile("./output.webp", animation);
Vector SVG
import { renderSvg } from "takumi-js";
import { writeFile } from "node:fs/promises";
const svg = await renderSvg(
<div tw="w-full h-full flex items-center justify-center bg-gradient-to-b from-blue-100 to-red-50">
<h1 tw="text-6xl font-bold">Hello from Takumi</h1>
</div>,
{ width: 1200, height: 630 },
);
await writeFile("./output.svg", svg);
Rust
cargo add takumi
Start from the Rust example.
Comparison
Images
| Feature | next/og (Satori) | Takumi |
|---|---|---|
| Runtime | Node / Edge | Node, Edge, CF Workers, Browser, Rust crate |
| Template input | JSX / React | JSX, HTML strings, JSON node trees from any language |
| Layout | Flexbox | Flexbox, CSS Grid, block, inline, float |
| Selectors | Limited | Complex selectors, :is(), :where(), ::before, ::after |
backdrop-filter, blend modes | ✗ | ✅ |
| Animated output | ✗ | WebP / APNG / GIF / video frames |
| Vector SVG output | ✅ Native | ✅ Plus raster and animated output |
| Headless browser | ✗ | ✗ |
ImageResponse API | ✅ Native | ✅ Compatible |
Compare rendering output across providers at image-bench.kane.tw.
The benchmark uses an 80-line invoice with two pages and a page-number footer. Warm figures are the median of 20 renders. The environment was an Apple M1 Pro with macOS 15.7.4, Bun 1.3.14, and Chrome 151. The bench source reproduces the table.
| takumi-pdf 0.4 | @react-pdf/renderer 4.5.1 | Puppeteer + Chrome | |
|---|---|---|---|
| Cold start to first PDF | 176 ms | 495 ms | 0.7 to 2.8 s |
| Warm render (median) | 26 ms | 236 ms | 198 ms |
| Output size | 19 KB (15 KB with tagged: false) | 16 KB | 52 KB |
| Deploy needs | 1.5 MB gzip wasm | pure JS | Chrome install (hundreds of MB) |
| Template language | JSX, HTML, node trees with CSS and Tailwind | its own primitives (<View>, <Text>, StyleSheet) | HTML with full CSS |
| Selectable text, subset fonts | yes | yes | yes |
| Arabic and bidi text | yes | shaping only, manual direction | yes |
| Runs on edge runtimes | yes (Cloudflare Workers) | no (Node) | no |
Chrome has the most complete CSS support of the three. Takumi's PDF output does not support filter: blur(), drop-shadow(), or backdrop-filter. Use Puppeteer when you need to reproduce a complex web page pixel for pixel. See the PDF comparison for install sizes and more caveats.
Who's Using Takumi
- Dcard renders post share images
- TanStack renders OG images for its docs
- Fumadocs generates its docs OG images
- Nuxt OG Image ships Takumi as a built-in renderer
- Luma renders event share images
- shiki-image turns syntax-highlighted code into images
More projects in the showcase. Takumi is part of the Vercel OSS Program.
Core Architecture
Takumi converts any template into a node tree with three node kinds: container, image, and text. That tree runs through:
- Layout via taffy: Flexbox, Grid, block, float,
calc(), absolute positioning, z-index - Text shaping via parley and skrifa: WOFF/WOFF2 fonts, emoji, RTL, multi-span inline blocks
- Compositing: stacking contexts, blend modes, filters, transforms, SVG via resvg
- Output: PNG, JPEG, WebP, ICO for statics; GIF, APNG, WebP for animations; paged PDF; raw RGBA frames for video pipelines
- Any template system that serializes to HTML or JSON can feed the node tree: React, Svelte, Vue, plain strings, or your own serializer in any language.
- A time axis threads through the pipeline. A PNG is the tree at
t=0. A GIF samples the same tree acrosst. CSS@keyframes, theanimationshorthand, and Tailwind animation utilities resolve at render time. - The same layout drives two vector backends:
renderSvg()for Satori-style SVG,takumi-pdffor paged PDF.
flowchart LR
A[Templates] --> N[Node Tree] --> P[Rendering Pipeline]
C[Stylesheets] --> P
R[Resources] --> P
D(Time Axis) -.-> P
P --> F[(Raw Pixels)]
P --> S[Vector SVG]
P --> V[Paged PDF]
F --> G[PNG / JPEG / WebP / ICO]
F --> H[GIF / APNG]
F --> I[Video frames]
Showcase
| Takumi OG image (source) | Package OG card (source) |
|---|---|
![]() | ![]() |
| Prisma-style API card (source) | X-style social post (source) |
![]() | ![]() |
| Keyframe Animation (source) | shiki-image |
![]() |
See more examples for invoices, e-invoices, Next.js, Cloudflare Workers, TanStack Start, Svelte, Rust, and ffmpeg keyframe animation.
Contributing
Read CONTRIBUTING.md for local setup and the fixture workflow.
We welcome bug reports, feature requests, doc improvements, and new example integrations.
License
MIT or Apache-2.0




