Summit Themes
Blog

Defining typography on your Tailwind CSS project

Typography is the part of a design that visitors absorb before they consciously read a single word. Get it right and the page feels polished and trustworthy. Get it wrong — mismatched weights, sizes that don't scale, a font that never quite loads — and it quietly undermines everything else you built. If you are working with Tailwind CSS v4, the good news is that defining a complete, consistent type system is cleaner than it has ever been. The JavaScript config file is gone; everything lives in CSS, in one place, using variables Tailwind already understands.

This guide walks through the full setup: loading a font, registering it with @theme, customising the size scale, dialling in weights and spacing, and wiring it all into a coherent system you can maintain without hunting across files.

How Tailwind v4 handles configuration

Tailwind CSS v4 ships without a tailwind.config.js. Design tokens — fonts, colours, spacing, everything — are defined directly in CSS using the @theme directive. When Tailwind sees a variable inside @theme, it does two things: it generates a corresponding utility class, and it exposes the value as a standard CSS custom property on :root so you can reference it anywhere in your own CSS.

This distinction matters: @theme is not the same as :root. Writing --font-sans: ... inside @theme tells Tailwind to create the font-sans utility class. Writing the same variable inside :root sets a plain CSS variable, but Tailwind ignores it for class generation. Use @theme for anything that should produce a utility; use :root for everything else.

Step 1 — Load your font

Before you can use a font in @theme, the browser needs to know about it. You have two options.

Google Fonts (or any hosted import)

Put the @import at the very top of your CSS file, before the Tailwind import. Order matters here — browsers process @import statements before anything else, and placing them after other rules is invalid CSS.

@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
@import "tailwindcss";

@theme {
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
}

Self-hosted fonts

Self-hosted fonts give you full control over loading and avoid a third-party request. Use @font-face in your CSS, then register the family in @theme.

@import "tailwindcss";

@font-face {
  font-family: "Geist";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/fonts/Geist.woff2") format("woff2");
}

@font-face {
  font-family: "Geist Mono";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/fonts/GeistMono.woff2") format("woff2");
}

@theme {
  --font-sans: "Geist", ui-sans-serif, system-ui, sans-serif;
  --font-mono: "Geist Mono", ui-monospace, monospace;
}

The font-display: swap declaration tells the browser to show a system fallback immediately and swap in the custom font once it arrives, which prevents invisible text during load.

Step 2 — Register fonts with @theme

The --font-* namespace generates font-* utilities. Define as many families as you need.

@theme {
  --font-sans:    "Inter", ui-sans-serif, system-ui, sans-serif;
  --font-display: "Fraunces", Georgia, serif;
  --font-mono:    "Geist Mono", ui-monospace, monospace;
}

These map directly to font-sans, font-display, and font-mono in your HTML. Tailwind's built-in font-sans, font-serif, and font-mono are overridden when you redefine those same variables.

Setting OpenType features per font

Some fonts ship with OpenType features worth enabling — contextual alternates, stylistic sets, tabular numerals. You can attach them to a font variable using companion variables in the same namespace.

@theme {
  --font-display: "Fraunces", Georgia, serif;
  --font-display--font-feature-settings: "cv02", "cv11";
  --font-display--font-variation-settings: "opsz" 32;
}

Whenever you apply font-display, Tailwind will include these feature settings automatically. You do not have to remember to add them at each usage site.

Setting the default font globally

To apply a font across the whole page without adding a class to <body>, override --default-font-family inside @theme.

@theme {
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
  --default-font-family: var(--font-sans);
}

This sets font-family on the html element in Tailwind's base layer, so every element inherits it by default.

Step 3 — Customise the size scale

Tailwind ships a sensible default scale from text-xs to text-9xl. You can extend it with additional steps, override existing ones, or do both.

@theme {
  /* Add a step smaller than xs */
  --text-2xs: 0.625rem;

  /* Override the base size */
  --text-base: 1.0625rem;   /* 17px — often a better reading size */

  /* Override an existing step with a custom line-height */
  --text-sm: 0.875rem;
  --text-sm--line-height: 1.5rem;
}

The double-dash modifier syntax (--text-{name}--line-height, --text-{name}--letter-spacing, --text-{name}--font-weight) lets you bundle a complete text style into a single named step. When you write text-2xs, Tailwind sets the font size, line height, letter spacing, and font weight all at once — you do not have to stack four classes.

@theme {
  --text-2xs: 0.625rem;
  --text-2xs--line-height: 1rem;
  --text-2xs--letter-spacing: 0.04em;
  --text-2xs--font-weight: 500;
}

Step 4 — Weights, leading, and tracking

The same pattern — @theme variable generates a utility class — applies to every other typographic property.

@theme {
  /* Font weights — generates font-{name} utilities */
  --font-weight-normal:    400;
  --font-weight-medium:    500;
  --font-weight-semibold:  600;
  --font-weight-bold:      700;

  /* Line heights — generates leading-{name} utilities */
  --leading-tight:    1.2;
  --leading-snug:     1.35;
  --leading-normal:   1.5;
  --leading-relaxed:  1.65;

  /* Letter spacing — generates tracking-{name} utilities */
  --tracking-tight:   -0.02em;
  --tracking-normal:   0em;
  --tracking-wide:     0.04em;
  --tracking-widest:   0.12em;
}

Use only the steps you actually need. A smaller vocabulary of weights and spacings — say four or five options each — is much easier to apply consistently than a thirty-stop scale where everything is almost but not quite the right choice.

Step 5 — Putting it together as a type system

With the tokens defined, you can build named text styles using @layer components and reference those variables directly. This is especially useful for prose content — blog posts, documentation, any HTML you do not control at the element level.

@layer components {
  .prose-body {
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: var(--leading-relaxed);
    color: inherit;
  }

  .prose-body h2 {
    font-family: var(--font-display);
    font-size: var(--text-2xl);
    font-weight: var(--font-weight-semibold);
    line-height: var(--leading-tight);
    letter-spacing: var(--tracking-tight);
    margin-top: 2em;
    margin-bottom: 0.5em;
  }

  .prose-body h3 {
    font-size: var(--text-xl);
    font-weight: var(--font-weight-semibold);
    line-height: var(--leading-snug);
    margin-top: 1.5em;
    margin-bottom: 0.4em;
  }

  .prose-body p {
    margin-bottom: 1.25em;
  }
}

For utility-heavy pages like landing pages and marketing sections, you will mostly use the generated classes directly in your HTML: text-2xl font-display font-semibold tracking-tight leading-tight. The advantage of defining the tokens in @theme first is that "tracking-tight" everywhere means the same value, and changing it is a one-line edit.

A note on the inline modifier

If you need a theme variable to reference another theme variable — for example, aliasing --font-sans to resolve to whatever --font-inter holds — use @theme inline.

@theme {
  --font-inter: "Inter", ui-sans-serif, system-ui, sans-serif;
}

@theme inline {
  --font-sans: var(--font-inter);
}

Without inline, Tailwind stores the literal string var(--font-inter) as the utility value. With inline, it resolves the reference so the utility uses the underlying value at build time. Use inline any time you are aliasing one token to another.

Putting it all in one file

A complete typography setup for a typical project looks roughly like this — everything in your main CSS entry point, nothing scattered across config files.

@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Fraunces:opsz,[email protected],300;9..144,400;9..144,600&display=swap");
@import "tailwindcss";

@theme {
  /* Families */
  --font-sans:    "Inter", ui-sans-serif, system-ui, sans-serif;
  --font-display: "Fraunces", Georgia, serif;
  --default-font-family: var(--font-sans);

  /* Size scale additions */
  --text-2xs: 0.625rem;
  --text-2xs--line-height: 1rem;

  /* Weights */
  --font-weight-normal:   400;
  --font-weight-medium:   500;
  --font-weight-semibold: 600;
  --font-weight-bold:     700;

  /* Leading */
  --leading-tight:   1.2;
  --leading-snug:    1.35;
  --leading-normal:  1.5;
  --leading-relaxed: 1.65;

  /* Tracking */
  --tracking-tight:   -0.02em;
  --tracking-normal:   0em;
  --tracking-wide:     0.04em;
}

This gives you a complete, predictable type system: two font families, every weight and spacing step named, and a scale that extends Tailwind's defaults without replacing anything you need. Swap a font name in one place and every font-display across the project updates.

A few things worth remembering

  • Keep @import statements first — before @import "tailwindcss". CSS ignores imports that appear after non-import rules.
  • Use font-display: swap in every @font-face block. It prevents invisible text while the font loads.
  • Limit your scale — five or six size steps, three or four weights, two or three leading values. More than that and you spend time choosing instead of building.
  • Prefer em for letter-spacing — it scales with font size automatically. rem or px will be too tight at large sizes and too loose at small ones.
  • @theme variables become CSS custom properties on :root — so you can use var(--font-sans) in any custom CSS you write, including animations, pseudo-elements, or component layers.

The biggest shift in Tailwind v4 typography is that the design tokens and the utility classes are the same thing, defined once, in CSS. There is no JSON to translate or config file to sync. You write the token, you get the class, and the value is available everywhere. That is a good foundation for a type system that stays consistent as a project grows.