Type something to search...
How to Configure Typography in Tailwind CSS v4?

How to Configure Typography in Tailwind CSS v4?

You upgrade a project to Tailwind CSS v4, open the repo, and the tailwind.config.js file where your fontFamily and fontSize extensions used to live is gone. The build still runs, but your headings fell back to the default sans stack, text-display no longer exists, and a teammate is asking where the type scale is supposed to go now. The answer is that it moved into your CSS. Tailwind v4 is configured with CSS custom properties inside an @theme block, and once you understand the naming conventions, typography configuration is shorter and more predictable than it ever was in JavaScript.

This article walks through how Tailwind v4 turns theme variables into typography utilities, how to add fonts (including next/font and self-hosted files), how to build a type scale with paired line heights, how to reset or extend the defaults, and the mistakes that trip people up during migration.

How Tailwind v4 Handles Typography Configuration

In Tailwind v4, the theme is a set of CSS variables declared inside @theme. Each variable belongs to a namespace, and the namespace decides which utility classes get generated. Declare --font-display and you get a font-display class. Declare --text-hero and you get text-hero. There is no separate config object to keep in sync, and every value is also available at runtime as a normal CSS variable on :root.

The typography-related namespaces are:

NamespaceGeneratesExample variableExample class
--font-*font-family utilities--font-seriffont-serif
--text-*font-size (+ optional extras)--text-xltext-xl
--font-weight-*font-weight utilities--font-weight-semiboldfont-semibold
--leading-*line-height utilities--leading-tightleading-tight
--tracking-*letter-spacing utilities--tracking-widetracking-wide

Your main stylesheet is the entry point. A minimal v4 setup looks like this:

/* src/styles/main.css */
@import "tailwindcss";

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

That is the whole configuration. font-sans is now Inter, and a new font-display utility exists. Because Tailwind's preflight sets the html font family from --font-sans (through --default-font-family), overriding --font-sans changes the default typeface for the whole page.

Adding Font Families

Self-Hosted Fonts with @font-face

If you serve your own WOFF2 files, declare the faces with normal @font-face rules outside the @theme block, then reference the family name in a theme variable:

@import "tailwindcss";

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

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

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

The font-weight: 100 900 range tells the browser this single file is a variable font covering every weight, so font-light through font-black all map to one download.

Fonts from next/font

With Next.js, next/font downloads and self-hosts the font at build time and exposes it as a CSS variable. You then wire that variable into Tailwind using @theme inline:

// src/app/layout.tsx
import { Inter, Fraunces } from "next/font/google";
import "@/styles/main.css";

const inter = Inter({
  subsets: ["latin"],
  variable: "--font-inter",
  display: "swap",
});

const fraunces = Fraunces({
  subsets: ["latin"],
  variable: "--font-fraunces",
  display: "swap",
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={`${inter.variable} ${fraunces.variable}`}>
      <body>{children}</body>
    </html>
  );
}
@import "tailwindcss";

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

The inline keyword matters. Without it, Tailwind emits font-family: var(--font-sans), and --font-sans is defined on :root as var(--font-inter). But next/font sets --font-inter on the html element through a class, and custom properties referencing other custom properties are resolved where they are declared. @theme inline makes the utility use the value directly (font-family: var(--font-inter), ...), so it resolves correctly wherever it is applied. For the full next/font workflow, see how to use next/font in Next.js.

Font Features and Variation Settings

Tailwind v4 lets a font variable carry default OpenType features or variable axes through sub-properties:

@theme {
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
  --font-sans--font-feature-settings: "cv11", "ss01";
  --font-sans--font-variation-settings: "opsz" 32;
}

Every element using font-sans now gets those features. Use this for stylistic sets that belong to the brand, such as Inter's single-storey "a", rather than sprinkling font-feature-settings around component CSS.

Building a Type Scale with --text-*

Font sizes are where v4 is most different from v3. A --text-* variable sets the size, and optional double-dash sub-properties attach a line height, letter spacing, and weight to the same utility:

@theme {
  --text-sm: 0.875rem;
  --text-sm--line-height: 1.5;

  --text-base: 1rem;
  --text-base--line-height: 1.625;

  --text-lg: 1.125rem;
  --text-lg--line-height: 1.6;

  --text-xl: 1.25rem;
  --text-xl--line-height: 1.5;

  --text-2xl: 1.5rem;
  --text-2xl--line-height: 1.35;

  --text-3xl: 1.875rem;
  --text-3xl--line-height: 1.25;

  --text-4xl: 2.25rem;
  --text-4xl--line-height: 1.15;
  --text-4xl--letter-spacing: -0.01em;

  --text-hero: 3.5rem;
  --text-hero--line-height: 1.05;
  --text-hero--letter-spacing: -0.02em;
  --text-hero--font-weight: 700;
}

Now text-hero sets four properties at once. That pairing is the main reason to configure sizes this way: large type needs tighter leading and slightly negative tracking, while body text needs looser leading, and encoding the pair in the token stops developers from forgetting one half.

A few rules for picking values:

  1. Use rem, not px. Tailwind's defaults are already in rem, which respects the user's browser font-size setting.
  2. Use unitless line heights. A unitless 1.5 scales with the element's font size; a fixed 24px does not.
  3. Keep body text at least 1rem (16px) with a line height of 1.5 or more. WCAG 1.4.12 requires that content survives users forcing line height to 1.5 and paragraph spacing to 2× the font size, so design for that range from the start.
  4. Derive sizes from a ratio. A 1.2 or 1.25 ratio gives a coherent scale. The method is explained in how to build a modular type scale.

Fluid Sizes with clamp()

Theme values are plain CSS, so clamp() works directly:

@theme {
  --text-hero: clamp(2.25rem, 1.5rem + 3vw, 4rem);
  --text-hero--line-height: 1.05;
}

Always include a rem component in the preferred value (1.5rem + 3vw, not 5vw alone). A pure viewport unit ignores browser zoom and text-size settings, which can fail WCAG 1.4.4 (resize text to 200%).

Line-Height Modifiers in Markup

Tailwind v4 also supports a slash modifier for one-off pairings: text-lg/7 sets the text-lg size with a line height of calc(var(--spacing) * 7), and text-lg/snug uses the --leading-snug value. That is handy in components, but keep the default pairing in the theme so most markup only needs text-lg.

Weights, Leading, and Tracking

The remaining namespaces are straightforward. Override or add only what you need:

@theme {
  --font-weight-book: 450;
  --font-weight-semibold: 600;

  --leading-tight: 1.2;
  --leading-body: 1.65;

  --tracking-tight: -0.015em;
  --tracking-caps: 0.08em;
}

This generates font-book, leading-body, and tracking-caps alongside the defaults. A custom weight like 450 only makes sense with a variable font; with static files, the browser rounds to the nearest weight you actually loaded.

Use em for tracking. Letter spacing should scale with the text it is applied to, and an em value does that automatically. A dedicated tracking-caps token is worth having because all-caps labels almost always need extra spacing.

Replacing the Default Scale Entirely

By default, @theme extends Tailwind's built-in values. If your design system has its own named sizes and you do not want text-7xl or font-mono to exist, reset the namespace first with initial:

@theme {
  --text-*: initial;

  --text-caption: 0.8125rem;
  --text-caption--line-height: 1.4;
  --text-body: 1rem;
  --text-body--line-height: 1.6;
  --text-title: 1.5rem;
  --text-title--line-height: 1.3;
  --text-display: 2.5rem;
  --text-display--line-height: 1.1;
}

Now only text-caption, text-body, text-title, and text-display exist. This is a good fit for teams that want designers and developers to share one vocabulary, but it is disruptive mid-project because any existing text-sm class silently stops working. You can also reset everything with --*: initial;, though that removes colors and spacing too.

Base Styles and Custom Typography Utilities

Theme variables give you utilities, but you still want sensible defaults on raw elements, especially for content rendered from Markdown. Use @layer base and reference the theme variables:

@layer base {
  body {
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: var(--text-base--line-height);
    text-rendering: optimizeLegibility;
  }

  h1,
  h2,
  h3 {
    font-family: var(--font-display);
    text-wrap: balance;
  }

  p {
    text-wrap: pretty;
  }
}

For repeated combinations, define a custom utility with @utility. Unlike a plain class, it works with variants like md: and hover::

@utility eyebrow {
  font-size: var(--text-sm);
  font-weight: var(--font-weight-semibold);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
}
<p class="eyebrow text-slate-600 md:text-base">Case study</p>

Avoid @apply chains that rebuild the whole design system in CSS. Utilities in markup plus a small base layer is easier to maintain. If you need prose styling for long-form Markdown, the dedicated Tailwind Typography plugin guide covers that separately.

Migrating Typography from tailwind.config.js

Here is how v3 settings map to v4:

v3 (tailwind.config.js)v4 (@theme)
fontFamily.display: ["Fraunces", "serif"]--font-display: "Fraunces", serif;
fontSize.hero: ["3.5rem", "1.05"]--text-hero: 3.5rem; + --text-hero--line-height
fontSize.hero: ["3.5rem", { letterSpacing }]--text-hero--letter-spacing: -0.02em;
fontWeight.book: "450"--font-weight-book: 450;
lineHeight.body: "1.65"--leading-body: 1.65;
letterSpacing.caps: "0.08em"--tracking-caps: 0.08em;

The official upgrade tool handles most of this automatically:

npx @tailwindcss/upgrade

If you cannot migrate the config yet, v4 can still load it with @config "../../tailwind.config.js"; in your CSS. Treat that as a bridge, not a destination; new features like theme sub-properties are CSS-only.

Some projects, including this site, generate the @theme block from a design-token file. A script reads a JSON file with font families and a base size and scale, then writes a generated CSS file that the main stylesheet imports. That is a good pattern when the same tokens feed other platforms, and it is the approach described in setting up typography tokens in a design system. Just never hand-edit the generated file.

Common Mistakes

Putting @font-face Inside @theme

@theme only accepts custom properties (and @keyframes). Put @font-face rules at the top level of the stylesheet.

Forgetting the Fallback Stack

--font-sans: "Inter"; with no fallbacks means a serif default shows during loading and on any failure. Always end with a generic family, and consider metric-adjusted fallbacks to reduce layout shift.

Using theme() Instead of var()

v3 code often used theme('fontSize.lg') in CSS. In v4 you reference the variable directly: var(--text-lg). The theme() function still exists for compatibility, but variables are the native approach and work at runtime in inline styles and JavaScript too.

Defining Variables in :root Instead of @theme

A variable in :root is just a variable. Only variables declared inside @theme generate utilities. If text-hero is not appearing, check where you defined --text-hero.

Skipping inline with Variable References

If a font utility resolves to the fallback even though next/font loaded, you almost certainly need @theme inline.


Tailwind CSS v4 Typography FAQ

No. Fonts, sizes, weights, line heights, and letter spacing are all configured with CSS variables inside an @theme block in your main stylesheet. A JavaScript config can still be loaded with the @config directive for legacy projects, but it is optional.

Add a sub-property with a double dash after the size variable, such as text-xl followed by double-dash line-height. Tailwind applies that line height whenever the matching text utility is used, and you can also attach letter spacing and font weight the same way.

Usually because the theme variable references the next/font variable without the inline keyword. Use @theme inline so the generated utility contains the next/font variable directly, and make sure the font's variable class is applied to the html or body element.

Set the namespace to initial at the top of your @theme block, for example by setting the text namespace wildcard to initial, and then declare your own sizes. Only the sizes you define afterward will generate utilities.

Yes. Theme values are ordinary CSS, so a clamp expression works as a font size token. Include a rem term in the preferred value so the text still responds to browser zoom and user font settings.

Yes. Tailwind v4 emits every theme variable as a CSS custom property on the root element, so you can read them in plain CSS, inline styles, or JavaScript with getComputedStyle.

Conclusion

Typography configuration in Tailwind CSS v4 comes down to a handful of namespaces inside @theme: --font-* for families, --text-* for sizes with optional line height, tracking, and weight attached, and --font-weight-*, --leading-*, and --tracking-* for everything else. Because these are real CSS variables, the same values drive your utilities, your base styles, and any runtime code, and there is no second source of truth to drift.

Start by setting --font-sans and a display family, define a rem-based scale with paired line heights, and decide early whether you are extending the defaults or replacing them with initial. Use @theme inline when wiring in next/font, keep @font-face outside the theme block, and add a small base layer for raw elements. That setup covers nearly every project and is easy for the next developer to read.

Here are some useful references for going deeper on Tailwind CSS v4 typography:

  1. Tailwind CSS Docs: Theme variables — the full list of namespaces, @theme inline, and resetting defaults.
  2. Tailwind CSS Docs: font-size — size utilities, line-height modifiers, and custom size sub-properties.
  3. Tailwind CSS Docs: Upgrade guide — how v3 config maps to v4 and what the upgrade tool changes.
  4. Next.js Docs: Font optimization — using next/font with CSS variables.
  5. W3C: Understanding WCAG 1.4.12 Text Spacing — the spacing overrides your type scale must tolerate.
Share :

Related Posts

Ascenders, Descenders, and Baselines: The Anatomy of a Letterform

Ascenders, Descenders, and Baselines: The Anatomy of a Letterform

You align an icon next to a button label and it looks a pixel or two too high, no matter how you adjust vertical-align. You set overflow: hidden

Continue Reading
Are Google Fonts GDPR-Compliant?

Are Google Fonts GDPR-Compliant?

In late 2022, thousands of small business owners in Germany and Austria opened letters demanding a few hundred euros in "damages" because their websi

Continue Reading
How to Audit Web Font Performance with Lighthouse?

How to Audit Web Font Performance with Lighthouse?

A client sends you a screenshot of their PageSpeed Insights report: performance score 61, LCP 3.9 seconds, and a vague list of warnings. They want to

Continue Reading