agentsclimarketplace

Fonts

Skill teamniteo/hakuto/skills/fonts

Configure web fonts via Astro's Fonts API (top-level `fonts` in Astro 6+, `experimental.fonts` in 5.x). Use whenever custom fonts come up — user mentions Google Fonts, Fontsource, local fonts, typography changes, font loading or performance, or asks to "add fonts", "change typography", "use custom fonts", "improve font loading", "optimize fonts". The Fonts API replaces `@import` and `@font-face` in CSS. Not for changing font size, weight or tracking on an already-configured family — those are Tailwind utilities in `src/index.css`, and loading this skill for them wastes a round-trip.From its SKILL.md

Install
npx -y skills add teamniteo/hakuto --skill fonts

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

10.2 KB, as published. Nobody here has run it

Fonts (Astro Fonts API)

Configure performant web fonts with automatic optimization, preloading, and privacy-focused delivery from your own site.

Critical Workflow Order

Font setup MUST follow this exact sequence. The <Font /> component and CSS variables are only available after the dev server processes the config.

Step 1: Configure astro.config.mjs (FIRST)

Add fonts to the top-level fonts array (Astro 6+ promoted from experimental.fonts):

import { defineConfig, fontProviders } from "astro/config";

export default defineConfig({
  fonts: [{
    provider: fontProviders.google(),
    name: "Crimson Pro",
    cssVariable: "--font-display",
    weights: [400, 600, 700],
    styles: ["normal"]
  }]
});

Note: Astro ≤ 5.x required experimental: { fonts: [...] }. Astro 6.x graduated the API; wrap in experimental only if you're on an older Astro.

Step 2: Restart the dev/preview server

The dev server reads astro.config.mjs once at startup. Without a restart, import { Font } from 'astro:assets' will fail and the CSS variables won't exist yet.

How to restart depends on how the server was started — pick the one that matches your setup:

  • Hakuto preview hook (this repo): the preview server is managed externally; ask the user to restart it, or wait for the next file-save hook trigger.
  • Manual bun run dev / bun run preview: stop the process (Ctrl-C) and re-run the same command.
  • PM2-managed (pm2 list shows it): pm2 restart <process-name> — typical names are preview or the package name.

Don't proceed to Step 3 until the server has restarted — the next step depends on the new config being live.

When invoked from website-builder's style-preview loop, always end by telling the user to restart their dev server before reviewing the preview — and encourage locking fonts early in the iteration, since every font swap repeats this restart cycle while palette/layout tweaks don't.

Step 3: Add Font declarations to .astro files (ONLY AFTER RESTART)

Now you can use the <Font /> component and CSS variables:

Layout.astro - Add <Font /> to head:

---
import { Font } from 'astro:assets';
---

<head>
  <Font cssVariable="--font-display" preload />
  <!-- other head elements -->
</head>

index.css - Wire into Tailwind:

@import 'tailwindcss';

@theme {
  --font-sans: var(--font-display), ui-sans-serif, system-ui, sans-serif;
}

Provider Selection

ProviderUse WhenImport
fontProviders.google()Quick setup, vast selectionBuilt-in
fontProviders.fontsource()Open-source, granular controlBuilt-in
fontProviders.bunny()Privacy-focused, GDPR compliantBuilt-in
fontProviders.fontshare()Free distinctive fontsBuilt-in
fontProviders.local()Custom brand fonts, offline supportBuilt-in

Configuration Examples

Google Fonts

import { defineConfig, fontProviders } from "astro/config";

export default defineConfig({
  fonts: [
    {
      provider: fontProviders.google(),
      name: "Crimson Pro",
      cssVariable: "--font-display",
      weights: [400, 600, 700],
      styles: ["normal", "italic"],
      subsets: ["latin"]
    },
    {
      provider: fontProviders.google(),
      name: "DM Sans",
      cssVariable: "--font-body",
      weights: [400, 500, 600],
      styles: ["normal"]
    }
  ]
});

Fontsource (Open-Source)

import { defineConfig, fontProviders } from "astro/config";

export default defineConfig({
  fonts: [
    {
      provider: fontProviders.fontsource(),
      name: "JetBrains Mono",
      cssVariable: "--font-mono",
      weights: [400, 700],
      subsets: ["latin"],
      fallbacks: ["monospace"]
    }
  ]
});

Local Fonts

import { defineConfig, fontProviders } from "astro/config";

export default defineConfig({
  fonts: [{
    provider: fontProviders.local(),
    name: "Brand Font",
    cssVariable: "--font-brand",
    options: {
      variants: [
        {
          weight: 400,
          style: "normal",
          src: ["./src/assets/fonts/BrandFont-Regular.woff2"]
        },
        {
          weight: 700,
          style: "normal",
          src: ["./src/assets/fonts/BrandFont-Bold.woff2"]
        }
      ]
    }
  }]
});

Important: Store local fonts in src/assets/fonts/, NOT in public/ (avoids duplicate files in build).

Variable Fonts

{
  provider: fontProviders.google(),
  name: "Recursive",
  cssVariable: "--font-recursive",
  weights: ["300 1000"],  // Variable weight range
  styles: ["normal"]
}

Recursive (and Sora, Inter Tight, Crimson Pro, Spectral) ships as a single variable font file covering the full weight range — one network request instead of 5–9 separate weight files. Pick a distinctive variable font; avoid Inter and Roboto defaults.

Integration with Tailwind v4

Define font families in src/index.css:

@import 'tailwindcss';

@theme {
  /* Override default font families */
  --font-sans: var(--font-body), ui-sans-serif, system-ui, sans-serif;
  --font-serif: var(--font-display), ui-serif, Georgia, serif;
  --font-mono: var(--font-mono), ui-monospace, monospace;

  /* Or create custom font utilities */
  --font-display: var(--font-crimson);
  --font-heading: var(--font-sora);
}

Usage in components:

<h1 class="font-display text-4xl">Beautiful Heading</h1>
<p class="font-sans text-base">Body text content</p>
<code class="font-mono">Code snippet</code>

Layout Integration

Basic Setup (Layout.astro)

Import CSS via the frontmatter — Astro processes the import and emits a hashed stylesheet link automatically. Don't use <link rel="stylesheet" href="/src/index.css" />; /src/ paths don't resolve in production builds.

---
import { Font } from 'astro:assets';
import '../index.css';
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />

    <!-- Font declarations and preloads -->
    <Font cssVariable="--font-display" preload />
    <Font cssVariable="--font-body" preload />
  </head>
  <body class="font-sans">
    <slot />
  </body>
</html>

Granular Preloading (Performance)

Only preload fonts actually used above-the-fold:

<Font
  cssVariable="--font-display"
  preload={[
    { weight: '700', style: 'normal', subset: 'latin' }
  ]}
/>

<Font
  cssVariable="--font-body"
  preload={[
    { weight: '400', style: 'normal', subset: 'latin' },
    { weight: '600', style: 'normal', subset: 'latin' }
  ]}
/>

Performance Tips

Limit Weights and Styles

// Bad - downloads all weights
weights: [100, 200, 300, 400, 500, 600, 700, 800, 900]

// Good - only what you use
weights: [400, 600, 700]

Subset Optimization

// Only Latin characters (most Western sites)
subsets: ["latin"]

// Add if needed
subsets: ["latin", "latin-ext", "cyrillic"]

Use Fallbacks

{
  name: "Crimson Pro",
  cssVariable: "--font-display",
  fallbacks: ["Georgia", "serif"]  // Shown while loading
}

Type Contrast (Read This Before Pairing)

Pairings work because of contrast, not similarity. The two cardinal rules:

  1. Don't pair two fonts from the same family of sans-serifs. Sora + Inter, Archivo + Nunito, DM Sans + Work Sans — all flat. The reader can't tell where the heading ends and the body begins. Pair a serif with a sans, a display with a monospace, or a humanist sans with a geometric sans.
  2. Push weight contrast hard — 300 body vs 800–900 display. Timid scales (400 body / 600 heading) read as generic. The same goes for size: aim for ≥3× jumps between H1 and body.

A single distinctive font used decisively across the whole site often beats a weak pair.

Recommended Font Pairings

Avoid "AI slop" aesthetics with distinctive combinations. Each row pairs across-family deliberately:

DisplayBodyWhy this works
Crimson Pro (serif)DM Sans (geometric sans)Editorial weight against modern minimal — clear hierarchy
Spectral (serif)Source Sans Pro (humanist sans)Refined editorial paired with workhorse legibility
Instrument SerifInstrument SansDesigned-as-a-pair; complementary contrast built-in
Bitter (slab serif)Open Sans (humanist sans)Friendly slab against neutral body — warm but readable
Fraunces (display serif)Inter Tight (geometric sans)Expressive display paired with tight, distraction-free body
Sora (geometric sans, 800)Crimson Pro (serif, 400)Single-distinctive-font feel by inverting the usual sans-display / serif-body convention

Troubleshooting

IssueSolution
Font import fails or CSS variable undefinedServer hasn't picked up the new config — see Step 2. Also verify cssVariable name matches between astro.config.mjs and your CSS/component usage.
Fonts not loadingCheck <Font /> is in Layout head
Build failsEnsure local font paths are correct
FOUT (flash of unstyled text)Add preload to critical fonts
Large bundleReduce weights/subsets, prefer a variable font

Advanced: Programmatic Access

Get font data for OpenGraph images or other uses:

import { fontData } from "astro:assets";

const data = fontData["--font-display"];
// Returns array of font face data with src, weight, style

See Also

  • references/font-providers.md - Detailed provider configurations
  • website-builder/SKILL.md - Overall site workflow
  • brand-designer/SKILL.md - Color and typography decisions

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.