
What Are Ligatures, and Should You Use Them on the Web?
- Sajjad
- Typography
- 01 Oct, 2026
A client sends a screenshot: the word "office" in their new headline looks like it has a strange joined letter in the middle, and a user has complained that searching the page for "fi" doesn't seem to match it. Another team's code blocks suddenly render != as a single slashed-equals symbol after someone switched the monospace font, and half the developers love it while the other half find it confusing. Both situations come down to ligatures, a centuries-old typographic feature that browsers now apply automatically and that most web developers never consciously decide about.
This article explains what ligatures are, the different kinds defined in OpenType, which ones browsers turn on by default, and how to control them with CSS. It also covers the accessibility and usability questions they raise, programming ligatures in code fonts, and a practical policy for when to use each type.
What Is a Ligature?
A ligature is a single glyph that replaces two or more characters when they appear together. The classic examples are "fi" and "fl": in many serif typefaces, the hook of the lowercase f collides with the dot of the i or the ascender of the l. Type designers solve the collision by drawing a combined shape where the f's terminal merges with the next letter.
Ligatures date back to handwriting and metal type, where joining common pairs saved space and avoided awkward collisions. Some became letters in their own right: the ampersand (&) started as a ligature of "e" and "t" (Latin et), and the German ß descends from a long-s and s or z combination.
On the web, ligatures are a font feature, not a character. The underlying text is still "f" followed by "i". The font contains a substitution rule (an OpenType GSUB lookup) that tells the text shaping engine to draw a single combined glyph when it sees that sequence. The post on glyphs vs. characters explains that distinction in detail.
Types of Ligatures
OpenType defines several ligature features, each with a four-letter tag:
| Feature tag | Name | Purpose | Default in browsers |
|---|---|---|---|
liga | Standard ligatures | Fix collisions: fi, fl, ff, ffi, ffl | On |
clig | Contextual ligatures | Ligatures that depend on surrounding letters | On |
rlig | Required ligatures | Mandatory joins in scripts like Arabic | Always on |
dlig | Discretionary ligatures | Decorative joins: ct, st, sp | Off |
hlig | Historical ligatures | Archaic forms such as long-s combinations | Off |
calt | Contextual alternates | Swaps glyphs based on context (used by code fonts) | On |
Standard Ligatures
Standard ligatures exist to solve a functional problem: letters that would otherwise collide. A good standard ligature should be barely noticeable. Readers shouldn't see "a ligature"; they should just see well-set text. Many modern sans-serifs have no f-collision problem and ship few or no standard ligatures.
Discretionary and Historical Ligatures
Discretionary ligatures are decorative. A "ct" or "st" with a swash connecting the letters gives text an old-style or ornate flavor. They're appropriate for a wedding invitation, a book cover, or a display headline, and almost never for body text.
Historical ligatures reproduce archaic forms. Unless you're setting a facsimile of an eighteenth-century document, leave them off.
Required Ligatures
Some writing systems need ligatures to be correct. Arabic letters join and change shape depending on position, and Devanagari forms conjuncts from consonant clusters. These are handled by required ligatures and other shaping features. You should never disable them, and CSS doesn't give you a simple switch to do so for good reason. For scripts like these, see displaying Bengali, Hindi, and other non-Latin scripts.
Programming Ligatures
Code fonts like Fira Code, JetBrains Mono, and Cascadia Code use contextual alternates and ligature features to combine sequences like =>, !==, >=, and -> into single-looking symbols. The underlying characters are unchanged; only the drawing differs. More on these below.
What Browsers Do by Default
All modern browsers enable liga, clig, and calt by default for most text. If the font contains standard ligatures, you get them without writing any CSS.
There's one long-standing exception: browsers typically disable optional ligatures when letter-spacing is set to a non-zero value. The reasoning is that a ligature can't be letter-spaced (it's one glyph), so a tracked "fi" ligature would look inconsistent next to spaced-out letters. The CSS Text specification recommends this behavior, and Chrome, Firefox, and Safari follow it.
How to Control Ligatures in CSS
font-variant-ligatures
The high-level property is font-variant-ligatures. Prefer it over low-level feature tags, because it composes properly with other font settings.
/* Browser default: standard and contextual on */
p {
font-variant-ligatures: normal;
}
/* Turn off all ligatures and contextual alternates */
.no-ligatures {
font-variant-ligatures: none;
}
/* Keep standard ligatures, add decorative ones for a headline */
.fancy-heading {
font-variant-ligatures: common-ligatures discretionary-ligatures;
}
/* Disable only standard ligatures */
.plain-fi {
font-variant-ligatures: no-common-ligatures;
}
/* Disable contextual alternates (common for code fonts) */
code {
font-variant-ligatures: no-contextual;
}
The keywords map to OpenType features like this:
| Keyword | Features controlled |
|---|---|
common-ligatures / no-common-ligatures | liga, clig |
discretionary-ligatures / no-discretionary-ligatures | dlig |
historical-ligatures / no-historical-ligatures | hlig |
contextual / no-contextual | calt |
none | All of the above off |
font-feature-settings
font-feature-settings gives direct access to any OpenType feature tag. It's the fallback for features without a high-level property, but it has a catch: it's a single property, so a later declaration replaces the whole list instead of adding to it.
.headline {
font-feature-settings: "dlig" 1;
}
/* This replaces the rule above entirely, so dlig is lost */
.headline.tabular {
font-feature-settings: "tnum" 1;
}
/* You have to repeat every feature you still want */
.headline.tabular {
font-feature-settings: "dlig" 1, "tnum" 1;
}
That's why font-variant-ligatures is the better choice for ligatures specifically. For a broader treatment of feature tags, see CSS font-feature-settings.
Checking Whether a Font Has Ligatures
A font only applies ligatures it actually contains. To see what features a font supports, you can inspect it with fontTools:
pip install fonttools
python3 -c "
from fontTools.ttLib import TTFont
f = TTFont('SourceSerif4-Regular.woff2')
tags = sorted({fr.FeatureTag for fr in f['GSUB'].table.FeatureList.FeatureRecord})
print(tags)
"
Firefox DevTools also shows a font's features in the Fonts panel of the Inspector, and Wakamai Fondue (wakamaifondue.com) lets you drop a font file into the browser and lists every feature with a live preview.
Watch Out for Subsetting
When you subset a font to reduce its file size, tools can drop the GSUB tables that hold ligatures. pyftsubset keeps a default set of layout features, including liga, clig, and calt, but drops dlig unless you ask for it:
pyftsubset SourceSerif4-Regular.ttf \
--unicodes="U+0000-00FF,U+2013-2014,U+2018-201D" \
--layout-features+=dlig \
--flavor=woff2 \
--output-file=source-serif-4-latin.woff2
If your decorative ligatures disappear after optimization, this is usually the cause.
Ligatures and Accessibility
Because ligatures are a rendering substitution, the text in the DOM is unchanged. Screen readers read "office" as "office", copy and paste returns the separate characters, and the browser's find-in-page matches "fi" across the ligature. In current browsers, standard ligatures are safe from an accessibility perspective.
There are a few edge cases worth knowing:
- PDF and print exports. Some PDF generators, and older PDF readers, map a ligature glyph to a Unicode presentation-form character like U+FB01 (fi) instead of "f" + "i". Copying text out of such a PDF can produce odd characters, and search inside the PDF may fail. If your site generates PDFs, test copy and search on the output.
- Letter-spaced UI text. As noted above, browsers drop optional ligatures when you add tracking. That's the correct behavior, but it means you shouldn't rely on a ligature for visual effect in tracked text.
- Readers with dyslexia or low vision. Unusual joined shapes can slow recognition. Discretionary ligatures in body text add a decoding cost for every reader, and more for those who already struggle. Keep them for display sizes.
- Programming ligatures. A combined
!==glyph can be ambiguous to someone who doesn't know the font, and it can hide the difference between!=and!==at a glance. In tutorials and documentation aimed at learners, disable them.
None of these require WCAG-specific settings, but they fit WCAG's broader readability intent, and they're the kind of detail covered in making website typography accessible.
Programming Ligatures in Code Blocks
Code ligatures are a personal preference in an editor and a publishing decision on a website. In your own VS Code setup, enable them if you like them. On a public documentation site, readers copy code and need to know exactly which characters they're typing.
A sensible default for documentation:
pre,
code,
kbd,
samp {
font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
font-variant-ligatures: none;
}
If you want to offer them as an option, tie it to a class and a user toggle:
.code-ligatures pre,
.code-ligatures code {
font-variant-ligatures: contextual common-ligatures;
}
const toggle = document.querySelector<HTMLInputElement>("#ligature-toggle");
toggle?.addEventListener("change", () => {
document.documentElement.classList.toggle("code-ligatures", toggle.checked);
try {
localStorage.setItem("code-ligatures", String(toggle.checked));
} catch {
// storage unavailable; preference lasts for this page view only
}
});
For a Next.js site using next/font, you can load JetBrains Mono and apply the same rule to the variable:
import { JetBrains_Mono } from "next/font/google";
export const mono = JetBrains_Mono({
subsets: ["latin"],
display: "swap",
variable: "--font-mono",
});
Then reference var(--font-mono) in your code-block styles with font-variant-ligatures: none.
A Practical Ligature Policy
If you want a default that works for almost every site:
- Body text: leave standard ligatures on (
normal). They fix collisions and are invisible when the font is well made. - Headlines: consider discretionary ligatures only for brand or editorial display text, and only after reviewing every headline that will use them.
- Tracked text (all-caps labels, small caps): don't fight the browser's default of dropping ligatures.
- Code blocks: turn ligatures off for published code; offer a toggle if your audience wants them.
- Non-Latin scripts: never disable ligatures or shaping features globally. Scope any
font-variant-ligatures: nonerule to code or specific Latin elements.
Avoid global resets like * { font-variant-ligatures: none; }. They look harmless in English and break nothing visibly, until someone adds an Arabic or Hindi translation and contextual shaping features stop working where the reset applies.
Ligatures FAQ
No. Ligatures are applied at render time by the browser, and the HTML still contains the separate characters. Search engines index the text exactly as it appears in your markup, so a word set with an fi ligature is indexed normally.
Browsers intentionally turn off optional ligatures when letter-spacing is not zero, because a single ligature glyph cannot be spaced like the letters around it. This follows the CSS Text specification. If you need tracking, accept that ligatures will drop out.
No. Those presentation-form characters exist for compatibility with old encodings. Using them hurts search, copy and paste, and screen reader output. Type the normal letters and let the font apply the ligature.
They are not bad, but they are a preference. In your own editor they can make code easier to scan. On public documentation they can confuse readers about which characters to type, so most documentation sites turn them off or make them optional.
For ligatures, yes. The font-variant-ligatures property is a high-level control that combines cleanly with other font-variant properties, while font-feature-settings replaces its entire value whenever it is redeclared, which makes it easy to lose features by accident.
Either the font does not include ligature substitutions, or they were removed during subsetting or conversion. Inspect the font with Firefox DevTools or fontTools to check for liga and dlig features, and review your subsetting options.
Conclusion
Ligatures are substitution rules inside a font that replace character sequences with single combined glyphs. Standard ligatures fix collisions like f followed by i and are on by default in every modern browser. Discretionary and historical ligatures are decorative and off by default. Required ligatures are part of correct shaping for many scripts and should never be touched.
For most websites, the right approach is to leave the browser defaults alone in body text, add discretionary ligatures only deliberately for display type, turn programming ligatures off in published code, and use font-variant-ligatures rather than raw feature tags. Test copy, search, and any generated PDFs, and avoid global resets that could break other scripts.
Here are some useful references for going deeper on ligatures:
- MDN Web Docs: font-variant-ligatures — every keyword, with browser support details.
- MDN Web Docs: OpenType font features guide — how ligatures fit into the wider set of OpenType features.
- W3C: CSS Fonts Module Level 4 — the specification for
font-variant-ligaturesandfont-feature-settings. - Microsoft Typography: OpenType Layout tag registry: features — official definitions of
liga,dlig,calt, and the rest. - Butterick's Practical Typography: Ligatures — a practical opinion on when ligatures help and when they don't.


