Introduction
Takumi lays out JSX, HTML, and CSS with one engine and turns the result into images, PDFs, or paint trees, without a browser.
Takumi is one layout engine, with text and inline layout ported from Chromium. It turns JSX, HTML, and CSS into images, PDFs, and other outputs without a headless browser.
Pick a package
Each package runs the same layout engine. Pick the one that produces your output.
Images and animations
takumi-js renders PNG, JPEG, WebP, SVG, and animated WebP, APNG, or GIF. Open Graph images are one use.
PDF documents
takumi-pdf writes paged documents such as invoices, reports, and tickets.
Paint
takumi-paint returns every box, text run, and image with its position. Draw them with Canvas, slides, or a native API, or read the layout.
Command line
The takumi CLI renders an HTML file to an image or a PDF from a terminal or a script, without writing code.
The rest of this page uses takumi-js.
For how a render goes from input to output, and where it differs from a browser, see How Takumi works.
Install
npm i takumi-jstakumi-js ships two backends and picks one at runtime. You install nothing else.
| Runtime | Backend |
|---|---|
| Node.js, Bun | native (@takumi-rs/core) |
| Cloudflare Workers, edge runtimes, Deno, browsers | WebAssembly (@takumi-rs/wasm) |
Use WebAssembly in Node.js
Set the takumi-wasm export condition to render on WebAssembly in Node.js and Bun too. Use it when the native addon cannot ship with your server, such as a serverless function bundled into one file. Without a bundler, pass the condition to Node.js:
node --conditions=takumi-wasm server.jsWhen Vite bundles your server, add the condition to its server conditions and bundle Takumi into the server build:
import { defaultServerConditions, defineConfig } from "vite";
export default defineConfig({
ssr: {
noExternal: ["takumi-js", "@takumi-rs/wasm", "@takumi-rs/helpers"],
resolve: { conditions: ["takumi-wasm", ...defaultServerConditions] },
},
build: { ssrEmitAssets: true },
});ssrEmitAssets writes the WebAssembly binary next to the server bundle, where Takumi reads it at runtime. Without it, the binary goes only to the client build. Takumi then also looks in a client directory next to the server output, and fails with Unable to locate Takumi WASM asset for SSR if the binary is not there either.
With esbuild, add module after takumi-wasm, and copy the binary with the file loader:
import { build } from "esbuild";
await build({
entryPoints: ["src/server.ts"],
bundle: true,
platform: "node",
format: "esm",
outdir: "dist",
conditions: ["takumi-wasm", "module"],
loader: { ".wasm": "file" },
});Custom conditions replace the module condition esbuild sets by default. Without module, the bundle keeps a loader that reads the binary from node_modules and fails with ENOENT once it runs from dist.
render and ImageResponse load the binary on their first call. To create a Renderer from takumi-js/wasm yourself, await ready() first:
import { , } from "takumi-js/wasm";
await ();
const = new ();ready() loads the binary the export conditions pick, on every runtime. Only the first call loads it, and later calls wait for that load. A failed load is retried on the next call.
Render an image
render takes JSX and returns encoded image bytes. Save this as image.tsx and run bun image.tsx. In Node.js, compile the JSX first.
import { } from "takumi-js";
import { } from "node:fs/promises";
const = await (
<
={{
: 72,
: "linear-gradient(to bottom right, #fff7ed, #fecaca)",
}}
="w-full h-full flex items-center justify-center"
>
Hello Takumi
</>,
{ : 1200, : 630 },
);
await ("hello.png", );widthandheightset the image size. Leave one out to size that side to the content.tw="w-full h-full"makes the root fill the image.
Serve it over HTTP
ImageResponse extends the web Response. Return it from any route handler that uses Request and Response. Its API matches next/og.
import { } from "takumi-js/response";
export function () {
return new (< ="w-full h-full grid place-items-center">Hello Takumi</>, {
: 1200,
: 630,
});
}Takumi never reads system fonts. The only built-in font is Geist, which covers Latin at weights
300 to 800. Load fonts for other scripts through fonts, or their glyphs go missing. See
Fonts.
Load a font
googleFonts loads Google Fonts families in one request. A weight range or an axes entry loads the variable font. Then font-weight and font-variation-settings set its axes per element.
import { } from "takumi-js";
import { } from "takumi-js/helpers";
const = await (
<
={{
: 72,
: "Fraunces",
: "'opsz' 72, 'wght' 700",
}}
="w-full h-full flex items-center justify-center"
>
Hello Takumi
</>,
{
: 1200,
: 630,
: ([
{
: "Fraunces",
: "100..900",
: { : "9..144" },
},
]),
},
);fonts also takes a font URL, raw bytes, or a descriptor. To load fonts before the first request, register them on a Renderer.
Coming from satori
A bare <div> lays out as a block, as in a browser. Keep explicit Flexbox styles when you move a Satori template, and compare the output. renderSvg() returns SVG like satori() does. render() returns encoded image bytes. See the Satori comparison and migration guide.
Next steps
Last updated on