OpenType Features: Ligatures, Tabular Numbers, and Small Caps
font-feature-settings doesn't cascade—it replaces. Learn the high-level font-variant-* alternatives
Enable ligatures with font-feature-settings: "liga"—then add tabular numbers with font-feature-settings: "tnum" on a child element and watch the ligatures disappear. OpenType feature settings don't cascade—they replace. Each declaration overwrites all previous features, creating unexpected rendering bugs in production.
What Are OpenType Features?
OpenType fonts contain optional typographic features—ligatures, alternate glyphs, numeric styles, small caps—that designers can activate with CSS. Think of them as switches built into the font file itself. A single font might include dozens of features, but only a few are enabled by default.
/* Common OpenType features */
liga /* Standard ligatures (fi, fl, ff) */
dlig /* Discretionary ligatures (ct, st) */
tnum /* Tabular numbers (fixed-width digits) */
onum /* Old-style numbers (3 has descender) */
smcp /* Small capitals */
c2sc /* Capitals to small caps */
calt /* Contextual alternates (smart glyph substitution) */The problem: CSS provides two ways to control these features—font-feature-settings and font-variant-* properties—and they behave radically differently.
font-feature-settings: The Low-Level API
font-feature-settings is a direct pass-through to OpenType's feature tags. It gives complete control but has a critical flaw: it doesn't cascade, it replaces.
/* This DOES NOT work as expected */
.parent {
font-feature-settings: "liga" 1; /* Enable ligatures */
}
.child {
font-feature-settings: "tnum" 1; /* Enable tabular numbers */
}
/* Result: .child has ONLY "tnum", no "liga" */
/* The child declaration completely replaces the parent's */Every font-feature-settings declaration must explicitly list all features you want active. There's no accumulation, no merging.
/* Correct approach: list all features */
.child {
font-feature-settings: "liga" 1, "tnum" 1;
/* Now both ligatures AND tabular numbers are active */
}This makes font-feature-settings fragile in component-based codebases where multiple CSS rules might apply to the same element.
font-variant-*: The High-Level API
The font-variant-* properties (CSS Fonts Module Level 4) provide named properties for common features. Unlike font-feature-settings, these cascade properly.
/* This DOES work as expected */
.parent {
font-variant-ligatures: common-ligatures; /* Enables "liga" */
}
.child {
font-variant-numeric: tabular-nums; /* Enables "tnum" */
}
/* Result: .child has BOTH ligatures and tabular numbers */
/* Properties cascade independently */Always prefer font-variant-* over font-feature-settings when a high-level property exists. Use font-feature-settings only for features without dedicated properties (like stylistic sets ss01, ss02).
Ligatures: liga, dlig, clig
Ligatures combine multiple characters into a single glyph for better visual flow. Most fonts have "fi" and "fl" ligatures to prevent collision between the f's hook and the ascender of i/l.
Standard Ligatures (liga)
Common ligatures (fi, fl, ff, ffi, ffl) enabled by default in most browsers. Disable them explicitly:
/* Disable standard ligatures */
.no-ligatures {
font-variant-ligatures: no-common-ligatures;
/* Equivalent: font-feature-settings: "liga" 0; */
}Use case: Code snippets. Ligatures in code (like fi in filter) hurt readability—readers expect individual characters.
/* Disable ligatures in code blocks */
code, pre {
font-variant-ligatures: none; /* Disables all ligatures */
}Discretionary Ligatures (dlig)
Stylistic ligatures (ct, st, sp) for display typography. Disabled by default; opt-in for headings:
h1, h2 {
font-variant-ligatures: common-ligatures discretionary-ligatures;
/* Enables both "liga" and "dlig" */
}Contextual Ligatures (clig)
Script fonts use contextual ligatures to connect letters. Enabled by default in OpenType fonts.
Numeric Features: tnum, lnum, onum
Numbers have multiple stylistic variants in OpenType fonts. The wrong choice makes data tables unreadable.
Tabular Numbers (tnum)
Fixed-width digits that align vertically in columns. Critical for financial data, tables, timers.
/* Proportional (default) - numbers have varying widths */
.price-bad {
font-variant-numeric: normal;
}
/* Tabular - numbers have fixed widths */
.price-good {
font-variant-numeric: tabular-nums;
/* Equivalent: font-feature-settings: "tnum" 1; */
}Example impact:
/* Without tabular nums */
$1,234.56
$ 89.12
$ 456.00
/* Numbers don't align - hard to scan */
/* With tabular nums */
$1,234.56
$ 89.12
$ 456.00
/* Perfect alignment - easy to scan */Lining vs Old-Style Numbers
Lining numbers (lnum): All digits same height, sit on baseline. Default for most fonts. Best for UI, data tables.
Old-style numbers (onum): Digits have ascenders/descenders (3, 4, 5, 7, 9 drop below baseline). Better for body text, editorial design.
/* Old-style numbers in prose */
.article-body {
font-variant-numeric: oldstyle-nums;
/* The year 2025 renders with varying heights */
}Small Caps: smcp, c2sc
True small caps are specially designed uppercase glyphs sized to match lowercase x-height. They're not just scaled-down capitals (which look too thin and spindly).
Lowercase to Small Caps (smcp)
/* Convert lowercase to small caps */
.small-caps {
font-variant-caps: small-caps;
/* Activates "smcp" feature */
}
/* Input: The Quick Brown Fox */
/* Output: THE QUICK BROWN FOX (with lowercase as smaller caps) */Capitals to Small Caps (c2sc)
Converts existing capitals to small caps. Combine with smcp for all-small-caps text:
/* All characters to small caps */
.all-small-caps {
font-variant-caps: all-small-caps;
/* Activates both "smcp" and "c2sc" */
}
/* Input: The Quick Brown Fox */
/* Output: ᴛʜᴇ ǫᴜɪᴄᴋ ʙʀᴏᴡɴ ғᴏx (all uniform small caps) */Fake vs True Small Caps
If the font lacks OpenType small caps features, browsers synthesize them by scaling capitals to 80%. These look terrible—uneven stroke weights, poor readability.
/* Check if font supports real small caps */
@supports (font-variant-caps: small-caps) {
.small-caps {
font-variant-caps: small-caps; /* Use real small caps */
}
}
/* Fallback: don't use small caps at all */
@supports not (font-variant-caps: small-caps) {
.small-caps {
text-transform: uppercase; /* Just use regular capitals */
font-size: 0.85em;
}
}Contextual Alternates: calt
Smart glyph substitution based on surrounding characters. The font analyzes context and swaps glyphs for better flow. Examples:
- Script fonts: Connecting tail of 'a' extends when followed by 'n'
- Sans-serif: Adjust spacing when 'T' is followed by 'o' (overlap)
- Monospace code fonts: Swap '!=' to '≠' ligature
Contextual alternates are enabled by default. Disable them if they cause issues:
/* Disable contextual alternates */
.no-alternates {
font-variant-ligatures: no-contextual;
/* Equivalent: font-feature-settings: "calt" 0; */
}Common problem: Some code fonts (Fira Code, JetBrains Mono) use calt for programming ligatures (-> becomes →). Disable for non-code text:
/* Allow programming ligatures in code */
code {
font-variant-ligatures: contextual;
}
/* Disable programming ligatures in UI */
body {
font-variant-ligatures: no-contextual;
}Combining Multiple Features
Use separate font-variant-* properties—they compose properly:
/* Financial table cell */
.table-number {
font-variant-numeric: tabular-nums lining-nums;
font-variant-ligatures: none; /* No ligatures in numbers */
}
/* Elegant heading */
h1 {
font-variant-ligatures: common-ligatures discretionary-ligatures;
font-variant-caps: small-caps;
}
/* Code block */
code {
font-variant-ligatures: none; /* Disable all ligatures */
font-variant-numeric: tabular-nums; /* Align numbers */
}Browser Support
font-feature-settings: Universal support (IE 10+, all modern browsers).
font-variant-* properties:
font-variant-ligatures: Chrome 34+, Firefox 34+, Safari 9.1+font-variant-numeric: Chrome 52+, Firefox 34+, Safari 9.1+font-variant-caps: Chrome 52+, Firefox 34+, Safari 9.1+
For older browsers, font-variant-* gracefully degrades to defaults. No polyfill needed.
Debugging OpenType Features
Chrome/Edge DevTools
- Inspect element → Computed tab → Filter for "font-feature"
- Shows active features as
"liga" 1, "tnum" 1 - Scroll to "Rendered Fonts" to verify font file supports features
Wakamai Fondue
Wakamai Fondue is the gold standard for inspecting OpenType features. Drop a font file, see all available features with live previews.
Production Recommendations
- Default setup: Enable common ligatures, disable them in
codeblocks. - Data tables: Always use
font-variant-numeric: tabular-numsfor columns of numbers. - Headings: Consider discretionary ligatures for display typography (test readability).
- Avoid font-feature-settings: Use
font-variant-*properties for better cascade behavior. - Check font support: Not all fonts include all features. Test before deploying.
OpenType features are the difference between mediocre and polished typography. They're invisible when done right—but their absence is glaring to anyone who notices misaligned numbers or ugly ligatures.
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Related Insights
Explore related edge cases and patterns
Advertisement