[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-font-loading-api-font-display-cls":44,"search-suggestions":60,"quiz-article-font-loading-api-font-display-cls":108,"path-membership-font-loading-api-font-display-cls":302},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a1749f-2402-739b-8914-8e4cced13328","font-loading-api-font-display-cls","PRACTICE_QUIZ","font-display and the layout shift it leaves behind","Checks the real takeaways: what font-display actually controls, why swap can still cause a Cumulative Layout Shift, and the metric-matching descriptors that fix it.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},7,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,100,104],{"slug":62,"name":63,"articles":64},"webdev","Webdev",158,{"slug":66,"name":67,"articles":68},"javascript","Javascript",129,{"slug":70,"name":71,"articles":72},"frontend","Frontend",89,{"slug":74,"name":75,"articles":76},"tutorial","Tutorial",61,{"slug":78,"name":79,"articles":80},"css","Css",47,{"slug":82,"name":83,"articles":84},"performance","Performance",27,{"slug":86,"name":87,"articles":88},"typescript","Typescript",22,{"slug":90,"name":91,"articles":92},"react","React",19,{"slug":94,"name":95,"articles":96},"programming","Programming",15,{"slug":98,"name":99,"articles":96},"browser","Browser",{"slug":101,"name":102,"articles":103},"node","Node",13,{"slug":105,"name":106,"articles":107},"html","Html",11,{"id":109,"slug":46,"title":110,"subtitle":52,"excerpt":111,"coverUrl":112,"locale":13,"readingMinutes":113,"publishedAt":114,"viewCount":115,"likeCount":19,"commentCount":19,"author":116,"vertical":121,"topic":122,"tags":124,"_count":131,"playground":133,"body":135,"bodyMd":288,"seo":289,"translationGroupId":292,"series":52,"podcastUrl":52,"verticalId":5,"thread":293,"assessments":295,"translations":298,"quiz":300},"01a1749f-22b7-73c8-9c8c-4074d47d2832","font-display: swap Fixes Invisible Text. It Also Shifts Your Layout.","Setting font-display: swap kills the flash of invisible text, so most teams call the font-loading problem solved and move on. It doesn't — it just trades FOIT for a layout shift Lighthouse will happily dock you for. Here's the actual fix.","\u002Fmedia\u002Fcovers\u002Ffont-loading-api-font-display-cls.png",6,"2026-10-29T06:10:25.373Z",55,{"id":117,"name":118,"username":119,"avatarUrl":52,"headline":120},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":78,"name":123},"CSS",[125,126,129,130],{"slug":78,"name":79,"color":52},{"slug":127,"name":128,"color":52},"webperf","Webperf",{"slug":62,"name":63,"color":52},{"slug":82,"name":83,"color":52},{"assessments":132},1,{"slug":46,"title":134},"font-display: swap — the layout shift it leaves behind",{"blocks":136,"version":132},[137,141,144,147,152,155,160,163,167,170,179,182,185,188,191,194,197,200,203,206,209,212,215,219,222,225,228,233,237,240,246,249,252,255,258,261,264,267,270,273,276,279,282],{"id":138,"html":139,"type":140},"b1","\u003Cp>The ticket said &quot;homepage CLS regressed after the rebrand.&quot; Nobody had touched the layout. Nobody had touched the images. The only diff in that deploy was a font swap — the body copy went from a system font to a licensed one, loaded from \u003Ccode>@font-face\u003C\u002Fcode>.\u003C\u002Fp>","paragraph",{"id":142,"html":143,"type":140},"b2","\u003Cp>Lighthouse didn&#39;t care that the new font looked better. It saw text that painted in one spot, then jumped, and it scored that jump like it scores a banner ad shoving the article down the page.\u003C\u002Fp>",{"id":145,"html":146,"type":140},"b3","\u003Cp>The fix everyone reaches for here is \u003Ccode>font-display: swap\u003C\u002Fcode>. It was already set. That was the problem.\u003C\u002Fp>",{"id":148,"html":149,"text":150,"type":151,"level":31},"b4","The belief: &quot;swap&quot; is the whole fix","The belief: \"swap\" is the whole fix","heading",{"id":153,"html":154,"type":140},"b5","\u003Cp>Here&#39;s the belief, and it&#39;s not wrong, exactly: web fonts load over the network, and by default most browsers render \u003Cem>nothing\u003C\u002Fem> until the font arrives — the flash of invisible text, FOIT. You add \u003Ccode>font-display: swap\u003C\u002Fcode> to your \u003Ccode>@font-face\u003C\u002Fcode> rule, the browser paints your fallback font immediately, and swaps in the real font the moment it&#39;s ready. No more invisible text. Problem solved. Ship it.\u003C\u002Fp>",{"id":156,"code":157,"type":158,"language":78,"highlight":159},"b6","@font-face {\n  font-family: \"Brand Sans\";\n  src: url(\"\u002Ffonts\u002Fbrand-sans.woff2\") format(\"woff2\");\n  font-display: swap;\n}","code",[],{"id":161,"html":162,"type":140},"b7","\u003Cp>That line is in a lot of style guides as \u003Cem>the\u003C\u002Fem> answer to font loading. It&#39;s treated as a toggle: FOIT off, done.\u003C\u002Fp>",{"id":164,"html":165,"text":166,"type":151,"level":31},"b8","The crack: swap doesn&#39;t hide the font load. It just moves where you feel it.","The crack: swap doesn't hide the font load. It just moves where you feel it.",{"id":168,"html":169,"type":140},"b9","\u003Cp>\u003Ccode>font-display\u003C\u002Fcode> doesn&#39;t control \u003Cem>whether\u003C\u002Fem> the browser substitutes a fallback font — every strategy except \u003Ccode>block\u003C\u002Fcode> does that. What it controls is the \u003Cstrong>timeline\u003C\u002Fstrong>: how long the browser waits invisibly, and how long it&#39;s willing to keep showing the fallback before giving up on the real font for that paint. Per the spec, each value defines a block period and a swap period:\u003C\u002Fp>",{"id":171,"type":172,"items":173,"ordered":18},"b10","list",[174,175,176,177,178],"\u003Ccode>auto\u003C\u002Fcode> — browser&#39;s own default, usually close to \u003Ccode>block\u003C\u002Fcode>.","\u003Ccode>block\u003C\u002Fcode> — invisible for ~3s, then swap (classic FOIT).","\u003Ccode>swap\u003C\u002Fcode> — essentially no invisible period; show the fallback instantly, swap in whenever the real font lands (FOUT instead).","\u003Ccode>fallback\u003C\u002Fcode> — a brief invisible flash, a short swap window, then it commits to the fallback if the font&#39;s still not here.","\u003Ccode>optional\u003C\u002Fcode> — barely waits at all, and won&#39;t swap mid-visit on a slow connection; the real font may only show up on the \u003Cem>next\u003C\u002Fem> navigation, once it&#39;s cached.",{"id":180,"html":181,"type":140},"b11","\u003Cp>Notice what none of those values promise: that the fallback font and the real font occupy the same pixels. \u003Ccode>swap\u003C\u002Fcode> guarantees a render happens fast. It says nothing about whether that render holds still when the second one replaces it.\u003C\u002Fp>",{"id":183,"html":184,"type":140},"b12","\u003Cp>Fonts aren&#39;t interchangeable rectangles. Two fonts at the same \u003Ccode>font-size\u003C\u002Fcode> can have wildly different x-heights, character widths, and line heights — a serif webfont next to \u003Ccode>Arial\u003C\u002Fcode> as a fallback is a common offender. When the swap happens, every line that used the fallback re-wraps or re-measures against the new metrics. Paragraphs reflow. Buttons that sat right under the headline jump down eight pixels. If that happens after the page has already settled — which, on a flaky connection, it will — it&#39;s an \u003Cem>unexpected\u003C\u002Fem> shift, and Cumulative Layout Shift counts every one of those against you.\u003C\u002Fp>",{"id":186,"html":187,"type":140},"b13","\u003C!-- playground:start -->",{"id":189,"html":190,"text":190,"type":151,"level":31},"b14","🎮 Try it yourself",{"id":192,"html":193,"type":140},"b15","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ffont-loading-api-font-display-cls\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":195,"html":196,"type":140},"b16","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":198,"html":199,"type":140},"b17","\u003C!-- playground:end -->",{"id":201,"html":202,"type":140},"b18","\u003Cp>So you didn&#39;t fix the font-loading problem. You moved it from &quot;the user sees nothing&quot; (bad, but at least static) to &quot;the user sees something, then watches it move&quot; (arguably worse, and it&#39;s the one Lighthouse actively measures).\u003C\u002Fp>",{"id":204,"html":205,"text":205,"type":151,"level":31},"b19","Why the belief persists",{"id":207,"html":208,"type":140},"b20","\u003Cp>Because \u003Ccode>swap\u003C\u002Fcode> usually \u003Cem>looks\u003C\u002Fem> fine in a quick check. If your fallback stack happens to be metrically close to your webfont — two similar sans-serifs, say — the shift is a pixel or two and nobody notices scrolling past it. It only becomes visible, and scoreable, when the fonts diverge more: a wide geometric sans swapped in over \u003Ccode>Helvetica\u003C\u002Fcode>, or a serif swapped in over \u003Ccode>system-ui\u003C\u002Fcode>. And most font-loading guides stop at &quot;FOIT vs FOUT,&quot; because that&#39;s the part you can see with your eyes. CLS is the part a profiler tells you about, days later, in a Core Web Vitals report nobody opens until SEO flags it.\u003C\u002Fp>",{"id":210,"html":211,"text":211,"type":151,"level":31},"b21","The better mental model: make the fallback lie convincingly",{"id":213,"html":214,"type":140},"b22","\u003Cp>The fix isn&#39;t picking a different \u003Ccode>font-display\u003C\u002Fcode> value — \u003Ccode>swap\u003C\u002Fcode> was the right call. The fix is making the fallback font&#39;s box match the real font&#39;s box closely enough that swapping it out doesn&#39;t move anything. CSS Fonts Level 4 gives you the descriptors to do exactly that, declared on a \u003Cstrong>second\u003C\u002Fstrong> \u003Ccode>@font-face\u003C\u002Fcode> for your fallback, under its own name:\u003C\u002Fp>",{"id":216,"code":217,"type":158,"language":78,"highlight":218},"b23","@font-face {\n  font-family: \"Brand Sans Fallback\";\n  src: local(\"Arial\");\n  size-adjust: 104%;\n  ascent-override: 92%;\n  descent-override: 24%;\n  line-gap-override: 0%;\n}\n\nbody {\n  font-family: \"Brand Sans\", \"Brand Sans Fallback\", sans-serif;\n}",[],{"id":220,"html":221,"type":140},"b24","\u003Cp>\u003Ccode>size-adjust\u003C\u002Fcode> scales the fallback font&#39;s whole metrics uniformly; \u003Ccode>ascent-override\u003C\u002Fcode>, \u003Ccode>descent-override\u003C\u002Fcode>, and \u003Ccode>line-gap-override\u003C\u002Fcode> tune the vertical metrics that drive line-box height independently. Get them close to the webfont&#39;s own metrics, and the fallback occupies almost exactly the same footprint — so when the real font swaps in, nothing reflows. The text might still change shape slightly, but the \u003Cem>layout\u003C\u002Fem> holds still, which is what CLS actually measures.\u003C\u002Fp>",{"id":223,"html":224,"type":140},"b25","\u003Cp>Nobody tunes these four numbers by hand from a font&#39;s metadata — that&#39;s what tools like Capsize or the automatic fallback generation in \u003Ccode>next\u002Ffont\u003C\u002Fcode> exist for. The point isn&#39;t to memorize the math; it&#39;s to know this is a solved, declarative problem, not a &quot;pick a better \u003Ccode>font-display\u003C\u002Fcode> value&quot; problem.\u003C\u002Fp>",{"id":226,"html":227,"type":140},"b26","\u003Cp>Two more pieces worth knowing once you&#39;re here:\u003C\u002Fp>",{"id":229,"type":172,"items":230,"ordered":18},"b27",[231,232],"\u003Cstrong>\u003Ccode>document.fonts.ready\u003C\u002Fcode>\u003C\u002Fstrong> is a promise that resolves once the document&#39;s font set has finished loading — useful when you&#39;re doing something that depends on final text metrics, like measuring a canvas-rendered label, and need to know the swap has already happened.","\u003Cstrong>\u003Ccode>font-display: optional\u003C\u002Fcode>\u003C\u002Fstrong> is the right call, not a worse one, for anything where showing \u003Cem>a\u003C\u002Fem> font instantly matters more than showing \u003Cem>the\u003C\u002Fem> font eventually — a above-the-fold hero on a page where first paint speed outranks brand fidelity. It trades &quot;might never show the webfont this visit&quot; for &quot;will never cause a mid-visit shift.&quot;",{"id":234,"html":235,"text":236,"type":151,"level":31},"b28","When plain \u003Ccode>swap\u003C\u002Fcode> is still fine","When plain swap is still fine",{"id":238,"html":239,"type":140},"b29","\u003Cp>Don&#39;t go tune override descriptors on every \u003Ccode>@font-face\u003C\u002Fcode> in the project. It&#39;s unnecessary when:\u003C\u002Fp>",{"id":241,"type":172,"items":242,"ordered":18},"b30",[243,244,245],"Your fallback and webfont are metrically close already (most humanist sans-serif webfonts over \u003Ccode>system-ui\u003C\u002Fcode> shift by a pixel or less).","The text is small and short — a button label has little room to reflow even with a metrics mismatch.","The font is almost always cached before paint (self-hosted, preloaded, same-origin, repeat visitor) — there&#39;s rarely a visible swap to cause a shift in the first place.",{"id":247,"html":248,"type":140},"b31","\u003Cp>Reach for the fallback-matching technique where the shift is visible and the element is large: headlines, hero copy, anything above the fold that Lighthouse measures in its first few seconds.\u003C\u002Fp>",{"id":250,"html":251,"type":140},"b32","\u003C!-- quiz:start -->",{"id":253,"html":254,"text":254,"type":151,"level":31},"b33","🧠 Test yourself",{"id":256,"html":257,"type":140},"b34","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ffont-loading-api-font-display-cls\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":259,"html":260,"type":140},"b35","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":262,"html":263,"type":140},"b36","\u003C!-- quiz:end -->",{"id":265,"html":266,"text":266,"type":151,"level":31},"b37","The lesson",{"id":268,"html":269,"type":140},"b38","\u003Cp>\u003Ccode>font-display: swap\u003C\u002Fcode> answers &quot;does the user see text immediately?&quot; It was never an answer to &quot;does the page stay still when the real font shows up?&quot; — that&#39;s a layout question, and layout questions get layout answers: matched metrics, not a different loading strategy.\u003C\u002Fp>",{"id":271,"html":272,"type":140},"b39","\u003Cp>Next time CLS regresses right after a font swap and nobody touched the CSS, you&#39;ll know exactly where to look. What&#39;s the worst &quot;nobody touched the layout&quot; regression you&#39;ve chased down to a font? I&#39;ll start in the comments.\u003C\u002Fp>",{"id":274,"type":275},"b40","divider",{"id":277,"html":278,"type":140},"b41","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":280,"html":281,"type":140},"b42","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":283,"type":172,"items":284,"ordered":18},"b43",[285,286,287],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","The ticket said \"homepage CLS regressed after the rebrand.\" Nobody had touched the layout. Nobody had touched the images. The only diff in that deploy was a font swap — the body copy went from a system font to a licensed one, loaded from `@font-face`.\n\nLighthouse didn't care that the new font looked better. It saw text that painted in one spot, then jumped, and it scored that jump like it scores a banner ad shoving the article down the page.\n\nThe fix everyone reaches for here is `font-display: swap`. It was already set. That was the problem.\n\n## The belief: \"swap\" is the whole fix\n\nHere's the belief, and it's not wrong, exactly: web fonts load over the network, and by default most browsers render *nothing* until the font arrives — the flash of invisible text, FOIT. You add `font-display: swap` to your `@font-face` rule, the browser paints your fallback font immediately, and swaps in the real font the moment it's ready. No more invisible text. Problem solved. Ship it.\n\n```css\n@font-face {\n  font-family: \"Brand Sans\";\n  src: url(\"\u002Ffonts\u002Fbrand-sans.woff2\") format(\"woff2\");\n  font-display: swap;\n}\n```\n\nThat line is in a lot of style guides as *the* answer to font loading. It's treated as a toggle: FOIT off, done.\n\n## The crack: swap doesn't hide the font load. It just moves where you feel it.\n\n`font-display` doesn't control *whether* the browser substitutes a fallback font — every strategy except `block` does that. What it controls is the **timeline**: how long the browser waits invisibly, and how long it's willing to keep showing the fallback before giving up on the real font for that paint. Per the spec, each value defines a block period and a swap period:\n\n- `auto` — browser's own default, usually close to `block`.\n- `block` — invisible for ~3s, then swap (classic FOIT).\n- `swap` — essentially no invisible period; show the fallback instantly, swap in whenever the real font lands (FOUT instead).\n- `fallback` — a brief invisible flash, a short swap window, then it commits to the fallback if the font's still not here.\n- `optional` — barely waits at all, and won't swap mid-visit on a slow connection; the real font may only show up on the *next* navigation, once it's cached.\n\nNotice what none of those values promise: that the fallback font and the real font occupy the same pixels. `swap` guarantees a render happens fast. It says nothing about whether that render holds still when the second one replaces it.\n\nFonts aren't interchangeable rectangles. Two fonts at the same `font-size` can have wildly different x-heights, character widths, and line heights — a serif webfont next to `Arial` as a fallback is a common offender. When the swap happens, every line that used the fallback re-wraps or re-measures against the new metrics. Paragraphs reflow. Buttons that sat right under the headline jump down eight pixels. If that happens after the page has already settled — which, on a flaky connection, it will — it's an *unexpected* shift, and Cumulative Layout Shift counts every one of those against you.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ffont-loading-api-font-display-cls\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\nSo you didn't fix the font-loading problem. You moved it from \"the user sees nothing\" (bad, but at least static) to \"the user sees something, then watches it move\" (arguably worse, and it's the one Lighthouse actively measures).\n\n## Why the belief persists\n\nBecause `swap` usually *looks* fine in a quick check. If your fallback stack happens to be metrically close to your webfont — two similar sans-serifs, say — the shift is a pixel or two and nobody notices scrolling past it. It only becomes visible, and scoreable, when the fonts diverge more: a wide geometric sans swapped in over `Helvetica`, or a serif swapped in over `system-ui`. And most font-loading guides stop at \"FOIT vs FOUT,\" because that's the part you can see with your eyes. CLS is the part a profiler tells you about, days later, in a Core Web Vitals report nobody opens until SEO flags it.\n\n## The better mental model: make the fallback lie convincingly\n\nThe fix isn't picking a different `font-display` value — `swap` was the right call. The fix is making the fallback font's box match the real font's box closely enough that swapping it out doesn't move anything. CSS Fonts Level 4 gives you the descriptors to do exactly that, declared on a **second** `@font-face` for your fallback, under its own name:\n\n```css\n@font-face {\n  font-family: \"Brand Sans Fallback\";\n  src: local(\"Arial\");\n  size-adjust: 104%;\n  ascent-override: 92%;\n  descent-override: 24%;\n  line-gap-override: 0%;\n}\n\nbody {\n  font-family: \"Brand Sans\", \"Brand Sans Fallback\", sans-serif;\n}\n```\n\n`size-adjust` scales the fallback font's whole metrics uniformly; `ascent-override`, `descent-override`, and `line-gap-override` tune the vertical metrics that drive line-box height independently. Get them close to the webfont's own metrics, and the fallback occupies almost exactly the same footprint — so when the real font swaps in, nothing reflows. The text might still change shape slightly, but the *layout* holds still, which is what CLS actually measures.\n\nNobody tunes these four numbers by hand from a font's metadata — that's what tools like Capsize or the automatic fallback generation in `next\u002Ffont` exist for. The point isn't to memorize the math; it's to know this is a solved, declarative problem, not a \"pick a better `font-display` value\" problem.\n\nTwo more pieces worth knowing once you're here:\n\n- **`document.fonts.ready`** is a promise that resolves once the document's font set has finished loading — useful when you're doing something that depends on final text metrics, like measuring a canvas-rendered label, and need to know the swap has already happened.\n- **`font-display: optional`** is the right call, not a worse one, for anything where showing *a* font instantly matters more than showing *the* font eventually — a above-the-fold hero on a page where first paint speed outranks brand fidelity. It trades \"might never show the webfont this visit\" for \"will never cause a mid-visit shift.\"\n\n## When plain `swap` is still fine\n\nDon't go tune override descriptors on every `@font-face` in the project. It's unnecessary when:\n\n- Your fallback and webfont are metrically close already (most humanist sans-serif webfonts over `system-ui` shift by a pixel or less).\n- The text is small and short — a button label has little room to reflow even with a metrics mismatch.\n- The font is almost always cached before paint (self-hosted, preloaded, same-origin, repeat visitor) — there's rarely a visible swap to cause a shift in the first place.\n\nReach for the fallback-matching technique where the shift is visible and the element is large: headlines, hero copy, anything above the fold that Lighthouse measures in its first few seconds.\n\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ffont-loading-api-font-display-cls\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n## The lesson\n\n`font-display: swap` answers \"does the user see text immediately?\" It was never an answer to \"does the page stay still when the real font shows up?\" — that's a layout question, and layout questions get layout answers: matched metrics, not a different loading strategy.\n\nNext time CLS regresses right after a font swap and nobody touched the CSS, you'll know exactly where to look. What's the worst \"nobody touched the layout\" regression you've chased down to a font? I'll start in the comments.\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":110,"canonical":290,"description":291},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ffont-loading-api-font-display-cls","Setting font-display: swap kills the flash of invisible text, so most teams call the font-loading problem solved and move on. It doesn't — it just trades FOIT for a layout shift Li","01a1749f-22b7-73c8-9c8c-45be2a6669ae",{"id":294,"locked":18},"01a1749f-23e5-75be-a4ae-0ce134bbf429",[296],{"id":45,"slug":46,"title":48,"_count":297},{"questions":51},[299],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":301,"questionCount":51},{"questions":51},[]]