
How to Configure Typography in Tailwind CSS v4?
- Sajjad
- Typography
- 01 Oct, 2026
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:
| Namespace | Generates | Example variable | Example class |
|---|---|---|---|
--font-* | font-family utilities | --font-serif | font-serif |
--text-* | font-size (+ optional extras) | --text-xl | text-xl |
--font-weight-* | font-weight utilities | --font-weight-semibold | font-semibold |
--leading-* | line-height utilities | --leading-tight | leading-tight |
--tracking-* | letter-spacing utilities | --tracking-wide | tracking-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:
- Use
rem, notpx. Tailwind's defaults are already inrem, which respects the user's browser font-size setting. - Use unitless line heights. A unitless
1.5scales with the element's font size; a fixed24pxdoes not. - 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.
- 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:
- Tailwind CSS Docs: Theme variables — the full list of namespaces,
@theme inline, and resetting defaults. - Tailwind CSS Docs: font-size — size utilities, line-height modifiers, and custom size sub-properties.
- Tailwind CSS Docs: Upgrade guide — how v3 config maps to v4 and what the upgrade tool changes.
- Next.js Docs: Font optimization — using
next/fontwith CSS variables. - W3C: Understanding WCAG 1.4.12 Text Spacing — the spacing overrides your type scale must tolerate.


