How to kerning storage formats: kern vs gpos vs class-based
- Step 1Recognise the legacy kern table — A flat list of `(left glyph index, right glyph index, value)` triplets. Simple to parse, grows linearly with pair count, and is what opentype.js exposes as `font.kerningPairs` — so it's the only encoding the Kerning Pair Auditor enumerates. Common in pre-2012 fonts; many modern fonts drop it entirely.
- Step 2Recognise GPOS pair-pos (the modern equivalent of kern) — GPOS LookupType 2, format 1 stores explicit pairs the same way the kern table does, but inside the OpenType layout engine so it can be gated behind the `kern` feature and interact with other features. Browsers read it. opentype.js parses the GPOS table but does NOT feed these pairs into `font.kerningPairs`, so the auditor can't list them.
- Step 3Recognise GPOS class-based kerning — GPOS LookupType 2, format 2 groups glyphs into classes ('caps with a diagonal right stroke', 'round lowercase') and defines kerning between class pairs. A few dozen class pairs can express what would be thousands of explicit pairs. This is the dominant modern encoding — and the hardest to enumerate, because you must walk the class definitions to expand it.
- Step 4Understand browser precedence — If a font has both a `kern` table and GPOS kerning, browsers use GPOS (richer, feature-aware) and ignore the legacy table. The CSS `kern` feature controls both: `font-feature-settings: 'kern' 0` (or `font-kerning: none`) turns off kerning regardless of which table it lives in.
- Step 5Pick the right inspection tool per encoding — For the legacy `kern` table, use the [Kerning Pair Auditor](/font-tools/kerning-pair-auditor). To confirm a GPOS `kern` feature exists at all, use [opentype-features-inspector](/font-tools/opentype-features-inspector). To expand GPOS class kerning into individual pairs, you need desktop fontTools (`ttx` dump of GPOS) — no browser tool does this.
- Step 6Interpret an empty audit correctly — Zero pairs in the auditor + a `kern` feature present in the features inspector = the font kerns, just in GPOS. Zero pairs + no `kern` feature anywhere = the font genuinely has no kerning. Don't conclude 'no kerning' from the auditor alone.
The three kerning encodings compared
How each encoding stores data, its size profile, and whether each consumer reads it. 'Auditor' is the JAD Kerning Pair Auditor specifically.
| Encoding | How it stores pairs | Browser reads? | Auditor reads? |
|---|---|---|---|
Legacy kern table, format 0 | Flat list of (left index, right index, value) triplets | Yes (if no GPOS kern present) | Yes — this is the only encoding it enumerates |
| GPOS pair-pos, format 1 | Explicit pairs inside a GPOS LookupType 2, gated by the kern feature | Yes (preferred) | No — opentype.js parses GPOS but doesn't feed it to font.kerningPairs |
| GPOS class-based, format 2 | Glyph classes + a 2D matrix of class-pair values | Yes (preferred) | No — requires class expansion, which opentype.js doesn't perform for kerning enumeration |
kern table, format 2 (class-based) | Class array inside the legacy table | Yes (legacy) | No — opentype.js's kern parser reads only the simple pair list |
Size and expressiveness tradeoffs
Why foundries moved from explicit pairs to class-based GPOS. Numbers are illustrative orders of magnitude, not measured from a single font.
| Encoding | Typical entry count for full Latin | File-size impact | Enumeration difficulty |
|---|---|---|---|
Legacy kern (explicit pairs) | Hundreds to a few thousand explicit pairs | Grows linearly — every pair is one entry | Trivial — read the list (this is what the auditor does) |
| GPOS pair-pos (explicit) | Same pair count, in GPOS | Similar to kern table, plus GPOS overhead | Moderate — walk GPOS lookups (auditor doesn't) |
| GPOS class-based | ~20–60 class pairs expressing thousands of glyph pairs | Much smaller for large pair sets | Hard — expand classes to glyph pairs (no browser tool does this) |
| CJK / complex scripts | Tens of thousands of effective pairs | Only class-based makes this feasible | Hardest — practically requires desktop fontTools |
Cookbook
Concrete scenarios that map a font's storage choice to what you'll see in the auditor. Run the Kerning Pair Auditor and opentype-features-inspector side by side to read these correctly.
Legacy kern table — auditor sees everything
ExampleAn older TrueType font with a real kern table and no GPOS kerning. The auditor enumerates the full pair list. This is the happy path the tool was built for.
Auditor: total_kerning_pairs 842, pairs listed Features inspector: no 'kern' feature in GPOS (or no GPOS at all) Conclusion: kerning is in the legacy table; the auditor's list is the complete picture.
GPOS pair-pos only — auditor empty, font kerns
ExampleA modern font that dropped the kern table and stores explicit pairs in GPOS. The auditor returns zero because opentype.js doesn't feed GPOS into font.kerningPairs.
Auditor: total_kerning_pairs 0, pairs [] Features inspector: lists a 'kern' feature (GPOS) Conclusion: the font kerns fine in browsers; the legacy-table auditor just can't see GPOS pairs. NOT a missing-kerning case.
GPOS class-based — the common modern case
ExampleA retail or variable font using class-based GPOS. Thousands of effective pairs compressed into a few dozen class pairs. No browser tool expands these into a list.
Auditor: total_kerning_pairs 0, pairs [] Features inspector: lists a 'kern' feature (GPOS) Desktop ttx GPOS: LookupType 2, format 2 — class definitions To list individual pairs you must expand classes with fontTools: ttx -t GPOS Font.otf # then read the PairPosFormat2 matrix
Both tables present — browser uses GPOS
ExampleSome fonts ship a kern table for legacy compatibility AND GPOS kerning. Browsers use GPOS; the legacy table is dead weight they ignore. The auditor reads the legacy table, so its list may differ from what the browser applies.
Auditor: total_kerning_pairs 120 (the legacy table) Features inspector: lists a 'kern' feature (GPOS, ~2000 pairs) Browser: applies GPOS, ignores the 120-pair kern table The auditor's 120 pairs are real but are NOT what the browser renders — the browser uses the richer GPOS set.
Disabling kerning in CSS affects both tables
ExampleThe CSS kern feature toggles every kerning source at once. Useful to confirm visually that kerning is active before you trust an audit.
/* Turn kerning OFF — disables kern table AND GPOS kern */
.no-kern { font-feature-settings: 'kern' 0; }
/* or the high-level form */
.no-kern { font-kerning: none; }
/* Turn it back ON (default for most browsers above ~20px) */
.kern { font-kerning: normal; }Edge cases and what actually happens
Every row below was probed against the live API. Some documented requirements (alphabetical axis order, numerical tuple order) are not actually enforced in practice — useful to know if you've been blaming the wrong thing for a 400.
Empty audit on a font that clearly kerns
Storage mismatch, not a bugThe font stores kerning in GPOS; the auditor reads the legacy kern table. Both are 'correct' — they just look in different tables. Cross-check with opentype-features-inspector: a kern feature present in GPOS means the font kerns and the empty audit is expected.
Class-based GPOS can't be listed in any browser tool
Tooling limitationExpanding class-based GPOS into individual glyph pairs requires walking the class definitions and the class-pair matrix. opentype.js doesn't do this for kerning enumeration, and no JAD browser tool does either. The only reliable route is desktop fontTools (ttx -t GPOS) and reading the PairPosFormat2 structure manually.
Both kern and GPOS present — which is 'the' kerning?
GPOS wins in browsersWhen both exist, browsers apply GPOS and ignore the legacy table. So the auditor's list (from the legacy table) can differ from what users actually see. Treat the legacy-table audit as a compatibility artefact, not the rendered truth, whenever a GPOS kern feature is also present.
Format-2 class kerning inside the legacy kern table
Not enumeratedThe kern table itself has a format-2 (class-based) variant, separate from GPOS. opentype.js's kern parser reads only the simple pair list, so even legacy class kerning is invisible to the auditor. Rare today, but it means 'kern table present' doesn't guarantee a non-empty audit.
CJK fonts and complex scripts
Expected zero (class-based)Han, Hangul, and other large scripts have tens of thousands of effective pairs that are only practical as class-based GPOS. These near-always audit as zero here. The auditor isn't the right tool for CJK kerning review — desktop fontTools is.
Variable fonts
Usually emptyVariable fonts overwhelmingly use GPOS for kerning (and may vary it across the design space). The auditor reads the default instance's legacy kern table, which is usually absent — hence zero pairs. Freezing with variable-font-freezer preserves GPOS, not a legacy kern table, so the audit typically stays empty after freezing too.
Disabling the kern feature in CSS removes all kerning
Expectedfont-feature-settings: 'kern' 0 or font-kerning: none disables both the legacy kern table and GPOS kerning. There's no way to disable one and keep the other from CSS — the kern feature flag is the single switch for the whole kerning system.
Browsers auto-disable kerning at very small sizes
ExpectedHistorically some browsers disabled kerning below ~20px for performance; modern engines mostly keep it on but the effect is invisible at body sizes anyway. This is rendering behaviour, unrelated to which table the font uses — the storage encoding is the same whether or not the browser applies it at a given size.
Frequently asked questions
Why does the auditor say zero pairs when my font obviously kerns?
Because the font stores kerning in GPOS and the auditor reads only the legacy kern table. opentype.js fills font.kerningPairs from the kern table; GPOS goes into a separate structure the auditor doesn't enumerate. Confirm with opentype-features-inspector — if it lists a kern feature, the font kerns and the empty audit is expected.
What's the difference between the kern table and GPOS kerning?
The kern table is the original, pre-OpenType-layout way to store kerning: a flat list of explicit pairs. GPOS is the modern layout engine; its LookupType 2 stores kerning either as explicit pairs (format 1) or as class pairs (format 2). GPOS is feature-aware and interacts with the rest of OpenType shaping; the kern table is a dumb list. Browsers prefer GPOS when both exist.
What is class-based kerning?
Instead of listing every glyph pair, class-based kerning groups glyphs into classes — say, all caps with a diagonal right side (A, V, W, Y) — and defines kerning between class pairs. One class-pair entry can express dozens of glyph pairs, so a font with thousands of effective pairs needs only a few dozen entries. It's compact and dominant in modern fonts, but you must expand the classes to enumerate individual pairs.
Which encoding do most fonts use today?
GPOS, and usually class-based (format 2). Retail and open-source fonts cut after roughly 2012, and nearly all variable fonts, store kerning in GPOS and often drop the legacy kern table entirely. Pre-2012 fonts and many system fonts still carry a kern table — those are the ones the auditor enumerates fully.
Do browsers prefer one encoding?
Yes — when both a kern table and GPOS kerning are present, browsers apply GPOS and ignore the legacy table. GPOS is richer and feature-aware. This is why the auditor's legacy-table list can differ from what a user sees if the font also has GPOS kerning.
Can I see the actual pairs a GPOS class-based font applies?
Not in any browser tool. Expanding class-based GPOS into individual pairs means walking the class definitions and the class-pair matrix — desktop fontTools (ttx -t GPOS) is the practical route. The JAD opentype-features-inspector can confirm a kern feature exists, but it won't list the expanded pairs.
Does the Kerning Pair Auditor read class-based kerning at all?
No. It reads the legacy kern table's simple pair list via opentype.js. It does not expand GPOS class kerning, GPOS pair-pos, or even the format-2 class variant inside the legacy kern table. If your font uses any of those, the audit will undercount or show zero.
How do I turn kerning off to compare visually?
Use font-feature-settings: 'kern' 0 or the high-level font-kerning: none in CSS. This disables both the legacy kern table and GPOS kerning at once — there's no CSS switch that toggles just one. Set it on a test element, eyeball the AV/To pairs with and without, and you'll see whether the font's kerning is doing real work.
Why did foundries move away from explicit pairs?
File size and maintenance. A fully kerned Latin font can have thousands of explicit pairs; class-based GPOS expresses the same spacing logic in a few dozen class pairs, which is smaller on the wire and easier to edit (change a class, affect all its members). For large scripts like CJK, class-based is the only practical approach.
If a font has both tables, which list should I trust?
Trust GPOS for what the browser renders. The auditor's legacy-table list is real data but browsers ignore it when GPOS kerning is present. So a 120-pair audit on a font whose GPOS holds 2,000 pairs tells you about a compatibility table, not the rendered result. Check the features inspector to know whether GPOS kerning exists.
Is GPOS kerning slower than the kern table?
Not meaningfully for the end user. GPOS shaping is more general, but browser text-shaping engines (HarfBuzz in Chrome/Firefox) handle it efficiently. The performance difference is irrelevant to page load; the real costs are font file size and whether the engine applies kerning at a given size, not which table holds the data.
What should my QA workflow be for kerning?
Run opentype-features-inspector first to learn where kerning lives. If it's in the legacy table, the Kerning Pair Auditor gives you the full pair list. If it's in GPOS, accept that browser tools can't enumerate it and do a visual review of critical pairs at display size, or dump GPOS with desktop fontTools. Pair with glyph-inspector and font-metrics-analyzer for the rest of the spacing picture.
Privacy first
Every JAD Font tool runs entirely in your browser using opentype.js and the wawoff2 WASM Brotli encoder. Your fonts never leave your device — verified by zero outbound network requests during processing.