Type something to search...
How to Use CSS font-feature-settings for Advanced Typography?

How to Use CSS font-feature-settings for Advanced Typography?

You licensed a good typeface, loaded it on your pricing page, and the numbers in the comparison table still wobble from row to row because every digit has a different width. Meanwhile the brand guide shows a single-storey "a" and a slashed zero that you cannot find anywhere on the live site. The font file already contains all of those glyphs. You just have not asked for them. Modern OpenType fonts ship with dozens of hidden features, and CSS gives you two ways to switch them on: the high-level font-variant-* properties and the low-level font-feature-settings property.

This article explains what OpenType features are, how font-feature-settings works, when to reach for the font-variant-* properties instead, how to avoid the inheritance trap that catches almost everyone, and how to find out which features a font actually supports before you write a line of CSS.

What Are OpenType Features?

An OpenType feature is a set of substitution or positioning rules stored inside a font file, identified by a four-letter feature tag such as liga, tnum, or smcp. When the browser shapes text, it reads those rules and swaps or repositions glyphs. Some features are on by default (standard ligatures and kerning), and most are off until you request them.

A single font can contain many more glyphs than characters. The digit "1" might exist as a proportional oldstyle figure, a tabular lining figure, a superscript, and a numerator. Features are the switches that decide which one appears.

Here are the tags you will use most often on the web:

TagNameWhat it doesDefault
kernKerningApplies the font's pair-kerning tableOn
ligaStandard ligaturesReplaces fi, fl, ff with joined glyphsOn
caltContextual alternatesSwaps glyphs based on neighbours (common in scripts and code)On
dligDiscretionary ligaturesDecorative ligatures such as ct and stOff
tnumTabular figuresEvery digit gets the same advance widthOff
pnumProportional figuresDigits get natural widthsVaries
lnumLining figuresDigits sit on the baseline at cap heightVaries
onumOldstyle figuresDigits with ascenders and descenders, like lowercaseOff
zeroSlashed zeroDistinguishes 0 from OOff
fracFractionsTurns 1/2 into a proper diagonal fractionOff
supsSuperscriptTrue superscript glyphs instead of scaled onesOff
smcpSmall capitalsLowercase letters become small capsOff
c2scCaps to small capsUppercase letters become small capsOff
caseCase-sensitive formsRaises punctuation to suit all-caps textOff
ss01–ss20Stylistic setsFont-specific alternate designs, defined by the type designerOff
cv01–cv99Character variantsAlternates for individual charactersOff

The registered tags are documented in the OpenType specification, but stylistic sets and character variants mean whatever the designer decided. In Inter, ss01 gives you open digits and cv11 gives you a single-storey "a". In another font, ss01 might do something completely different.

How font-feature-settings Works

The syntax is a comma-separated list of quoted four-letter tags, each with an optional value:

.price-table {
  font-feature-settings: "tnum" 1, "zero" 1;
}

/* A value of 1 (or "on") enables, 0 (or "off") disables */
.no-ligatures {
  font-feature-settings: "liga" 0, "clig" 0;
}

/* Omitting the value means 1 */
.small-caps-heading {
  font-feature-settings: "smcp", "c2sc";
}

The tag must be exactly four ASCII characters and must be quoted. Most features are binary, but some take an integer that selects one of several alternates. The salt (stylistic alternates) and aalt features, for example, can expose multiple options, and "salt" 2 picks the second.

.logo-word {
  font-feature-settings: "salt" 2;
}

Browsers have supported unprefixed font-feature-settings for years, so you do not need -webkit- or -moz- variants in 2026.

Prefer the font-variant Properties When They Exist

font-feature-settings is the low-level escape hatch. For most common features, CSS has a higher-level property that you should use first. The CSS Fonts specification explicitly recommends this, and there are practical reasons for it.

GoalHigh-level propertyLow-level equivalent
Tabular figuresfont-variant-numeric: tabular-nums"tnum" 1
Oldstyle figuresfont-variant-numeric: oldstyle-nums"onum" 1
Slashed zerofont-variant-numeric: slashed-zero"zero" 1
Diagonal fractionsfont-variant-numeric: diagonal-fractions"frac" 1
Ordinals (1st, 2nd)font-variant-numeric: ordinal"ordn" 1
Small capsfont-variant-caps: small-caps"smcp" 1
All small capsfont-variant-caps: all-small-caps"smcp" 1, "c2sc" 1
Disable ligaturesfont-variant-ligatures: none"liga" 0, "clig" 0, "dlig" 0...
Discretionary ligaturesfont-variant-ligatures: discretionary-ligatures"dlig" 1
Superscriptfont-variant-position: super"sups" 1
Kerningfont-kerning: normal"kern" 1

The high-level properties have three advantages:

  1. They combine cleanly. font-variant-numeric and font-variant-caps are separate properties, so setting one never clobbers the other.
  2. They inherit independently. A child element can add small caps without losing the tabular figures it inherited from its parent.
  3. Browsers can synthesize fallbacks. If a font has no smcp feature, font-variant-caps: small-caps makes the browser fake small caps by scaling capitals. font-feature-settings does nothing when the feature is missing.

You can also combine multiple numeric values in one declaration:

.data-table td {
  font-variant-numeric: tabular-nums lining-nums slashed-zero;
}

If you are building data-heavy interfaces, the dedicated post on tabular figures for numbers and tables goes deeper into alignment and column design.

The Inheritance Trap

The single biggest gotcha with font-feature-settings is that it is one property holding a list. When a child sets it, the child's list replaces the parent's list entirely. Nothing merges.

body {
  font-feature-settings: "ss01" 1; /* brand alternates everywhere */
}

table {
  font-feature-settings: "tnum" 1; /* oops: ss01 is now off inside tables */
}

Inside the table you get tabular figures but lose the stylistic set. The fix is to repeat every feature you want at every level, which gets unmanageable fast. A cleaner approach uses custom properties so each feature is controlled independently:

:root {
  --ff-ss01: "ss01" 0;
  --ff-tnum: "tnum" 0;
  --ff-smcp: "smcp" 0;
  --ff-dlig: "dlig" 0;
}

* {
  font-feature-settings: var(--ff-ss01), var(--ff-tnum), var(--ff-smcp), var(--ff-dlig);
}

body {
  --ff-ss01: "ss01" 1;
}

table {
  --ff-tnum: "tnum" 1;
}

.eyebrow {
  --ff-smcp: "smcp" 1;
}

Custom properties inherit individually, and the universal selector recomputes the full list on every element. A table inside the body now gets both ss01 and tnum. If you are already building a token system, this pattern fits naturally alongside the approach in CSS custom properties for a typography system.

The other way to avoid the trap is simply to use font-variant-* for everything it covers and reserve font-feature-settings for stylistic sets and character variants, which have no simpler property.

Stylistic Sets and font-variant-alternates

Stylistic sets are where font-feature-settings earns its keep. They are font-specific, so there is no generic keyword for "the single-storey a". You have two options.

The direct option:

.brand {
  font-family: "Inter", system-ui, sans-serif;
  font-feature-settings: "cv11" 1, "ss01" 1;
}

The structured option uses @font-feature-values to give the sets readable names, then font-variant-alternates to apply them. This is supported in all current major browsers.

@font-feature-values Inter {
  @styleset {
    open-digits: 1;
    disambiguation: 2;
  }
  @character-variant {
    single-a: 11;
  }
}

.brand {
  font-family: "Inter", system-ui, sans-serif;
  font-variant-alternates: styleset(open-digits) character-variant(single-a);
}

.code-label {
  font-variant-alternates: styleset(disambiguation);
}

The advantage is readability and scoping: the names only apply to the font family named in the at-rule, so if a fallback font loads, you do not accidentally trigger an unrelated ss01 in some other typeface. That is a real problem with raw font-feature-settings, which applies to whatever font ends up rendering the text.

Setting Features at the @font-face Level

The @font-face rule accepts a font-feature-settings descriptor, which would let you bake features into a font definition so they apply wherever that face is used:

@font-face {
  font-family: "Brand Sans";
  src: url("/fonts/brand-sans.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap;
  font-feature-settings: "ss01" 1;
}

Support for this descriptor is inconsistent across engines, so do not rely on it as your only mechanism. Treat it as progressive enhancement and keep the feature declaration on your selectors as well. If you control the font build, a more reliable approach is to freeze features into the font file itself with a tool like pyftfeatfreeze from the opentype-feature-freezer package, which remaps default glyphs to their alternates.

How to Find Out Which Features a Font Supports

Writing "ss03" 1 for a font that has no ss03 does nothing and produces no error, so check first.

  1. Read the type foundry's specimen. Google Fonts lists features on many family pages, and commercial foundries usually publish a PDF or web specimen with every stylistic set illustrated.
  2. Use Wakamai Fondue. Drop a font file onto wakamaifondue.com and it lists every feature, shows a live preview of each one, and generates the CSS for you.
  3. Inspect the file from the command line. fonttools can dump the GSUB and GPOS tables:
pip install fonttools brotli

# Dump the substitution table to brand-sans.ttx, then list the feature tags
ttx -t GSUB brand-sans.woff2
grep -o 'FeatureTag value="[a-z0-9]*"' brand-sans.ttx | sort -u
  1. Check in DevTools. Firefox's Fonts panel in the Inspector shows the font actually used for an element, which confirms whether your fallback stack is rendering instead of the font you meant to test.

Do Not Strip Features When Subsetting

If you subset your fonts to cut file size, the subsetter may discard the features you want. pyftsubset keeps a default list of layout features (including kern, liga, calt, and a handful of others) and drops the rest unless you ask for them:

pyftsubset brand-sans.ttf \
  --unicodes="U+0000-00FF,U+2013-2014,U+2018-201D,U+2026" \
  --layout-features+=tnum,zero,smcp,c2sc,ss01,cv11 \
  --flavor=woff2 \
  --output-file=brand-sans-latin.woff2

Use --layout-features='*' to keep everything if you are unsure. Hosted services also subset aggressively: the Google Fonts CSS API serves files that may not include every stylistic set from the original family, so self-hosting the full file is sometimes the only way to get them.

Practical Recipes

Financial and Dashboard Numbers

.metric,
.data-table td,
.invoice-total {
  font-variant-numeric: tabular-nums lining-nums;
}

Elegant Running Text with Oldstyle Figures

Oldstyle figures blend into lowercase text and look less shouty in long paragraphs. Switch back to lining figures in headings and tables.

article p {
  font-variant-numeric: oldstyle-nums proportional-nums;
}

article h2,
article table {
  font-variant-numeric: lining-nums;
}

Small-Caps Abbreviations and Eyebrows

abbr,
.eyebrow {
  font-variant-caps: all-small-caps;
  letter-spacing: 0.05em;
}

A little extra tracking helps small caps breathe. Keep tracking in em so it scales with the text.

Turn Off Coding Ligatures in Prose

Fonts like Fira Code use calt to merge => and != into single glyphs. That is nice in an editor and confusing in a tutorial where the reader needs to know what to type.

pre code.no-ligatures {
  font-variant-ligatures: none;
  font-feature-settings: "calt" 0;
}

In Tailwind CSS v4

Tailwind ships utilities for the numeric variants (tabular-nums, oldstyle-nums, slashed-zero, diagonal-fractions, ordinal). For stylistic sets, define a small utility with the CSS-first @utility directive:

@import "tailwindcss";

@utility font-ss01 {
  font-feature-settings: "ss01" 1;
}

@utility font-cv11 {
  font-feature-settings: "cv11" 1;
}

Remember that two of these on the same element will fight, because they set the same property. For combined features, write a single named utility or use the custom property pattern shown earlier.

Performance and Rendering Notes

OpenType features are applied during text shaping, which every browser does anyway, so enabling a few features has no measurable runtime cost. The real costs are elsewhere:

  • File size. A font with dozens of stylistic sets and alternates is larger. Subset to the features you use.
  • Layout shift. Switching from proportional to tabular figures changes text width. If the feature is applied after the font loads, the swap from fallback to web font can shift layout more than usual. Metric overrides such as size-adjust on the fallback @font-face reduce that shift.
  • Fallback fonts. System fonts such as Segoe UI and San Francisco support tnum, but plenty of fallbacks support little else. Design so the page still reads correctly if the feature silently fails.

font-feature-settings FAQ

The font-variant properties are high-level, readable controls for common features such as tabular numbers, small caps, and ligatures. They inherit independently and can trigger synthesized fallbacks. The font-feature-settings property is a low-level list of raw OpenType tags that replaces itself entirely on each element. Use font-variant where it exists and font-feature-settings only for things like stylistic sets.

The most common reasons are that the font does not contain that feature, the feature was removed when the font was subset, a fallback font is rendering instead of your web font, or a more specific rule further down the cascade replaced your list. Check the font with a feature inspector and confirm in DevTools which font is actually in use.

Many Google Fonts families include features like tabular figures, small caps, and stylistic sets, but the files served by the Google Fonts API are often subset and may not include every feature from the source font. If a feature is missing, download the full family and self-host it.

Not smoothly. Feature values are discrete, so a transition just flips from one state to the other at the midpoint. If you need smooth typographic animation, use a variable font and animate its axes with font-variation-settings or the matching high-level properties instead.

Features change the visual glyph only, not the underlying characters, so screen readers, search engines, and copy and paste all see the original text. A slashed zero or a disambiguated lowercase l can actually improve legibility for readers who confuse similar shapes.

Standard ligatures are on by default and are usually fine in body text. Consider turning them off for letter-spaced text, where joined glyphs look odd, and for code samples using programming ligatures, where readers need to see the exact characters to type.

Conclusion

OpenType features are one of the cheapest upgrades you can make to a site's typography. The glyphs are already in the font you are paying for or downloading, and switching them on costs a line of CSS. Tabular figures fix jittery numbers, small caps make abbreviations quieter, oldstyle figures settle into running text, and stylistic sets let you match a brand guide exactly.

The discipline is in choosing the right tool. Reach for font-variant-numeric, font-variant-caps, and font-variant-ligatures first, because they combine and inherit sensibly. Use font-feature-settings for stylistic sets and character variants, wrap it in custom properties to avoid the inheritance trap, and confirm the feature actually exists in the file you ship, especially after subsetting.

Here are some useful references for going deeper on OpenType features in CSS:

  1. MDN Web Docs: font-feature-settings — syntax, values, and browser compatibility.
  2. MDN Web Docs: OpenType font features guide — a practical tour of features and their font-variant equivalents.
  3. W3C: CSS Fonts Module Level 4 — the specification for font-variant, font-feature-settings, and @font-feature-values.
  4. Microsoft Typography: Registered features — the official list of OpenType feature tags and what each one means.
  5. CSS-Tricks: font-feature-settings almanac entry — examples of common feature tags in practice.
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