Type something to search...
How to use the CSS font-feature-settings property?

How to use the CSS font-feature-settings property?

The font-feature-settings property gives you direct, low-level control over OpenType features in a font. You write a comma-separated list of four-letter feature tags in quotes, each followed by an optional value, such as font-feature-settings: "tnum" 1, "ss01" 1, "liga" 0. A value of 1 or on turns a feature on, 0 or off turns it off, and higher numbers pick a specific alternate glyph. Because each declaration replaces the whole list rather than adding to it, it's easy to switch features off by accident, so the CSS Fonts specification recommends using the font-variant-* properties wherever they cover the feature you need. Use font-feature-settings for features that have no higher-level equivalent, such as stylistic sets in some setups, character variants or a font's custom features.

OpenType fonts often contain far more than their default glyphs: tabular figures, stylistic alternates, fractions, slashed zeros and much more. font-feature-settings is the one property that can reach every one of them, which makes it powerful and easy to misuse. In this article you'll learn the exact syntax, how to find which features a font supports, how inheritance trips people up and a pattern that avoids it, how it relates to the font-variant properties, and how to keep features working when you subset or self-host fonts.

The Syntax

The property takes normal or a list of feature tags:

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

The rules are strict:

  • Tags are four characters: such as "liga", "kern", "smcp" or "ss01".
  • Tags must be quoted: single or double quotes both work.
  • Tags are case-sensitive: "SMCP" is not the same as "smcp". Registered OpenType tags are lowercase.
  • Values are optional: if you omit the value, it defaults to 1, so "tnum" and "tnum" 1 mean the same.
  • on and off are keywords: "liga" off equals "liga" 0.
  • Numbers above 1 select alternates: for features with several alternates, like "salt" 2 or "cv03" 4, the number picks which one.

An invalid tag, such as one with five characters, makes the entire declaration invalid, so the browser ignores the whole line, not just the bad tag. If several features suddenly stop working, check for a typo.

Commonly Used Tags

TagFeatureTypical use
kernKerningOn by default; turn off only for testing
ligaStandard ligaturesOn by default; turn off for code or letter-spaced text
dligDiscretionary ligaturesDecorative headings
tnumTabular figuresTables, prices, timers
pnumProportional figuresRunning text
onumOld-style figuresNumbers within body text
lnumLining figuresNumbers with capitals, UI
zeroSlashed zeroCodes, serial numbers
fracFractionsRecipes, measurements
smcpSmall capitalsAbbreviations, labels
c2scCapitals to small capitalsAll-small-caps text
caseCase-sensitive formsPunctuation in all-caps text
ss01 to ss20Stylistic setsAlternate letter designs
cv01 to cv99Character variantsAlternate forms of one character

Which of these actually do anything depends entirely on the font. A feature the font doesn't contain is simply ignored.

Finding Which Features a Font Supports

Before writing CSS, find out what the font offers. There are three practical ways.

Check the Foundry's Documentation

Most good fonts have a specimen page listing features, often with examples. Google Fonts' pages and the specimen sites for open-source families such as Inter or IBM Plex typically document their stylistic sets and character variants.

Inspect the Font in a Browser Tool

Firefox's DevTools have a Fonts panel showing which font renders the selected text, and online tools such as Wakamai Fondue let you drop in a font file and see every feature with a live preview.

List Features with fontTools

For a definitive answer, read the font file directly with the Python fontTools library. Install it, along with Brotli for WOFF2 support:

pip install fonttools brotli

Then save this script as features.py:

import sys
from fontTools.ttLib import TTFont

font = TTFont(sys.argv[1])
tags = set()

for table in ("GSUB", "GPOS"):
    if table in font:
        for record in font[table].table.FeatureList.FeatureRecord:
            tags.add(record.FeatureTag)

print(" ".join(sorted(tags)))

Run it against a font file:

python features.py fonts/Inter-Regular.woff2
aalt c2sc calt case ccmp cv01 cv02 cv03 cv04 cv05 dlig dnom frac kern liga locl mark mkmk numr onum pnum salt sinf smcp ss01 ss02 ss03 subs sups tnum zero

The output above is an abbreviated example; the exact list depends on the font and the version you have. GSUB features substitute glyphs, such as ligatures or alternates, and GPOS features adjust positioning, such as kerning.

The Inheritance Trap

font-feature-settings is inherited, which sounds convenient. The catch is that a new declaration on a child element replaces the inherited list entirely. It doesn't merge.

body {
  font-feature-settings: "ss01" 1;   /* a stylistic set for the brand look */
}

.price {
  font-feature-settings: "tnum" 1;   /* tabular figures for prices */
}

You might expect prices to have both the stylistic set and tabular figures. They don't. Inside .price, the computed value is only "tnum" 1, so ss01 is switched off and prices suddenly look different from the surrounding text.

The direct fix is to repeat every feature you want:

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

That works for small sites but becomes fragile as more combinations appear.

A Pattern Using Custom Properties

A more maintainable approach uses one custom property per feature. Custom properties inherit and can be changed independently, and a single rule combines them:

:root {
  --feat-ss01: "ss01" 0;
  --feat-tnum: "tnum" 0;
  --feat-zero: "zero" 0;
  --feat-case: "case" 0;
}

*,
*::before,
*::after {
  font-feature-settings:
    var(--feat-ss01),
    var(--feat-tnum),
    var(--feat-zero),
    var(--feat-case);
}

/* Turn features on wherever they're needed */
body          { --feat-ss01: "ss01" 1; }
.price,
td.numeric    { --feat-tnum: "tnum" 1; }
.order-code   { --feat-zero: "zero" 1; }
.eyebrow      { --feat-case: "case" 1; }

Now a price inside the body keeps ss01 from body and adds tnum from .price, because each feature is tracked separately. Adding a new feature means adding one variable and one line in the combining rule.

Prefer font-variant Where It Exists

The font-variant-* properties are the high-level way to switch on most common features. They don't have the inheritance trap, because each property only controls its own group of features, and they communicate intent more clearly.

font-feature-settingsHigher-level equivalent
"liga" 0font-variant-ligatures: no-common-ligatures
"dlig" 1font-variant-ligatures: discretionary-ligatures
"tnum" 1font-variant-numeric: tabular-nums
"onum" 1font-variant-numeric: oldstyle-nums
"frac" 1font-variant-numeric: diagonal-fractions
"zero" 1font-variant-numeric: slashed-zero
"smcp" 1font-variant-caps: small-caps
"smcp" 1, "c2sc" 1font-variant-caps: all-small-caps
"sups" 1font-variant-position: super
"kern" 0font-kerning: none

So the price example is better written as:

body {
  font-feature-settings: "ss01" 1;
}

.price {
  font-variant-numeric: tabular-nums;
}

Now .price inherits ss01 and adds tabular figures, with no repetition. You can also combine several numeric values in one declaration, such as font-variant-numeric: tabular-nums slashed-zero.

A useful bonus of font-variant-caps: small-caps is that if the font has no small caps, browsers can synthesise them. With "smcp" 1, nothing happens if the feature is missing.

When font-feature-settings Is Still the Right Tool

Some features have no dedicated font-variant value, or only have one through a more involved mechanism:

  • Character variants (cv01 and up): such as Inter's alternate forms of individual letters.
  • Stylistic sets (ss01 to ss20): these can be reached through font-variant-alternates with an @font-feature-values rule, but that requires naming each set per font family. font-feature-settings is often simpler.
  • Contextual alternates (calt): on by default; turning them off needs font-variant-ligatures: no-contextual or "calt" 0.
  • Case-sensitive forms (case): no font-variant value exists for this.
  • Custom or uncommon features: some fonts include features specific to the foundry.

Using @font-feature-values for Alternates

For completeness, here's the higher-level way to use a stylistic set. It's supported in current versions of Chrome, Edge, Firefox and Safari:

@font-feature-values Inter {
  @styleset {
    open-digits: 1;
    round-quotes: 3;
  }
}

.brand-heading {
  font-family: Inter, sans-serif;
  font-variant-alternates: styleset(open-digits, round-quotes);
}

This maps readable names to ss01 and ss03 for Inter only. The benefit is that if the font changes, the names no longer apply, instead of accidentally triggering an unrelated stylistic set in the new font.

Order of Precedence

When both mechanisms touch the same feature, the CSS Fonts specification defines the order. Features enabled by default (such as kern and liga) are applied first, then font-variant-* and font-kerning settings, then font-feature-settings last. In other words, the low-level property wins:

.demo {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 0;   /* this wins: figures are proportional */
}

That's useful for overriding a component library's defaults, but it also means a stray font-feature-settings can silently cancel a font-variant rule.

Variable Fonts and Features

font-feature-settings controls OpenType layout features, not variation axes. Weight, width, slant and custom axes in variable fonts use font-variation-settings or, better, high-level properties like font-weight and font-stretch. The two are often confused because they have similar syntax:

.headline {
  font-weight: 650;                       /* variation axis via high-level property */
  font-variation-settings: "opsz" 48;     /* low-level axis setting */
  font-feature-settings: "ss02" 1;        /* layout feature */
}

Both low-level properties share the same inheritance trap, so the custom property pattern above applies to font-variation-settings too.

Keeping Features When You Subset or Self-Host

Features live in the font file, so if the file doesn't contain them, no CSS will help. Two common ways to lose them:

  • Hosted font services: some services subset fonts for size and may not include every OpenType feature in every file they serve. If a feature works locally but not on the live site, compare the files.
  • Your own subsetting: tools like pyftsubset keep only a default list of layout features unless told otherwise.

To keep all features while subsetting to Latin characters:

pyftsubset Inter-Regular.ttf \
  --unicodes="U+0000-00FF,U+2000-206F,U+20AC" \
  --layout-features='*' \
  --flavor=woff2 \
  --output-file=Inter-Regular-latin.woff2

To keep only specific features and save a little more space, list them instead, for example --layout-features='kern,liga,calt,tnum,ss01'.

If you're applying features to data-heavy interfaces, tabular vs proportional numerals explains which figure style suits which content.


FAQ: font-feature-settings

It turns individual OpenType features in a font on or off, such as ligatures, tabular figures, small caps, stylistic sets and character variants. It works only if the font actually contains the feature.

Each font-feature-settings declaration replaces the full list instead of adding to it. A child rule with one tag switches off features set on its parent. Repeat all the tags you need, use font-variant properties, or use one custom property per feature.

Use font-variant properties when they cover the feature, because they're clearer and don't override unrelated features. Use font-feature-settings for features without a font-variant equivalent, such as character variants or case-sensitive forms.

Check the foundry's specimen page, load the file into a feature inspection tool such as Wakamai Fondue, or list the GSUB and GPOS features with the Python fontTools library.

Yes. It's supported unprefixed in all current versions of Chrome, Edge, Firefox and Safari. Very old browsers needed vendor prefixes, but those are no longer necessary.

It works with any font that includes the feature. Hosted files may not always include every feature the original font has, so if a feature doesn't appear, try self-hosting the full font or check the file with fontTools.


Conclusion

font-feature-settings is the most direct way to control OpenType features: a list of quoted four-letter tags with optional values that switch features on, off or to a specific alternate. It reaches everything a font offers, from stylistic sets and character variants to case-sensitive punctuation, and it wins over all other feature settings when they conflict.

That power comes with one major catch: each declaration replaces the inherited list. Reach for font-variant-* properties first, use font-feature-settings for what they can't do, and adopt the custom property pattern if you need several features in combination. Check which features your font really contains, keep them when you subset, and your typography can make full use of the details type designers built in.

Share :

Related Posts

How to make typography accessible?

How to make typography accessible?

You make typography accessible by choosing clear typefaces, setting text in relative units so it scales with user preferences, giving it enough colou

Dive Deeper
What are the parts of a letterform?

What are the parts of a letterform?

A letterform is the shape of a single letter, and typographers break it down into named parts. The main ones are the stem (the main vertical

Dive Deeper
What are ascenders, descenders and the baseline?

What are ascenders, descenders and the baseline?

The baseline is the invisible line that letters sit on. Ascenders are the parts of lowercase letters that rise above the x-height, such as th

Dive Deeper