Home / Snippets / Typography /
Variable-weight heading
Animate each word across the full wght axis — from hairline 100 to heavy 900 — using font-variation-settings and smooth transitions.
Extra thin to light to regular to bold black
Hover the heading to animate all words to full weight.
Quick implementation
/* Variable-weight heading — each word gets a wght value */
.varweight-heading {
font-family: 'DM Sans', sans-serif; /* must be a variable font */
display: flex;
flex-wrap: wrap;
gap: 0.25em;
}
.varweight-heading span {
display: inline-block;
transition: font-variation-settings 0.35s ease, color 0.35s ease;
}
/* Assign weight stops across the wght axis (100–900) */
.varweight-heading span:nth-child(1) { font-variation-settings: 'wght' 100; }
.varweight-heading span:nth-child(2) { font-variation-settings: 'wght' 200; }
.varweight-heading span:nth-child(3) { font-variation-settings: 'wght' 300; }
.varweight-heading span:nth-child(4) { font-variation-settings: 'wght' 400; }
.varweight-heading span:nth-child(5) { font-variation-settings: 'wght' 500; }
.varweight-heading span:nth-child(6) { font-variation-settings: 'wght' 600; }
.varweight-heading span:nth-child(7) { font-variation-settings: 'wght' 700; }
.varweight-heading span:nth-child(8) { font-variation-settings: 'wght' 800; }
.varweight-heading span:nth-child(9) { font-variation-settings: 'wght' 900; }
/* Hover: animate all spans to full weight with staggered delay */
.varweight-heading:hover span {
font-variation-settings: 'wght' 900;
}
.varweight-heading:hover span:nth-child(1) { transition-delay: 0s; }
.varweight-heading:hover span:nth-child(2) { transition-delay: 0.04s; }
.varweight-heading:hover span:nth-child(3) { transition-delay: 0.08s; }
.varweight-heading:hover span:nth-child(4) { transition-delay: 0.12s; }
.varweight-heading:hover span:nth-child(5) { transition-delay: 0.16s; }
.varweight-heading:hover span:nth-child(6) { transition-delay: 0.20s; }
.varweight-heading:hover span:nth-child(7) { transition-delay: 0.24s; }
.varweight-heading:hover span:nth-child(8) { transition-delay: 0.28s; }
.varweight-heading:hover span:nth-child(9) { transition-delay: 0.32s; }
@media (prefers-reduced-motion: reduce) {
.varweight-heading span {
transition: none;
}
}
Prompt this to your LLM
Includes role, font requirements, axis constraints, hover variant, and accessibility considerations.
You are a senior frontend engineer implementing advanced variable font
typography for a dark-mode design system.
Goal: A heading where each word (wrapped in a <span>) is set at a
different font-weight stop along the wght axis of a variable font,
creating a visual gradient from hairline-thin to heavy-black across
the text. Include a hover effect that smoothly animates all words to
full weight with a staggered transition-delay.
Technical constraints:
- The parent element must use a variable font that supports the wght
axis across 100–900 (e.g. DM Sans, Inter, or any VF with full range).
- Set weight via font-variation-settings: 'wght' N — not font-weight —
so intermediate values like 350 or 750 are available.
- Each <span> must have display: inline-block so transitions apply
(inline elements do not animate font-variation-settings reliably).
- Transition on font-variation-settings and color, using ease timing.
- On :hover of the parent, move all spans to 'wght' 900 with
transition-delay increments of ~0.04s per child for a wave effect.
- Use oklch() for any color values — no hex or rgba.
- Use CSS custom properties for theming (var(--text), var(--accent)).
Framework variant (pick one):
A) Pure CSS with :nth-child selectors — no JavaScript needed.
B) React component — accept a words prop (string[]) and render each
word in its own <span> with inline style for font-variation-settings,
computing the weight stop from its index.
Edge cases to handle:
- If the font does not support the full wght axis, clamp values to the
declared range in the @font-face descriptor (use font-weight: 100 900
in @font-face to declare full range support).
- Wrapping: use display: flex; flex-wrap: wrap; gap: 0.25em on the
parent so words wrap naturally at narrow viewports without breaking
the per-word weight assignment.
- Accessibility: the heading should have aria-label with the full plain
text so screen readers announce it as a single phrase, not nine
separate words with pauses.
- Include @media (prefers-reduced-motion: reduce) that sets
transition: none on all spans.
Return CSS only (or a React component if variant B is chosen).
Why variable fonts change the weight game
Before variable fonts, loading a heading in nine different weights meant nine separate HTTP requests — one font file per weight. Variable fonts collapse the entire weight range into a single file. The wght axis is a continuous slider from 100 to 900 (or whatever range the typeface declares), and the browser interpolates glyph shapes between the defined masters. You can set font-variation-settings: 'wght' 350 or 'wght' 763 — values that simply do not exist in static font families.
This makes the weight-gradient heading technique practical. In a static-font world, nine weights would cost hundreds of kilobytes of extra font data. With a single variable font file like DM Sans Variable, you get all 800 intermediate stops for free. The heading demo above uses DM Sans's full wght axis and transitions between stops on hover — something that would be impossible to animate smoothly with static fonts.
The wght axis and font-variation-settings
The font-variation-settings property accepts a list of axis tags and values: font-variation-settings: 'wght' 700, 'ital' 1. The wght tag controls weight. It is distinct from font-weight — while both influence the same visual output, font-variation-settings bypasses the keyword-to-number mapping and speaks directly to the font's internal axis. CSS allows you to write font-weight: 350 as a number too (since CSS Fonts Level 4), but font-variation-settings gives you explicit control and works even in older variable-font implementations.
One caveat: font-variation-settings is a low-level override that resets all other axis values to their defaults whenever it appears. If your font has multiple axes — say wght and opsz (optical size) — you must list all of them in every font-variation-settings declaration, or the missing ones revert to default. For single-axis control, font-weight with a numeric value is often cleaner.
To transition weight smoothly, font-variation-settings is fully animatable. The browser interpolates the numeric axis value between the from and to states, producing a smooth weight morph. This is what drives the hover animation: each <span> transitions from its assigned weight stop to 900 over 350ms, with a small transition-delay increment per word to create a cascading wave.
Accessibility & performance notes
Wrapping each word in a <span> introduces a risk for screen readers: some assistants pause briefly between inline elements, making "Extra thin to light to regular to bold black" sound choppy. Add aria-label with the full heading text on the parent <p> or <h1>, and set aria-hidden="true" on each child <span> to let assistive technology announce the phrase as a single unit.
Variable font transitions are compositor-friendly — the browser can animate the wght axis without triggering layout. However, text shaping (glyph outline interpolation) does require some CPU work, especially for large display sizes. Keep the animated heading to a reasonable font size and avoid animating dozens of spans simultaneously. The staggered transition-delay pattern naturally spreads the work over time rather than recalculating all spans at once.
Always include @media (prefers-reduced-motion: reduce) to disable transitions for users who have opted out of motion. The heading still shows the weight gradient statically — which is the main visual point — it just does not animate on hover.