How to Calculate Cap-Height for Web Typography: Metrics, CSS Math & Layout Stability

Root cause: Browsers calculate line-box height using default font metrics, ignoring actual uppercase glyph bounds. This mismatch causes unpredictable vertical spacing and Cumulative Layout Shift (CLS).

Diagnostic: Open DevTools > Computed tab. Compare the computed line-height against the rendered cap-height using the Elements panel's box model overlay.

Fix: Extract sCapHeight from the OS/2 table, normalize via unitsPerEm, and apply the native 1cap unit or a calculated multiplier. Align this workflow with Typography Fundamentals & System Architecture standards to eliminate CLS and stabilize responsive layouts.

Extract Raw Cap-Height from Font Tables

Parse the OS/2 table for the sCapHeight field. If absent, fall back to yMax in the hhea table.

Normalize the raw integer value: capHeight = rawValue / unitsPerEm. This yields a unitless ratio relative to the font's design grid.

Cross-reference baseline offsets using Font Metrics & Baseline Alignment to prevent vertical drift when mixing typefaces.

Validate extraction via fontkit CLI or opentype.js inspection before committing values to your CSS variables.

import { loadSync } from 'fontkit';
const font = loadSync('./font.woff2');
const unitsPerEm = font.unitsPerEm;
const rawCap = font.tables['OS/2'].sCapHeight || font.tables.hhea.yMax;
const capRatio = rawCap / unitsPerEm;
console.log(`Cap-height ratio: ${capRatio.toFixed(4)}`);

Extracts raw cap-height from OS/2 or hhea tables and normalizes against unitsPerEm for CSS multiplier generation.

CSS Implementation & Cap Unit Fallbacks

Apply the native 1cap unit where supported for precise uppercase alignment: line-height: 1.4cap;.

Calculate a legacy multiplier for unsupported browsers: --cap-ratio: 0.72; line-height: calc(1.4 * var(--cap-ratio) * 1em);.

Audit with DevTools > Rendering > Layout Shift Regions. Enable the "Layout Shift Regions" toggle to verify zero displacement during font swap events.

Use @supports (line-height: 1cap) for progressive enhancement, ensuring graceful degradation without layout jumps.

:root {
 --cap-ratio: 0.718;
 --base-line-height: 1.5;
}

@supports (line-height: 1cap) {
 body { line-height: var(--base-line-height)cap; }
}

@supports not (line-height: 1cap) {
 body { line-height: calc(var(--base-line-height) * var(--cap-ratio) * 1em); }
}

Progressive enhancement pattern using native cap unit with calculated fallback multiplier.

Vertical Rhythm & Type Scale Integration

Map the calculated cap-height to modular grid steps: grid-row-gap: calc(var(--cap-height) * 1.5);. This locks spacing to typographic anatomy rather than arbitrary pixel values.

Apply clamp() for fluid scaling across breakpoints while maintaining proportional rhythm.

Sync the opsz variable axis to maintain proportional cap-height at smaller sizes, preventing optical distortion in dense UI text.

Run Lighthouse > Best Practices > Text legibility audit to confirm contrast and spacing compliance after implementing fixed line-box heights.

@font-face {
 font-family: 'Fallback';
 src: local('Arial');
 size-adjust: 102%;
 ascent-override: 98%;
 descent-override: 22%;
 line-gap-override: 0%;
}

Overrides fallback font metrics to match primary font cap-height and prevent CLS during font loading.

Common Pitfalls

  • Assuming font-size equals cap-height; actual glyph bounds vary significantly by typeface design and optical sizing.
  • Ignoring unitsPerEm discrepancies between font families, causing inconsistent vertical spacing across mixed-typeface components.
  • Using viewport units (vh/vw) for cap-height calculations, which breaks accessibility zoom and print layouts.
  • Overriding line-height without recalculating baseline alignment, triggering text clipping in Safari's WebKit renderer.
  • Skipping size-adjust in fallback stacks, resulting in measurable layout shifts during asynchronous font swap events.

FAQ

Q: How do I calculate cap-height when the font lacks an sCapHeight table entry? A: Fallback to yMax from the hhea table, or measure the bounding box of a dominant uppercase glyph (e.g., 'H') via the Canvas API measureText() and normalize against unitsPerEm.

Q: Does the CSS 1cap unit work reliably across modern browsers? A: Supported in Chromium 117+, Safari 16.4+, and Firefox 120+. Implement @supports feature queries with a calculated em multiplier fallback for legacy environments.

Q: How does incorrect cap-height calculation trigger Cumulative Layout Shift (CLS)? A: Mismatched cap-height between primary and fallback fonts alters the computed line-box height. When the primary font loads, the browser recalculates line spacing, shifting subsequent DOM nodes. Lock spacing with explicit size-adjust or fixed line-height overrides.