Takumi

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.

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-js

takumi-js ships two backends and picks one at runtime. You install nothing else.

RuntimeBackend
Node.js, Bunnative (@takumi-rs/core)
Cloudflare Workers, edge runtimes, Deno, browsersWebAssembly (@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.js

When Vite bundles your server, add the condition to its server conditions and bundle Takumi into the server build:

vite.config.ts
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:

build.mjs
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:

Create a WebAssembly renderer
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.

image.tsx
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", );
  • width and height set 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.

app/og/route.tsx
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.

Load a variable font
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