A practical, copy-paste handbook to debunk visual differences (fonts, sizes, spacing, scaling) right from the browser console.
TL;DR — One-liners you can paste
Select the element in Elements panel (it becomes $0
), then open Console.
1) See what typography is actually applied
(() => {
const cs = getComputedStyle($0);
console.table({
fontFamily: cs.fontFamily,
fontSize: cs.fontSize,
lineHeight: cs.lineHeight,
letterSpacing: cs.letterSpacing,
wordSpacing: cs.wordSpacing,
fontWeight: cs.fontWeight,
fontStyle: cs.fontStyle,
fontStretch: cs.fontStretch, // width % (if variable fonts support it)
fontFeature: cs.fontFeatureSettings, // e.g. 'smcp','tnum'
fontVariantCaps: cs.fontVariantCaps,
fontVariantNumeric: cs.fontVariantNumeric,
fontKerning: cs.fontKerning,
fontSizeAdjust: cs.fontSizeAdjust,
fontOpticalSizing: cs.fontOpticalSizing,
fontVariation: cs.fontVariationSettings, // e.g. "wght" 400, "opsz" 14
textRendering: cs.textRendering
});
})();
2) Is the intended font really loaded? (vs a fallback)
await document.fonts.ready; // wait for font loading
document.fonts.check('16px "Lexend"'); // true = usable
getComputedStyle($0).fontFamily; // should start with "Lexend"
Tip: in Computed → Rendered Fonts (DevTools), you can see the exact font file used.
3) Measure actual text width (catches sneaky fallbacks/axes)
(function measure(el, txt='The quick brown fox 0123456789'){
const cs = getComputedStyle(el);
const s = document.createElement('span');
s.textContent = txt;
s.style.cssText = `
position:fixed;left:-9999px;white-space:nowrap;
font:${cs.fontStyle} ${cs.fontVariant} ${cs.fontWeight} ${cs.fontSize}/${cs.lineHeight} ${cs.fontFamily};
letter-spacing:${cs.letterSpacing};
`;
document.body.appendChild(s);
const w = s.getBoundingClientRect().width; s.remove();
console.log({ text: txt, width_px: w, font: cs.fontFamily, weight: cs.fontWeight, size: cs.fontSize });
})($0);
4) Audit parents for scaling/compositing that changes perceived size
(function audit(el){
const rows=[];
for (let n=el; n; n=n.parentElement) {
const cs = getComputedStyle(n);
let scale = 1;
if (cs.transform && cs.transform !== 'none') {
const m = cs.transform.match(/matrix\(([^)]+)\)/);
if (m) { const a = m[1].split(',').map(parseFloat); scale = Math.hypot(a[0], a[1]); }
}
rows.push({
node: n.tagName + (n.id ? '#'+n.id : ''),
transform: cs.transform, scale,
filter: cs.filter, backdropFilter: cs.backdropFilter,
opacity: cs.opacity, mixBlendMode: cs.mixBlendMode,
willChange: cs.willChange, zoom: cs.zoom || 'normal'
});
}
console.table(rows);
})($0);
5) Root sizing & zoom (affects all rem
and perceived size)
({
rootFontSize: getComputedStyle(document.documentElement).fontSize, // e.g. "16px"
DPR: window.devicePixelRatio, // e.g. 1, 1.25, 2
zoom: (window.visualViewport && visualViewport.scale) || 1 // page zoom
});
6) “Nuke test” — temporarily remove visual effects to see if size perception equalizes
(function(el){
for (let n=el; n; n=n.parentElement) {
n.style.setProperty('transform','none','important');
n.style.setProperty('filter','none','important');
n.style.setProperty('backdrop-filter','none','important');
n.style.setProperty('opacity','1','important');
n.style.setProperty('mix-blend-mode','normal','important');
n.style.setProperty('zoom','normal','important');
}
})($0);
If text suddenly “matches” in size, the culprit is transform/filter/opacity/mix-blend/zoom (not the font).
Why the same font-size
can look different
- Fallback fonts: your intended face didn’t load quickly; a fallback with different x-height renders.
-
Variable fonts: hidden axes like
opsz
(optical size, e.g. Inter) orwdth
/GRAD
change perceived weight/width. -
Synthetic weight:
font-weight
requested but not served → browser synthesizes bold/italics (looks off). -
Tracking/leading: tiny
letter-spacing
(e.g.0.02em
) or differentline-height
reduces perceived size. -
Compositing/AA:
transform
,filter
,opacity
,backdrop-filter
,mix-blend-mode
,will-change
push text to a GPU layer → different antialiasing. -
Zoom / root sizing: page zoom (
visualViewport.scale
), OS scaling (DPR), orhtml{font-size}
changes makerem
content look different.
A repeatable debugging flow (5 minutes)
1) Select the element → run the Typography table (Snippet #1).
- Confirm
fontFamily
(intended family first),fontSize
,fontWeight
,letterSpacing
,lineHeight
.
2) Verify font availability → run font checks (Snippet #2).
- If
false
, fix your font import or CORS; you’re seeing a fallback.
3) Compare actual glyph widths → run measurement (Snippet #3) on both elements.
- Different widths with same
font-size
= not the same font/axes/weight.
4) Hunt compositing → run parent audit (Snippet #4).
- If a parent has
transform/filter/opacity<1/mix-blend
, isolate text into a non-transformed subcontainer.
5) Check root/zoom → run root & zoom (Snippet #5).
- Align zoom to 100% (
Ctrl/Cmd+0
) and normalizehtml{font-size:16px}
if you rely onrem
.
6) (Optional) Nuclear test → Snippet #6 to temporarily remove effects and confirm the culprit.
Compare two parts of the page at once
function inspect(sel){
const el = document.querySelector(sel);
if (!el) return { sel, error:'not found' };
const cs = getComputedStyle(el);
return {
sel,
fontFamily: cs.fontFamily, fontSize: cs.fontSize, lineHeight: cs.lineHeight,
letterSpacing: cs.letterSpacing, fontWeight: cs.fontWeight,
fontStretch: cs.fontStretch, fontFeature: cs.fontFeatureSettings,
fontVariation: cs.fontVariationSettings, fontOpticalSizing: cs.fontOpticalSizing,
transform: cs.transform
};
}
// Example usage:
console.table([inspect('.old p'), inspect('.new p')]);
Quick fixes (drop-in)
Freeze variable-font axes/weight where needed
.scope {
font-weight: 400;
font-synthesis: none; /* no synthetic bold/italic */
letter-spacing: normal;
line-height: 1.5;
/* If you use Inter and see optical-size shifts: */
/* font-optical-sizing: none; */
/* Example of pinning axes if your font supports them: */
/* font-variation-settings: "wght" 400, "wdth" 100; */
}
Normalize root sizing & mobile text inflation
html {
font-size: 16px;
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
Isolate text from visual effects
Apply filters/opacity/transform to a background sibling or ::before, not the text container.
Bonus: one function to rule them all
function auditElement(el = $0) {
if (!el) throw new Error('Select an element in Elements panel so it becomes $0');
const cs = getComputedStyle(el);
const rect = el.getBoundingClientRect();
const result = {
tag: el.tagName + (el.id ? '#'+el.id : ''),
fonts: {
family: cs.fontFamily,
size: cs.fontSize,
weight: cs.fontWeight,
stretch: cs.fontStretch,
lineHeight: cs.lineHeight,
letterSpacing: cs.letterSpacing,
feature: cs.fontFeatureSettings,
variantCaps: cs.fontVariantCaps,
variantNumeric: cs.fontVariantNumeric,
kerning: cs.fontKerning,
sizeAdjust: cs.fontSizeAdjust,
optical: cs.fontOpticalSizing,
variation: cs.fontVariationSettings
},
layout: {
width: rect.width, height: rect.height,
boxSizing: cs.boxSizing,
padding: `${cs.paddingTop} ${cs.paddingRight} ${cs.paddingBottom} ${cs.paddingLeft}`,
margin: `${cs.marginTop} ${cs.marginRight} ${cs.marginBottom} ${cs.marginLeft}`,
border: `${cs.borderTopWidth} ${cs.borderRightWidth} ${cs.borderBottomWidth} ${cs.borderLeftWidth}`,
},
effects: {
transform: cs.transform,
filter: cs.filter,
backdropFilter: cs.backdropFilter,
opacity: cs.opacity,
mixBlendMode: cs.mixBlendMode,
willChange: cs.willChange,
zoom: cs.zoom || 'normal'
},
env: {
rootFontSize: getComputedStyle(document.documentElement).fontSize,
DPR: devicePixelRatio,
zoom: (window.visualViewport && visualViewport.scale) || 1
}
};
console.log(result);
console.table(result.fonts);
console.table(result.effects);
return result;
}
// Use it:
auditElement(); // with $0 selected
Wrap-up
When two blocks look different with the “same” settings, the console proves which of these is to blame:
- Fallback font vs intended one
-
Variable-font axes (
opsz
,wght
,wdth
,GRAD
) - Synthetic bold/italic
- Tracking/leading differences
- Transforms/filters/opacity changing anti-aliasing
- Root sizing / Zoom / DPR
Use the snippets above as a repeatable checklist. Copy, paste, verify, fix. Done ✅
Top comments (0)