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.
Last updated on