Skip to main content
ASoc
Tutorial

Tailwind Font Weight: 9 Named Steps, 4 This Codebase Uses

193 font-weight utility calls across this codebase, spanning only 4 of Tailwind's 9 named steps — traced to two atoms that set the hierarchy once.

The ASoc Team10 min read

Tailwind CSS ships nine named font-weight utilities — font-thin through font-black, mapped to CSS values 100–900 in --font-weight-* theme variables — and this storefront's own components use four of them: font-normal, font-medium, font-semibold and font-bold. The other five have zero call sites. That's not a gap; it's what a real interface actually needs once you stop treating "the full type scale" as a checklist.

The short answer

Apply font-{name}font-bold, font-semibold, font-medium, and so on — to set font-weight via a CSS custom property Tailwind defines in @theme, not a hardcoded number. Whether every named weight actually renders differently depends on the font file loaded, which is the part most Tailwind tutorials skip.

The nine weights, straight from the installed package

/* node_modules/tailwindcss/theme.css */
--font-weight-thin: 100;
--font-weight-extralight: 200;
--font-weight-light: 300;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-extrabold: 800;
--font-weight-black: 900;

font-{name} just applies font-weight: var(--font-weight-{name}). Tailwind doesn't validate that your loaded font file actually has a matching weight — request font-thin on a font with no 100-weight cut, and the browser synthesizes a fake bold/thin or silently falls back to the nearest weight it has, which is a rendering decision Tailwind has no visibility into.

What this site's own font setup avoids

// src/app/layout.tsx
import { Outfit } from "next/font/google";

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

No weight array is passed. Outfit is a variable font — Google serves one file covering its entire weight axis — so every one of Tailwind's nine font-* utilities resolves to a real, distinct rendered weight without next/font downloading nine separate static files. That's the opposite of the common mistake: pinning weight: ["400", "700"] on a variable font family throws away the axis you're paying to download and silently breaks every other font-* class in the app, which then falls back to the nearest of the two weights you did load.

/* src/app/globals.css */
--font-sans: var(--font-outfit), ui-sans-serif, system-ui, sans-serif;
@layer base {
  body {
    @apply bg-white font-sans text-base font-normal text-gray-700;
  }
}

font-normal (400) is the body default, set once in @layer base rather than repeated on every element — everything heavier is an intentional override at the component that needs emphasis.

What this codebase actually uses

A grep of every component, app-route and data file for the nine font-* utilities:

UtilityCSS valueCall sites in this repo
font-thin1000
font-extralight2000
font-light3000
font-normal4003
font-medium500129
font-semibold60030
font-bold70031
font-extrabold8000
font-black9000

193 total call sites, spanning 4 of the 9 named steps. font-medium alone accounts for two-thirds of them — it's the default weight for nav links, card labels, form labels and button text across every atom and molecule in the design system. font-bold and font-semibold cluster around headings and section labels. The four extremes — thin, extralight, light, extrabold, black — never appear, because a marketing-and-dashboard storefront's actual type hierarchy needs "body," "emphasis" and "heading," not a nine-step gradient.

The three-tier hierarchy hiding in two atoms

The 193-call-site total isn't scattered randomly across the codebase — most of it traces back to two atoms setting a weight once, by default, for every section on the site:

// src/components/atoms/SectionHeading.tsx
export default function SectionHeading({
  as: Tag = "h2",
  className = "text-3xl font-bold !leading-[1.2] text-title-color md:text-[40px]",
  children,
}: { /* ... */ }) {
  return <Tag className={className}>{children}</Tag>;
}
// src/components/atoms/SectionLabel.tsx — the small eyebrow above a heading
export default function SectionLabel({ className = "", children }: { /* ... */ }) {
  return (
    <span className={`inline-block text-lg font-medium text-primary ${className}`.trim()}>
      {children}
    </span>
  );
}

Three defaults, three atoms, three weights: body is font-normal (400, set once in @layer base), SectionLabel is font-medium (500), SectionHeading is font-bold (700). Every organism on the site composes from these atoms, so a new section gets a correct, consistent weight hierarchy for free — the author never chooses a font-* class by hand unless a specific card or button needs to break from it. That's most of why font-medium, font-semibold and font-bold show up hundreds of times combined while font-thin through font-light and font-extrabold/font-black show up zero: the three-step hierarchy is baked into the atoms, and nothing in this design system has ever needed a fourth step.

font-semibold (600) sits between the two atom defaults, at 30 call sites — used at the molecule level where a component needs more visual weight than a label but doesn't want to claim a full <h2>'s boldness: card titles, table headers, a FaqItem question button.

The comparison that matters

WeightTypical useUsed here?
font-thin / font-extralight / font-lightLarge display type, hero headlines in some design systemsNo — this site's headings use font-bold, not a lighter display cut
font-normalBody copy defaultYes — set once, globally, in @layer base
font-mediumUI chrome: nav links, labels, buttonsYes — 129 call sites, the workhorse weight
font-semiboldSection labels, card titlesYes — 30 call sites
font-boldHeadings, emphasisYes — 31 call sites
font-extrabold / font-blackOversized display headlinesNo — nothing on this site goes past 700

Responsive and state variants

Every font-* utility takes the same breakpoint and pseudo-class prefixes as any other Tailwind utility:

<Link
  className="font-medium text-text-color group-hover:text-primary
             dark:text-white/60 dark:group-hover:text-white"
  href="/pricing"
>
  Pricing
</Link>

This nav link — from src/components/organisms/Header.tsx — stays font-medium in every state; only the color shifts on hover and in dark mode. That's deliberate: changing font-weight on hover causes the classic layout-shift bug, because a heavier weight is wider, and unlike color there's no way to transition it smoothly (browsers don't animate discrete font-weight steps the way font-variation-settings on a true variable-font axis can).

Weight as a substitute for color contrast — and where it isn't one

A heavier weight can sometimes compensate for lower color contrast, because thicker strokes are easier to read at a given contrast ratio — but WCAG's contrast formula doesn't take font-weight into account at all below its "large text" threshold (24px regular, or roughly 18.66px bold and up), so font-bold on a marginal color pairing does not pass an accessibility check that the same color at font-normal fails, unless the text is also large enough to qualify for the relaxed large-text ratio. This site's own BlogTag component hit exactly that edge — see the Aug 2026 pass documented in this repo's CLAUDE.md, which found a tag missing AA contrast by 0.01 and fixed the color, not the weight, because the text was body-sized and weight alone wasn't going to close a contrast gap that small in a way a checker would credit.

Font weight and choosing a CSS approach at all

The bigger question this sidesteps — why reach for utility classes instead of hand-written CSS at all — is covered in Tailwind vs. CSS and, for teams coming from a component-library background, Tailwind vs. Bootstrap Dashboards. Font-weight utilities are one small corner of that larger trade: a --font-weight-* variable per named step, versus writing font-weight: 700; by hand in a stylesheet that has to stay in sync with every place a heading appears.

Dark mode: weight stays put, color does the work

This site's class-based dark mode toggles color tokens, not weight tokens. Every font-* class in this codebase is weight-only; dark-mode adjustments are handled entirely through separate dark: color utilities on the same element, never by swapping to a different weight per theme:

className="font-medium text-text-color group-hover:text-primary
           dark:text-white/60 dark:group-hover:text-white"

Keeping weight constant across themes matters for the same reason it matters across hover states — a heavier weight renders wider, so a theme toggle that also changed font-weight would reflow text on every switch. The one thing that does look different between themes at an unchanged weight is anti-aliasing: light text on a dark background typically renders visually thinner than dark text on light at the identical font-weight value, which is a rendering artifact of subpixel/gamma-correct anti-aliasing, not a reason to bump the Tailwind class per theme.

Troubleshooting

SymptomCauseFix
font-bold renders identically to font-semiboldThe loaded font file has no distinct 600/700 cuts, or is a non-variable static font missing those weightsLoad a variable font, or add the specific static weight files you need
Custom weight like font-[550] does nothingArbitrary numeric values need square-bracket syntax and a font that actually has that weightUse font-[550] only with a variable font whose axis covers 550, and verify it renders distinctly
Nav link shifts layout on hoverfont-weight changed on :hover and the heavier weight is widerKeep weight constant across states; transition color or a transform instead
next/font still downloads every weight even though you only vary twoA weight array was set on a variable font, discarding the rest of the axisDrop the weight option entirely for variable fonts — the whole axis loads in one file
Dark mode text looks thinner than light mode at the same font-* classFont smoothing/anti-aliasing renders lighter on a dark background, not an actual weight changeThis is a rendering artifact, not a bug — don't compensate by bumping the Tailwind weight class per theme

Frequently asked questions

What's the difference between font-bold and font-extrabold? font-bold sets font-weight: 700; font-extrabold sets 800. Whether they look different depends entirely on whether your loaded font file has distinct cuts at both values — a variable font like Outfit renders them distinctly, but many static font deliveries only ship 400/700 and silently collapse the rest.

Can I use a numeric value instead of a name? Yes, with Tailwind's arbitrary-value syntax: font-[550]. It bypasses the named scale entirely and only makes sense against a variable font whose weight axis actually includes that value.

Why does this site only use 4 of the 9 named weights? Because a storefront's type hierarchy is body/emphasis/heading, not a nine-step gradient — the measured call-site count above (193 total, zero at either extreme) is the evidence, not a guess.

Does next/font load every weight even if I don't use most of them? Only if you explicitly restrict weight to a static list, in which case it downloads exactly those files. Omit weight on a variable font family (as this repo does with Outfit) and one file covers the whole axis, used or not.

Templates in this post

ASoc Fade, ASoc Fiscal and ASoc Flow are Next.js + Tailwind landing page templates built on the same variable-font setup described above.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates.

Keep reading

Tutorial9 min read

Tailwind Grid: 26 Files, and Not One col-span

A 538-page site's whole grid vocabulary is three utilities — plus the display:contents trick that reorders a product page on mobile without duplicating state.

Read more