Render JSX to images.
Skip the browser.

Takumi parses CSS, lays out the tree, shapes text, and encodes pixels in a single Rust binary. Headless Chromium spends 300 MB and a cold start on an OG card. Takumi spends a function call.

Takumi OG image rendered by TakumiX post image rendered by TakumiPrisma-style OG image rendered by Takumi

Output from example/twitter-images — every image on this page was rendered by Takumi.

The code is the design file.

ImageResponse is drop-in compatible with next/og. This component is the exact source of the card next to it.

export default function DemoCard() {
  return (
    <div tw="flex h-full w-full flex-col justify-between bg-[#16130f] p-14 text-white">
      <div tw="flex items-center justify-between">
        <span tw="text-2xl text-[#a8a29a]">takumi.kane.tw</span>
        <span tw="h-10 w-10 bg-[#ff4d4d]" />
      </div>
      <h1
        tw="text-7xl font-bold leading-tight"
        style={{
          backgroundClip: "text",
          backgroundImage: "linear-gradient(110deg, #fff 60%, #ff4d4d)",
          color: "transparent",
        }}
      >
        This card is the code beside it.
      </h1>
      <div tw="flex items-center justify-between text-2xl text-[#a8a29a]">
        <span>Rendered without a browser</span>
        <span>1200 × 630</span>
      </div>
    </div>
  );
}
The card rendered from the source on the left

home-demo-card@2x.webp · 52 KB · rendered by bun example/twitter-images

One tree, sampled across time.

The renderer takes a timestamp. A PNG is your tree at t = 0. An animated WebP is the same tree sampled across t. CSS @keyframes and Tailwind animate-* utilities resolve at render time.

@keyframes morph {
  from { border-radius: 12%; transform: rotate(0deg) scale(1); }
  50%  { border-radius: 50%; transform: rotate(90deg) scale(0.72); }
  to   { border-radius: 12%; transform: rotate(180deg) scale(1); }
}
Frame sampled at t=0ms
t=0
Frame sampled at t=125ms
t=125
Frame sampled at t=250ms
t=250
Frame sampled at t=375ms
t=375
Frame sampled at t=500ms
t=500
Frame sampled at t=625ms
t=625
Frame sampled at t=750ms
t=750
Frame sampled at t=875ms
t=875

The CSS you actually write.

Support reaches past the usual OG-image subset. If your generator made you remove a property, put it back.

Layout

  • display: grid
  • float
  • position: absolute
  • calc()
  • z-index

Selectors

  • :is()
  • :where()
  • ::before
  • ::after

Paint

  • backdrop-filter
  • mix-blend-mode
  • conic-gradient()
  • clip-path
  • mask
  • background-clip: text

Text

  • WOFF2 fonts
  • emoji
  • RTL scripts
  • multi-span inline

Motion

  • @keyframes
  • animation
  • Tailwind animate-*

Runs as a native Node.js binding, a WASM build for Cloudflare Workers and browsers, and a Rust crate. Prebuilt for macOS, Linux, and Windows on x64 and ARM64.

In production.

Dcard renders post share images with it, Fumadocs generates its docs OG images, and Nuxt OG Image ships it as a built-in renderer.

OG image from dcard.twOG image from fumadocs.devOG image from github.comOG image from www.alfanjauhari.comOG image from who-to-bother-at.comOG image from image-bench.kane.twOG image from shotwell.appOG image from prmpt.bio

View all projects →

Render your first image.

bun i takumi-js

Layout by taffy · text by parley & skrifa · SVG by resvg

MIT / Apache-2.0