[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-css-contain-rendering-isolation":44,"search-suggestions":60,"quiz-article-css-contain-rendering-isolation":109,"path-membership-css-contain-rendering-isolation":297},[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},"01a12aed-367c-76b3-af5c-64765cfb15c3","css-contain-rendering-isolation","PRACTICE_QUIZ","CSS contain: draw the boundary","Eight questions on what contain actually promises the browser, the keywords that compose it, and the size-containment gotcha that can collapse an element to nothing.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},8,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,101,105],{"slug":62,"name":63,"articles":64},"webdev","Webdev",141,{"slug":66,"name":67,"articles":68},"javascript","Javascript",114,{"slug":70,"name":71,"articles":72},"frontend","Frontend",86,{"slug":74,"name":75,"articles":76},"tutorial","Tutorial",53,{"slug":78,"name":79,"articles":80},"css","Css",43,{"slug":82,"name":83,"articles":84},"performance","Performance",23,{"slug":86,"name":87,"articles":88},"typescript","Typescript",18,{"slug":90,"name":91,"articles":92},"react","React",17,{"slug":94,"name":95,"articles":96},"browser","Browser",13,{"slug":98,"name":99,"articles":100},"node","Node",12,{"slug":102,"name":103,"articles":104},"programming","Programming",10,{"slug":106,"name":107,"articles":108},"accessibility","Accessibility",9,{"id":110,"slug":46,"title":111,"subtitle":52,"excerpt":112,"coverUrl":113,"locale":13,"readingMinutes":114,"publishedAt":115,"viewCount":116,"likeCount":19,"commentCount":19,"author":117,"vertical":122,"topic":123,"tags":125,"_count":130,"playground":132,"body":134,"bodyMd":283,"seo":284,"translationGroupId":287,"series":52,"podcastUrl":52,"verticalId":5,"thread":288,"assessments":290,"translations":293,"quiz":295},"01a12aed-3621-77cf-8827-f0fa7f687105","Stop Letting One Widget Reflow the Whole Page. `contain` Scopes It.","A live counter or chat widget shouldn't make the rest of your page recompute layout every time it updates. The CSS contain property draws a hard boundary the browser can actually trust — here's what it promises, and the one gotcha that breaks pages when people misuse it.","\u002Fmedia\u002Fcovers\u002Fcss-contain-rendering-isolation.png",6,"2026-10-16T09:23:09.005Z",45,{"id":118,"name":119,"username":120,"avatarUrl":52,"headline":121},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":78,"name":124},"CSS",[126,127,128,129],{"slug":78,"name":79,"color":52},{"slug":82,"name":83,"color":52},{"slug":62,"name":63,"color":52},{"slug":70,"name":71,"color":52},{"assessments":131},1,{"slug":46,"title":133},"CSS contain — draw the boundary",{"blocks":135,"version":131},[136,140,143,146,150,153,159,162,166,169,172,175,178,182,185,193,196,199,202,205,208,211,215,218,222,225,228,232,235,238,241,244,247,250,253,256,259,262,265,268,271,274,277],{"id":137,"html":138,"type":139},"b1","\u003Cp>Open the \u003Cstrong>Performance\u003C\u002Fstrong> tab, record five seconds of a dashboard with a live &quot;3 new messages&quot; counter in the corner, and look at the flame chart. There&#39;s a good chance you&#39;ll see a \u003Cstrong>Layout\u003C\u002Fstrong> block fire every time that counter ticks — not sized to one \u003Ccode>&lt;span&gt;\u003C\u002Fcode>, but spanning a chunk of the page that has nothing to do with it.\u003C\u002Fp>","paragraph",{"id":141,"html":142,"type":139},"b2","\u003Cp>Nobody wrote code that does that on purpose. The browser just doesn&#39;t know any better: by default, a change anywhere in the DOM is a reason to ask whether anything \u003Cem>outside\u003C\u002Fem> that element needs to be measured again. A \u003Ccode>&lt;span&gt;\u003C\u002Fcode> updating its text could, in principle, change its own size, which could push its parent, which could push a sibling, which could ripple all the way out. The layout engine can&#39;t assume otherwise, so for anything it isn&#39;t sure about, it checks.\u003C\u002Fp>",{"id":144,"html":145,"type":139},"b3","\u003Cp>That&#39;s the part almost nobody profiles for, because the obvious suspect is always JavaScript.\u003C\u002Fp>",{"id":147,"html":148,"text":148,"type":149,"level":31},"b4","The fix everyone reaches for first: throttle the JS","heading",{"id":151,"html":152,"type":139},"b5","\u003Cp>Say the counter updates from a WebSocket message every couple of seconds, and someone notices the jank. The first instinct is almost always to blame the handler:\u003C\u002Fp>",{"id":154,"code":155,"type":156,"language":157,"highlight":158},"b6","socket.onmessage = (e) => {\n  \u002F\u002F \"it's probably firing too often\" — throttle it\n  updateCounterThrottled(e.data);\n};","code","js",[],{"id":160,"html":161,"type":139},"b7","\u003Cp>Sometimes that genuinely helps, if the handler is doing real work. But plenty of the time the handler is one \u003Ccode>textContent\u003C\u002Fcode> assignment, the throttle buys nothing, and the Layout block in the profiler is exactly the same width as before — because throttling how \u003Cem>often\u003C\u002Fem> something changes doesn&#39;t change how much the browser has to re-check \u003Cem>each time\u003C\u002Fem> it does. The cost was never in the JS. It was in what layout had to consider every time that one element changed.\u003C\u002Fp>",{"id":163,"html":164,"text":165,"type":149,"level":31},"b8","What&#39;s actually happening: layout has no boundaries by default","What's actually happening: layout has no boundaries by default",{"id":167,"html":168,"type":139},"b9","\u003Cp>Every element in the DOM is, by default, part of one connected layout problem. There&#39;s no built-in notion of &quot;this subtree is self-contained&quot; — the browser has to be conservative, because CSS gives it plenty of ways for a tiny element to affect something far away: a child wider than its parent, a negative margin, an absolutely positioned descendant escaping its ancestor, intrinsic sizing that depends on content. Any of those \u003Cem>could\u003C\u002Fem> be happening, so near any DOM change, the engine re-checks more than the one element that changed.\u003C\u002Fp>",{"id":170,"html":171,"type":139},"b10","\u003Cp>This is the part the Myth &quot;if it&#39;s slow, profile the JS&quot; misses: sometimes the JS is one line, and the real cost is a layout engine doing its job correctly with no boundary to stop at.\u003C\u002Fp>",{"id":173,"html":174,"text":174,"type":149,"level":31},"b11","The fix: draw the boundary yourself",{"id":176,"html":177,"type":139},"b12","\u003Cp>\u003Ca href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002Fcontain\">CSS containment\u003C\u002Fa> exists to let you make that promise explicit. \u003Ccode>contain: layout\u003C\u002Fcode> tells the browser: whatever happens inside this element, it does not affect anything outside it — and nothing outside affects its internal layout either. That&#39;s not a performance hint the browser might ignore, the way \u003Ccode>will-change\u003C\u002Fcode> is; it&#39;s a contract. Once you&#39;ve made it, the engine can stop checking past the boundary.\u003C\u002Fp>",{"id":179,"code":180,"type":156,"language":78,"highlight":181},"b13",".widget {\n  contain: content; \u002F* layout + style + paint, no size *\u002F\n}",[],{"id":183,"html":184,"type":139},"b14","\u003Cp>\u003Ccode>contain\u003C\u002Fcode> takes several keywords, and they compose:\u003C\u002Fp>",{"id":186,"type":187,"items":188,"ordered":18},"b15","list",[189,190,191,192],"\u003Cstrong>\u003Ccode>layout\u003C\u002Fcode>\u003C\u002Fstrong> — the element&#39;s internal layout can&#39;t leak out, and outside layout can&#39;t reach in. This is the one that stops the ripple.","\u003Cstrong>\u003Ccode>paint\u003C\u002Fcode>\u003C\u002Fstrong> — nothing the element paints (or its descendants paint) can render outside its own box. The browser can skip painting it entirely if it&#39;s off-screen or hidden.","\u003Cstrong>\u003Ccode>size\u003C\u002Fcode>\u003C\u002Fstrong> — the element&#39;s size can be computed without looking at its contents at all. Powerful, but it has a real catch (below).","\u003Cstrong>\u003Ccode>content\u003C\u002Fcode>\u003C\u002Fstrong> is shorthand for \u003Ccode>layout paint style\u003C\u002Fcode> together. \u003Cstrong>\u003Ccode>strict\u003C\u002Fcode>\u003C\u002Fstrong> adds \u003Ccode>size\u003C\u002Fcode> on top of that.",{"id":194,"html":195,"type":139},"b16","\u003Cp>Add \u003Ccode>layout paint\u003C\u002Fcode> (or \u003Ccode>content\u003C\u002Fcode>) to that counter widget&#39;s wrapper, and the Layout block in the next profile is scoped to the widget — the rest of the dashboard stops showing up in it, because the browser now has a guarantee it didn&#39;t have before.\u003C\u002Fp>",{"id":197,"html":198,"type":139},"b17","\u003C!-- playground:start -->",{"id":200,"html":201,"text":201,"type":149,"level":31},"b18","🎮 Try it yourself",{"id":203,"html":204,"type":139},"b19","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-contain-rendering-isolation\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":206,"html":207,"type":139},"b20","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":209,"html":210,"type":139},"b21","\u003C!-- playground:end -->",{"id":212,"html":213,"text":214,"type":149,"level":31},"b22","The gotcha: \u003Ccode>size\u003C\u002Fcode> containment can collapse your element to nothing","The gotcha: size containment can collapse your element to nothing",{"id":216,"html":217,"type":139},"b23","\u003Cp>Here&#39;s where people get burned. \u003Ccode>contain: strict\u003C\u002Fcode> sounds like the &quot;turn it all on&quot; option, and \u003Ccode>size\u003C\u002Fcode> containment is the part that quietly breaks things:\u003C\u002Fp>",{"id":219,"code":220,"type":156,"language":78,"highlight":221},"b24","\u002F* looks safe — it's not, if .widget has no explicit height *\u002F\n.widget {\n  contain: strict;\n}",[],{"id":223,"html":224,"type":139},"b25","\u003Cp>Size containment means the browser computes this element&#39;s size \u003Cstrong>as if it had no content\u003C\u002Fstrong> — on purpose, that&#39;s the whole point, so it never has to wait on the subtree to know how much space to reserve. But &quot;as if it had no content&quot; means an element with no explicit \u003Ccode>width\u003C\u002Fcode>\u002F\u003Ccode>height\u003C\u002Fcode> (or \u003Ccode>aspect-ratio\u003C\u002Fcode>) and no content to size from collapses toward zero. The widget doesn&#39;t jank anymore. It just silently stops taking up space, and you find out from a bug report, not a profiler.\u003C\u002Fp>",{"id":226,"html":227,"type":139},"b26","\u003Cp>The fix is to pair \u003Ccode>size\u003C\u002Fcode> containment with an explicit size:\u003C\u002Fp>",{"id":229,"code":230,"type":156,"language":78,"highlight":231},"b27",".widget {\n  contain: strict;\n  width: 280px;\n  height: 120px; \u002F* or min-height, or aspect-ratio *\u002F\n}",[],{"id":233,"html":234,"type":139},"b28","\u003Cp>This is exactly the trade \u003Ccode>content-visibility: auto\u003C\u002Fcode> makes for you automatically when an element scrolls off-screen — it&#39;s \u003Ccode>contain: layout style paint\u003C\u002Fcode>, plus \u003Ccode>size\u003C\u002Fcode> containment kicked in conditionally, which is why \u003Ca href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002Fcontent-visibility\">that property\u003C\u002Fa> asks you to set a \u003Ccode>contain-intrinsic-size\u003C\u002Fcode> placeholder. \u003Ccode>contain\u003C\u002Fcode> on its own gives you the same lever with none of the training wheels: you decide which boxes get which guarantees, and you&#39;re responsible for the size one.\u003C\u002Fp>",{"id":236,"html":237,"type":139},"b29","\u003Cp>One more side effect worth knowing before you add this everywhere: \u003Ccode>layout\u003C\u002Fcode>, \u003Ccode>paint\u003C\u002Fcode>, \u003Ccode>content\u003C\u002Fcode>, and \u003Ccode>strict\u003C\u002Fcode> all turn the element into a new \u003Cstrong>containing block\u003C\u002Fstrong> for absolutely- and fixed-positioned descendants, and a new \u003Cstrong>stacking context\u003C\u002Fstrong>. A child using \u003Ccode>position: fixed\u003C\u002Fcode> to escape to the viewport, or a dropdown relying on \u003Ccode>position: absolute\u003C\u002Fcode> against some ancestor further up the tree, will suddenly anchor to your newly-contained box instead. That&#39;s rarely what breaks a dashboard widget — but it&#39;s exactly what breaks a modal or a tooltip if you reach for \u003Ccode>contain\u003C\u002Fcode> on a wrapper without checking what&#39;s positioned relative to it.\u003C\u002Fp>",{"id":239,"html":240,"text":240,"type":149,"level":31},"b30","When plain, uncontained CSS is still the right call",{"id":242,"html":243,"type":139},"b31","\u003Cp>Most elements on most pages don&#39;t need this. A static hero section, a footer, a paragraph of text — none of it changes after paint, so there&#39;s nothing for containment to isolate it from. Containment earns its keep specifically where something \u003Cstrong>changes on its own schedule\u003C\u002Fstrong> independent of the rest of the page: live counters, chat widgets, auto-refreshing tickers, anything driven by a timer or a socket rather than user navigation. If you haven&#39;t opened the Performance tab and actually seen a Layout block wider than the element that triggered it, you don&#39;t have evidence containment will help — you have a hunch, and \u003Ccode>contain: strict\u003C\u002Fcode> on a hunch is how an element quietly collapses to zero height.\u003C\u002Fp>",{"id":245,"html":246,"text":246,"type":149,"level":31},"b32","The takeaway",{"id":248,"html":249,"type":139},"b33","\u003Cp>The layout engine isn&#39;t being wasteful when it re-checks more than you expected — it&#39;s being correct, given that it has no way to know your widget is self-contained. \u003Ccode>contain\u003C\u002Fcode> is how you tell it. Scope it to the one element that changes on its own, pick \u003Ccode>layout paint\u003C\u002Fcode> (or \u003Ccode>content\u003C\u002Fcode>) first, and only reach for \u003Ccode>size\u003C\u002Fcode> \u002F \u003Ccode>strict\u003C\u002Fcode> once that element has an explicit size to fall back on. The profiler cost that looked like a JavaScript problem was a missing boundary the whole time.\u003C\u002Fp>",{"id":251,"html":252,"type":139},"b34","\u003Cp>Go record a profile on the one thing on your page that updates without a page reload — a counter, a ticker, a live status dot — and check whether the Layout block is scoped to it. What did you find?\u003C\u002Fp>",{"id":254,"html":255,"type":139},"b35","\u003C!-- quiz:start -->",{"id":257,"html":258,"text":258,"type":149,"level":31},"b36","🧠 Test yourself",{"id":260,"html":261,"type":139},"b37","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-contain-rendering-isolation\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":263,"html":264,"type":139},"b38","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":266,"html":267,"type":139},"b39","\u003C!-- quiz:end -->",{"id":269,"type":270},"b40","divider",{"id":272,"html":273,"type":139},"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":275,"html":276,"type":139},"b42","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":278,"type":187,"items":279,"ordered":18},"b43",[280,281,282],"⭐ \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>","Open the **Performance** tab, record five seconds of a dashboard with a live \"3 new messages\" counter in the corner, and look at the flame chart. There's a good chance you'll see a **Layout** block fire every time that counter ticks — not sized to one `\u003Cspan>`, but spanning a chunk of the page that has nothing to do with it.\n\nNobody wrote code that does that on purpose. The browser just doesn't know any better: by default, a change anywhere in the DOM is a reason to ask whether anything *outside* that element needs to be measured again. A `\u003Cspan>` updating its text could, in principle, change its own size, which could push its parent, which could push a sibling, which could ripple all the way out. The layout engine can't assume otherwise, so for anything it isn't sure about, it checks.\n\nThat's the part almost nobody profiles for, because the obvious suspect is always JavaScript.\n\n## The fix everyone reaches for first: throttle the JS\n\nSay the counter updates from a WebSocket message every couple of seconds, and someone notices the jank. The first instinct is almost always to blame the handler:\n\n```js\nsocket.onmessage = (e) => {\n  \u002F\u002F \"it's probably firing too often\" — throttle it\n  updateCounterThrottled(e.data);\n};\n```\n\nSometimes that genuinely helps, if the handler is doing real work. But plenty of the time the handler is one `textContent` assignment, the throttle buys nothing, and the Layout block in the profiler is exactly the same width as before — because throttling how *often* something changes doesn't change how much the browser has to re-check *each time* it does. The cost was never in the JS. It was in what layout had to consider every time that one element changed.\n\n## What's actually happening: layout has no boundaries by default\n\nEvery element in the DOM is, by default, part of one connected layout problem. There's no built-in notion of \"this subtree is self-contained\" — the browser has to be conservative, because CSS gives it plenty of ways for a tiny element to affect something far away: a child wider than its parent, a negative margin, an absolutely positioned descendant escaping its ancestor, intrinsic sizing that depends on content. Any of those *could* be happening, so near any DOM change, the engine re-checks more than the one element that changed.\n\nThis is the part the Myth \"if it's slow, profile the JS\" misses: sometimes the JS is one line, and the real cost is a layout engine doing its job correctly with no boundary to stop at.\n\n## The fix: draw the boundary yourself\n\n[CSS containment](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002Fcontain) exists to let you make that promise explicit. `contain: layout` tells the browser: whatever happens inside this element, it does not affect anything outside it — and nothing outside affects its internal layout either. That's not a performance hint the browser might ignore, the way `will-change` is; it's a contract. Once you've made it, the engine can stop checking past the boundary.\n\n```css\n.widget {\n  contain: content; \u002F* layout + style + paint, no size *\u002F\n}\n```\n\n`contain` takes several keywords, and they compose:\n\n- **`layout`** — the element's internal layout can't leak out, and outside layout can't reach in. This is the one that stops the ripple.\n- **`paint`** — nothing the element paints (or its descendants paint) can render outside its own box. The browser can skip painting it entirely if it's off-screen or hidden.\n- **`size`** — the element's size can be computed without looking at its contents at all. Powerful, but it has a real catch (below).\n- **`content`** is shorthand for `layout paint style` together. **`strict`** adds `size` on top of that.\n\nAdd `layout paint` (or `content`) to that counter widget's wrapper, and the Layout block in the next profile is scoped to the widget — the rest of the dashboard stops showing up in it, because the browser now has a guarantee it didn't have before.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-contain-rendering-isolation\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## The gotcha: `size` containment can collapse your element to nothing\n\nHere's where people get burned. `contain: strict` sounds like the \"turn it all on\" option, and `size` containment is the part that quietly breaks things:\n\n```css\n\u002F* looks safe — it's not, if .widget has no explicit height *\u002F\n.widget {\n  contain: strict;\n}\n```\n\nSize containment means the browser computes this element's size **as if it had no content** — on purpose, that's the whole point, so it never has to wait on the subtree to know how much space to reserve. But \"as if it had no content\" means an element with no explicit `width`\u002F`height` (or `aspect-ratio`) and no content to size from collapses toward zero. The widget doesn't jank anymore. It just silently stops taking up space, and you find out from a bug report, not a profiler.\n\nThe fix is to pair `size` containment with an explicit size:\n\n```css\n.widget {\n  contain: strict;\n  width: 280px;\n  height: 120px; \u002F* or min-height, or aspect-ratio *\u002F\n}\n```\n\nThis is exactly the trade `content-visibility: auto` makes for you automatically when an element scrolls off-screen — it's `contain: layout style paint`, plus `size` containment kicked in conditionally, which is why [that property](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002Fcontent-visibility) asks you to set a `contain-intrinsic-size` placeholder. `contain` on its own gives you the same lever with none of the training wheels: you decide which boxes get which guarantees, and you're responsible for the size one.\n\nOne more side effect worth knowing before you add this everywhere: `layout`, `paint`, `content`, and `strict` all turn the element into a new **containing block** for absolutely- and fixed-positioned descendants, and a new **stacking context**. A child using `position: fixed` to escape to the viewport, or a dropdown relying on `position: absolute` against some ancestor further up the tree, will suddenly anchor to your newly-contained box instead. That's rarely what breaks a dashboard widget — but it's exactly what breaks a modal or a tooltip if you reach for `contain` on a wrapper without checking what's positioned relative to it.\n\n## When plain, uncontained CSS is still the right call\n\nMost elements on most pages don't need this. A static hero section, a footer, a paragraph of text — none of it changes after paint, so there's nothing for containment to isolate it from. Containment earns its keep specifically where something **changes on its own schedule** independent of the rest of the page: live counters, chat widgets, auto-refreshing tickers, anything driven by a timer or a socket rather than user navigation. If you haven't opened the Performance tab and actually seen a Layout block wider than the element that triggered it, you don't have evidence containment will help — you have a hunch, and `contain: strict` on a hunch is how an element quietly collapses to zero height.\n\n## The takeaway\n\nThe layout engine isn't being wasteful when it re-checks more than you expected — it's being correct, given that it has no way to know your widget is self-contained. `contain` is how you tell it. Scope it to the one element that changes on its own, pick `layout paint` (or `content`) first, and only reach for `size` \u002F `strict` once that element has an explicit size to fall back on. The profiler cost that looked like a JavaScript problem was a missing boundary the whole time.\n\nGo record a profile on the one thing on your page that updates without a page reload — a counter, a ticker, a live status dot — and check whether the Layout block is scoped to it. What did you find?\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-contain-rendering-isolation\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\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":111,"canonical":285,"description":286},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-contain-rendering-isolation","A live counter or chat widget shouldn't make the rest of your page recompute layout every time it updates. The CSS contain property draws a hard boundary the browser can actually t","01a12aed-3621-77cf-8827-f6bd050b3b14",{"id":289,"locked":18},"01a12aed-3660-726b-9d0f-6df0ad7b739e",[291],{"id":45,"slug":46,"title":48,"_count":292},{"questions":51},[294],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":296,"questionCount":51},{"questions":51},[]]