You're reading the v2 beta docs. For the stable release, switch to v1 →
TakumiTakumi

Typography & Fonts

Load fonts, route scripts across them, and style text.

Takumi never reads system fonts. Load every font a render needs, or its glyphs fall back to tofu.

@takumi-rs/core (napi) embeds full-axis Geist and Geist Mono. @takumi-rs/wasm embeds one Latin font, Manrope. Add your own family if you need monospace there.

Loading fonts

The fonts option

Pass the fonts a render needs through fonts. Each entry is raw bytes, or a { name, data, weight, style } descriptor. data can be a loader that returns the bytes. The renderer deduplicates them, so a reused renderer decodes each file once.

import {  } from "takumi-js/response";

export function () {
  return new (< />, {
    : [
      {
        : "Inter",
        : () => ("/path-to-inter.woff2").(() => .()),
      },
    ],
  });
}

render, renderAnimation, and encodeFrames take the same fonts option.

From Google Fonts

googleFont fetches a family from Google Fonts and returns fonts entries. You get one lazy loader per woff2 file. Each file downloads only when the renderer first needs it. Use the family by its name in font-family.

import { render } from "takumi-js";
import { googleFont } from "takumi-js/helpers";

const image = await render(<div style={{ fontFamily: "Inter" }}>Hello</div>, {
  width: 1200,
  height: 630,
  fonts: await googleFont("Inter", { weight: [400, 700] }),
});

weight takes one weight, an array, or a range like "100..900". A range loads the variable font and lets CSS font-weight drive it. style is "normal", "italic", or both. text downloads only the glyphs that string uses, which helps OG images where you know the text up front. display maps to CSS font-display.

fonts: await googleFont("Inter", { weight: 700, style: "italic", text: title }),

Multilingual content

Sometimes you can't predict which scripts appear. googleFontSubsets loads only the subsets the content needs. Give it your content and the families to draw from. It scans the text, fetches each family's metadata in one request, and keeps the subsets that match.

import { render } from "takumi-js";
import { fromJsx } from "takumi-js/helpers/jsx";
import { googleFontSubsets } from "takumi-js/helpers";

const element = <div style={{ fontFamily: "Inter" }}>Hello 你好 こんにちは</div>;

const { node } = await fromJsx(element);
const fonts = await googleFontSubsets(node, ["Inter", "Noto Sans JP", "Noto Sans TC"]);

const image = await render(node, { width: 1200, height: 630, fonts });

Each subset registers under its own internal name. font-family: Inter still expands across all of them, so each script finds the file that covers it. Pass a Map as cache to reuse the metadata across renders. This helps a playground that re-renders on every edit.

const fonts = await googleFontSubsets(node, ["Inter"], { cache });

Preloading with registerFont

registerFont is the escape hatch for preloading. Register a font on a renderer up front, outside the request path. Then reuse that renderer. It takes the same entries as fonts and returns the families it made.

const renderer = new Renderer();
await renderer.registerFont({ name: "Inter", data: inter });

return new ImageResponse(<OgImage />, { renderer });

Choosing a font

Fallback chain

fontFamilies is the ordered list of families to try when a glyph is missing. It defaults to all registered families, in registration order. Set it to pin which family wins and what backs it up.

const image = await renderer.render(node, {
  fontFamilies: ["Inter", "Noto Sans JP"],
});

A missing glyph walks the chain until a family covers it. googleFontSubsets builds on this: each subset registers under its own internal name, while one logical family name in font-family expands across them.

Variable axes & OpenType features

Control font axes with font-variation-settings. Control OpenType features with font-feature-settings. For variable fonts, font-weight is shorthand for the wght axis, and font-stretch drives wdth.

<div
  style={{
    fontFamily: "Manrope",
    fontVariationSettings: "'wght' 700, 'wdth' 150",
    fontFeatureSettings: "ss01",
  }}
>
  Variable Font Text
</div>

Synthetic bold & italic

When a family lacks the weight or style you asked for, Takumi fakes a bold or oblique. font-synthesis turns that off. Missing weights then stay regular.

<div style={{ fontFamily: "Inter", fontSynthesis: "none" }}>No faux bold</div>

Pass weight, style, or both to allow each one. none disables them. Emoji never get a fake bold.

Styling text

Color, stroke, and fill

color accepts modern CSS color spaces: rgb, hsl, oklch, lab, display-p3, and more. Outline glyphs with -webkit-text-stroke. Set a separate fill with -webkit-text-fill-color.

<div
  style={{
    color: "oklch(0.7 0.15 200)",
    WebkitTextStroke: "2px black",
    WebkitTextFillColor: "white",
  }}
>
  Outlined
</div>

The stroke color defaults to color when you omit it.

Decoration & transform

text-decoration draws underline, line-through, and overline. Combine them, and set color and thickness. text-transform changes the case.

<div
  style={{
    textDecoration: "underline overline",
    textDecorationColor: "red",
    textTransform: "uppercase",
  }}
>
  Marked up
</div>

Shadow

text-shadow takes offset, blur, and color. Stack layers with commas.

<div style={{ textShadow: "2px 2px 4px rgba(0,0,0,0.5), 0 0 8px blue" }}>Glow</div>

Spacing & alignment

letter-spacing, word-spacing, line-height, text-indent, and text-align (including justify) work as in the browser.

<div
  style={{
    letterSpacing: "0.05em",
    lineHeight: 1.4,
    textAlign: "justify",
    textIndent: "2em",
  }}
>
  Body copy
</div>

Flowing text

Wrapping

Takumi supports balance and pretty wrapping, adapted from satori. balance evens out line lengths. pretty stops the last line from stranding one word.

<div style={{ textWrap: "balance" }}>Super Long Text</div>

word-break and overflow-wrap decide where a long token splits. break-all, keep-all, and anywhere cover CJK and long URLs.

Truncation

When text-overflow is ellipsis, Takumi truncates at the line-clamp count or the container's max height, whichever comes first. You don't need white-space: nowrap, so multiline ellipsis works.

<div
  style={{
    textOverflow: "ellipsis",
    lineClamp: 3,
  }}
>
  Super Long Text
</div>

Fit to container

text-fit scales text to fit its line box instead of wrapping or overflowing. The mode comes first: grow, shrink, or none. An optional target and percentage cap follow.

<div style={{ textFit: "shrink" }}>Headline that always fits</div>
<div style={{ textFit: "grow per-line 150%" }}>Per-line scaled, capped at 150%</div>

consistent (default) uses one scale for the block. per-line scales each line except the last. per-line-all scales every line. The percentage caps how far the scale can move.

RTL & bidirectional text

Takumi handles Right-to-Left scripts like Arabic and Hebrew on its own, including mixed runs. There's no manual override for text direction yet (see issue #330). The direction property controls layout, not the text run.

Emoji rendering lives on its own page: Images & emoji.

Edit on GitHub

Last updated on

On this page