# Nextjs Font CSS Variable Collision

> Fix CSS custom property name collision between next/font and design system variables in globals.css. Use when: (1) fonts look wrong despite correct next/font configuration — falling back to Georgia, system-ui, or other fallback fonts, (2) DevTools shows --font-display or --font-body resolving to literal font names like 'Instrument Serif' instead of hashed names like '__Instrument_Serif_315a98', (3) fonts break after adding/removing next/font imports (bundle reordering), (4) @font-face uses hashed names but CSS variables hold unhashed literal names. Covers next/font variable naming, CSS cascade specificity with :root, and webpack CSS chunk ordering.

- Skill: `hubeiqiao/nextjs-font-css-variable-collision` (Agent Skill)
- Install (CLI): `npx skillmds@latest add hubeiqiao/nextjs-font-css-variable-collision`
- Raw SKILL.md: https://api.skillmd.com/api/skills/hubeiqiao/nextjs-font-css-variable-collision/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Design & Media
- Author: hubeiqiao (https://skillmd.com/u/hubeiqiao)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/hubeiqiao/nextjs-font-css-variable-collision

---


# next/font CSS Variable Name Collision

## Problem

When `next/font` configuration uses the same CSS custom property names as your
design system (e.g., both define `--font-display`), the CSS cascade determines
which value wins based on source order in the compiled CSS bundle. This creates
a **latent bug** that can appear or disappear when unrelated changes (like adding
new font imports) cause webpack to reorder CSS chunks.

The result: `@font-face` registers fonts under hashed internal names (e.g.,
`'__Instrument_Serif_315a98'`), but the CSS variable resolves to the literal
name (`'Instrument Serif'`) which has no matching `@font-face` rule. The browser
falls back to the next font in the stack (e.g., Georgia).

## Context / Trigger Conditions

- Next.js app using `next/font/google` or `next/font/local`
- Design system in `globals.css` or similar file defines CSS custom properties
  with the same names as `next/font`'s `variable` option (e.g., `--font-display`,
  `--font-body`, `--font-heading`)
- Fonts suddenly look wrong after adding or removing font imports
- DevTools Computed tab shows fallback fonts (Georgia, system-ui) instead of
  the loaded web font
- Inspecting the `<html>` element shows CSS variables holding literal font names
  instead of hashed `__FontName_hash` values

## Root Cause

Both systems try to set the same CSS custom property:

```css
/* next/font generates a class (specificity 0,1,0): */
.__variable_315a98 {
  --font-display: '__Instrument_Serif_315a98', '__Instrument_Serif_Fallback_315a98';
}

/* globals.css :root block (specificity 0,1,0): */
:root {
  --font-display: 'Instrument Serif', Georgia, serif;
}
```

Both have specificity `(0,1,0)`. The **later rule in the CSS bundle wins**.

- If `next/font`'s class appears after `:root` → hashed name wins (fonts work)
- If `:root` appears after `next/font`'s class → literal name wins (fonts break)

**Why this is latent**: CSS chunk ordering in webpack/turbopack is non-deterministic
and changes when you add/remove imports, modify `next.config.js`, or update
dependencies. The collision exists from day one but only manifests when ordering
flips.

## Solution

### Step 1: Give next/font unique variable names

Rename the `variable` option in each font declaration to a name that won't
collide with your design system:

```tsx
// BEFORE (collision-prone):
const instrumentSerif = Instrument_Serif({
  variable: "--font-display",        // collides with globals.css :root
  ...
});

// AFTER (collision-free):
const instrumentSerif = Instrument_Serif({
  variable: "--font-instrument-serif",  // unique source variable
  ...
});
```

**Naming convention**: Use `--font-{font-family-name}` for the source variable.
This describes which font, not what role it plays.

### Step 2: Reference source variables in the design system

Update your `:root` design tokens to consume the source variables via `var()`:

```css
/* BEFORE: */
:root {
  --font-display: 'Instrument Serif', Georgia, serif;
  --font-body: 'Satoshi', 'SF Pro Display', system-ui, sans-serif;
  --font-handwriting: 'Caveat', cursive;
}

/* AFTER: */
:root {
  --font-display: var(--font-instrument-serif), Georgia, serif;
  --font-body: var(--font-plus-jakarta), 'SF Pro Display', system-ui, sans-serif;
  --font-handwriting: var(--font-caveat), cursive;
}
```

### Step 3: Leave all component code unchanged

Components continue using semantic tokens like `var(--font-display)`. The
indirection is invisible to consumers. CJK `:lang()` overrides that set
`--font-display` directly still work because they override the semantic
variable, not the source variable.

## Architecture: Two-Layer Variable Pattern

```
Layer 1 (Source):     --font-instrument-serif  ← set by next/font class
                           ↓ var()
Layer 2 (Semantic):   --font-display           ← set by :root, consumed by components
                           ↓ var()
Components:           font-family: var(--font-display)
```

- **Source variables** (`--font-instrument-serif`) describe WHICH font
- **Semantic variables** (`--font-display`) describe WHAT ROLE the font plays
- Components only reference semantic variables
- CJK locale overrides set semantic variables directly

## Verification

1. **Build**: `npm run build` — zero errors
2. **DevTools**: Inspect `<html>` element:
   - `--font-instrument-serif` → hashed name like `'__Instrument_Serif_315a98'`
   - `--font-display` → resolves to the hashed name via `var()`
3. **Computed font**: Select a heading → Computed tab → `font-family` shows
   the loaded web font, not the fallback
4. **All locales**: Verify English, CJK, and other locale pages render correctly

## Example

Complete layout.tsx font configuration:

```tsx
import { Instrument_Serif, Plus_Jakarta_Sans, Caveat } from "next/font/google";

const instrumentSerif = Instrument_Serif({
  subsets: ["latin"],
  weight: ["400"],
  variable: "--font-instrument-serif",  // unique, no collision
  display: "swap",
});

const plusJakarta = Plus_Jakarta_Sans({
  subsets: ["latin"],
  weight: ["400", "500", "600", "700"],
  variable: "--font-plus-jakarta",      // unique, no collision
  display: "swap",
});

const caveat = Caveat({
  subsets: ["latin"],
  weight: ["400", "500", "600", "700"],
  variable: "--font-caveat",            // unique, no collision
  display: "swap",
});

// In JSX — className applies the generated CSS classes:
<html className={`${instrumentSerif.variable} ${plusJakarta.variable} ${caveat.variable}`}>
```

## Notes

- This pattern also works with `next/font/local` — same `variable` option
- The `.style.fontFamily` property on font objects is unaffected by the variable
  rename — it always returns the hashed name and can be used for inline styles
- If you use Tailwind's `fontFamily` config to reference CSS variables, ensure
  it references the semantic variables (e.g., `--font-display`), not the source
  variables
- This is not specific to any font — it can happen with ANY `next/font` where
  the `variable` name matches a CSS custom property in your stylesheets

## References

- [Next.js: Font Optimization](https://nextjs.org/docs/app/building-your-application/optimizing/fonts)
- [MDN: CSS Custom Properties (--*)](https://developer.mozilla.org/en-US/docs/Web/CSS/--*)
- [MDN: CSS Cascade — Order of Appearance](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascade/Specificity#order_of_appearance)
- [CSS Specificity calculator](https://specificity.keegan.st/)

