Takumi

Fonts

Load local or Google Fonts, choose fallbacks for multilingual text, and control typography in Takumi.

Load a font for every script your text uses.

Takumi never reads system fonts. The only built-in font is Geist, which covers Latin at weights 300 to 800 and comes last in every fallback chain. Generic families such as sans-serif resolve to the fonts you load.

Loading fonts

The fonts option

Pass the fonts a render needs through fonts. Each entry takes one of three forms:

EntryResolves to
URL stringFetched on demand, keyed by the URL. Name, weight, and style come from the file.
Raw bytesA Uint8Array, ArrayBuffer, or Buffer, registered directly.
Descriptor{ name, data, weight, style, generic }. The fields override what the file reports.

render, renderSvg, renderAnimation, measure, and ImageResponse all take fonts. It also takes a promise of the list, so pass googleFonts(...) without await.

The renderer decodes each file once and keeps it until you release it, or until fontCacheMaxBytes evicts it.

Load a font URL
import {  } from "takumi-js/response";

export function () {
  return new (<>Hello Takumi</>, {
    : ["https://example.com/Inter.woff2"], 
  });
}

A descriptor's data is raw bytes or a function that returns them:

Override font metadata
import {  } from "takumi-js/response";

export function () {
  return new (<>Hello Takumi</>, {
    : [
      {
        : "Inter",
        : 700,
        : () => ("https://example.com/Inter.woff2").(() => .()),
      },
    ],
  });
}

generic assigns the font to a CSS generic family keyword. A stack that ends in monospace or serif then reaches the font without naming it. This is how Tailwind's font-mono finds it:

Assign a generic font family
import {  } from "takumi-js/response";

export function () {
  return new (< ="font-mono">let x = 1;</>, {
    : [
      {
        : "Geist Mono",
        : "monospace", 
        : () => ("https://example.com/GeistMono.woff2").(() => .()),
      },
    ],
  });
}

From Google Fonts

Load multilingual fonts
import {  } from "takumi-js";
import {  } from "takumi-js/helpers";

const  = await (< ={{ : "Inter" }}>Hello 你好 こんにちは</>, {
  : 1200,
  : 630,
  : (["Inter", "Noto Sans JP", "Noto Sans TC"]),
});

googleFonts makes one CSS request for all the families. Font files download only when the text needs them; see subsetting. Use the family name in fontFamily.

Pass an object instead of a name to set a family's weight, style, or variable axes:

Choose font weights and axes
import {  } from "takumi-js/helpers";

await ([
  {
    : "Inter",
    : [400, 700],
    : "italic",
  },
  {
    : "Noto Sans JP",
    : "600..700", // range loads the variable font
  },
  {
    : "Fraunces",
    : "100..900",
    : { : "9..144" }, // vary the optical-size axis too
  },
]);
FieldAcceptsEffect
namea family nameThe family to load. Known families autocomplete their weights and axes.
weighta weight, an array, or a range "100..900"Defaults to 400. A range loads the variable font, and font-weight drives it.
style"normal", "italic", or bothDefaults to "normal". Which styles to fetch.
axes{ tag: value | "min..max" }Variable axes to load, such as { opsz: "9..144" }. A range keeps them live.
generica CSS generic family keywordEvery loaded subset claims it, so font-mono can resolve to the family.

Pass an object instead of an array for request options:

Limit font requests
import {  } from "takumi-js/helpers";

const  = await ({
  : ["Inter"],
  : 3000, 
});
OptionAcceptsEffect
familiesthe list aboveThe families to load, in fallback priority order.
displaya font-display valueSent as display in the CSS request.
cachea MapReplaces the default cache of CSS responses.
baseUrla css2 endpointPoint at a mirror such as https://fonts.bunny.net/css2.

googleFonts also takes the fetch options: timeout, signal, maxBytes, allowUrl, and fetch. The timeout covers each request on its own: the CSS request, then each font file. It does not cover the whole render.

The default CSS cache is bounded and shares concurrent requests for the same URL. A request with signal or allowUrl skips the cache, so cancellation and URL checks stay with the caller. maxBytes applies to cache hits too.

Fonts in CI and offline renders

Bundle font files when a render must work without Google Fonts:

Fonts in CI and offline renders
import {  } from "node:fs/promises";
import {  } from "takumi-js";

const  = await (new ("./fonts/Inter.woff2", import..));

const  = await (< ={{ : "Inter" }}>Hello</>, {
  : [{ : "Inter", :  }],
});

Retries recover a brief outage. They do not help when the host is down or the CI network blocks it. Run unit tests against local files or an injected fetch, and test the live service separately.

An outer test or route timeout can end the render before the font timeout does. Set timeout below that outer limit, and leave time for layout and encoding.

CJK families can have many coverage subsets. Takumi downloads only the subsets your text uses. Matching subsets from several families or weights still download together, so request only the families and weights you use. A timeout during await googleFonts(...) comes from the CSS request, before any font file downloads.

Subsetting

render registers only the subsets the content draws. The same fonts list works for any text, and unused ranges never download. This runs on every render.

Font kindBehavior
Ranged subsets (from googleFonts)Kept only when the text uses their range.
Whole files (URL string, raw bytes)No unicode-range, so they always load.

To filter a list yourself, call subsetFonts({ fonts, source }) from takumi-js/helpers. source is the string, node, or node array you render.

Preloading with registerFont

To load fonts before the first request, register them on a renderer. Then pass that renderer to each render. registerFont takes the same entries as fonts and returns the families it added.

Preload a local font
import {  } from "node:fs/promises";
import {  } from "takumi-js/node";
import {  } from "takumi-js/response";

const  = await (new ("./fonts/Inter.woff2", import..));
const  = new ();

await .({ : "Inter", :  }); 

export function () {
  return new (< ={{ : "Inter" }}>Hello Takumi</>, {
    : 1200,
    : 630,
    , 
  });
}

Releasing fonts

A renderer keeps every font it loads. Release the ones you no longer need:

renderer.unregisterFont({ name: "Inter" }); // every face and subset of Inter
renderer.clearFonts(); // back to the built-in Geist

A later render that lists a released font loads it again.

To release fonts automatically, cap the decoded bytes that fonts lists may keep:

const renderer = new Renderer({ fontCacheMaxBytes: 64 * 1024 * 1024 });

Past the cap, the least recently used families go first. Fonts from registerFont always stay. The font loaded last also stays, even if it alone exceeds the cap. Set the cap above what one render needs.

Choosing a font

Fallback chain

fontFamilies is the ordered list of families to try when a glyph is missing. Set it to choose which family wins and what backs it up.

Choose font fallback order
import {  } from "takumi-js";
import {  } from "takumi-js/helpers";

const  = await (<>Hello こんにちは</>, {
  : (["Inter", "Noto Sans JP"]),
  : ["Inter", "Noto Sans JP"], 
});

Without fontFamilies, the chain is the render's fonts in list order. A render without fonts uses every registered family in registration order. Geist always comes last.

A missing glyph walks the chain until a family covers it. googleFonts registers every subset under its family name with its unicode-range. Each character then uses the subset that covers it, as in a browser.

Variable axes & OpenType features

Set font axes with font-variation-settings and OpenType features with font-feature-settings. In a variable font, font-weight drives the wght axis and font-stretch drives wdth. Kerning is on by default; font-kerning: none turns it off.

Set axes and OpenType features
<div
  style={{
    fontFamily: "Fraunces",
    fontVariationSettings: "'opsz' 72, 'wght' 700",
    fontFeatureSettings: "ss01",
  }}
>
  Variable Font Text
</div>

Register a variable font once, with no weight, to keep its axes live:

Register a variable font
// a `weight` here pins the file to that instance instead
await renderer.registerFont({ name: "Fraunces", data: variableWoff2 });

googleFonts does this for you: a weight range or an axes entry fetches the variable file and leaves its axes free.

Which axes a file exposes depends on the build. Google Fonts fixes the axes you did not request at their defaults, so Fraunces from there exposes only opsz and wght. Other builds keep SOFT and WONK live, and setting them changes the letterforms.

Synthetic bold & italic

When a family lacks the weight or style you asked for, Takumi draws a synthetic bold or oblique. font-synthesis controls this.

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

List weight, style, small-caps, or any mix of them to allow each one. none turns them all off, so a missing bold weight draws at regular weight.

Typography covers styling text and controlling how it flows.

Last updated on