EdgeCases Logo
Nov 2025
CSS
Deep
7 min read

OpenType Features: Ligatures, Tabular Numbers, and Small Caps

font-feature-settings doesn't cascade—it replaces. Learn the high-level font-variant-* alternatives

css
fonts
opentype
typography
font-feature-settings
advanced

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

  1. Inspect element → Computed tab → Filter for "font-feature"
  2. Shows active features as "liga" 1, "tnum" 1
  3. 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

  1. Default setup: Enable common ligatures, disable them in code blocks.
  2. Data tables: Always use font-variant-numeric: tabular-nums for columns of numbers.
  3. Headings: Consider discretionary ligatures for display typography (test readability).
  4. Avoid font-feature-settings: Use font-variant-* properties for better cascade behavior.
  5. 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

Related Insights

Explore related edge cases and patterns

CSS
Deep
Font Metrics: Why Text Won't Center in Buttons
7 min
CSS
Surface
Dynamic Fonts in Web Development
5 min
CSS
Surface
Font Synthesis: Avoiding Fake Bold and Italic
6 min

Advertisement