[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-declarative-shadow-dom-ssr-web-components":44,"search-suggestions":225,"related-declarative-shadow-dom-ssr-web-components":263,"code:html:true:8wk77v":397,"code:html:true:1q2ai2m":398,"comments-01a0499d-bb9f-709b-9e8a-9c426ae95530":399},[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,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":53,"likeCount":19,"commentCount":19,"author":54,"vertical":59,"topic":60,"tags":63,"_count":75,"playground":77,"body":79,"bodyMd":208,"seo":209,"translationGroupId":212,"series":48,"podcastUrl":48,"verticalId":5,"thread":213,"assessments":215,"translations":221,"quiz":223},"01a0499d-bb9f-709b-9e8a-9c426ae95530","declarative-shadow-dom-ssr-web-components","Shadow DOM Needed JavaScript to Exist. Now It Doesn't.",null,"attachShadow() is a JavaScript-only API with no HTML serialization, so a server-rendered web component ships an empty host element until the client hydrates it. Declarative Shadow DOM attaches the real shadow root during HTML parsing — zero JS required.","\u002Fmedia\u002Fcovers\u002Fdeclarative-shadow-dom-ssr-web-components.png",5,"2026-09-02T10:57:52.645Z",27,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},"javascript","JavaScript",[64,67,70,72],{"slug":65,"name":66,"color":48},"webdev","Webdev",{"slug":68,"name":69,"color":48},"frontend","Frontend",{"slug":61,"name":71,"color":48},"Javascript",{"slug":73,"name":74,"color":48},"html","Html",{"assessments":76},1,{"slug":46,"title":78},"Declarative Shadow DOM — the parse-time race",{"blocks":80,"version":76},[81,85,88,91,94,99,102,109,112,115,118,121,124,127,132,136,139,142,148,151,154,157,160,163,166,169,172,175,178,181,184,187,190,193,196,199,202],{"id":82,"html":83,"type":84},"b1","\u003Cp>You curl your own page. The one with the fancy web component — the accordion, the tabs, whatever you built to be reusable everywhere. You SSR&#39;d it. You&#39;re proud of it. You grep the response for the text inside it.\u003C\u002Fp>","paragraph",{"id":86,"html":87,"type":84},"b2","\u003Cp>It&#39;s not there.\u003C\u002Fp>",{"id":89,"html":90,"type":84},"b3","\u003Cp>What&#39;s there instead is a custom element with nothing inside it, and if you squint at the raw HTML you might spot a \u003Ccode>&lt;template&gt;\u003C\u002Fcode> tag nearby, doing nothing. The content — the styled, encapsulated, actually-rendered thing a user sees — doesn&#39;t exist yet. It shows up a beat later, after JavaScript runs, which on a slow connection or a busy main thread is long enough to see the host element sit empty, then pop.\u003C\u002Fp>",{"id":92,"html":93,"type":84},"b4","\u003Cp>Here&#39;s the part that takes people longer to accept than it should: this was never a bug in your SSR setup. Shadow DOM itself had no way to be server-rendered. Not &quot;hard&quot; — structurally absent.\u003C\u002Fp>",{"id":95,"html":96,"text":97,"type":98,"level":31},"b5","The obvious fixes that don&#39;t touch the real problem","The obvious fixes that don't touch the real problem","heading",{"id":100,"html":101,"type":84},"b6","\u003Cp>The instinct, once you notice the flash, is to attack it like a loading-performance problem:\u003C\u002Fp>",{"id":103,"type":104,"items":105,"ordered":18},"b7","list",[106,107,108],"\u003Cstrong>Inline the critical CSS\u003C\u002Fstrong> so at least the \u003Cem>page\u003C\u002Fem> doesn&#39;t flash unstyled. Doesn&#39;t help — the shadow tree&#39;s content isn&#39;t unstyled, it&#39;s \u003Cem>absent\u003C\u002Fem>. There&#39;s nothing to style yet.","\u003Cstrong>Defer less, preload the script earlier.\u003C\u002Fstrong> Shaves milliseconds. The gap doesn&#39;t close, it shrinks — and on a throttled connection or a busy tab it&#39;s still visible.","\u003Cstrong>Skeleton-load it\u003C\u002Fstrong>, show a placeholder shape until hydration. This works, and it&#39;s honestly the standard answer today — but notice what you just admitted: you&#39;re not server-rendering the component, you&#39;re server-rendering an apology for the component and rendering the real thing later, client-side, same as if you&#39;d shipped no HTML at all.",{"id":110,"html":111,"type":84},"b8","\u003Cp>All three treat the symptom. None of them explain \u003Cem>why\u003C\u002Fem> a server-rendered custom element can serialize its attributes and its light-DOM children into HTML, but not its shadow tree.\u003C\u002Fp>",{"id":113,"html":114,"text":114,"type":98,"level":31},"b9","The real diagnosis: there was no way to write it down",{"id":116,"html":117,"type":84},"b10","\u003Cp>\u003Ccode>element.attachShadow({ mode: &quot;open&quot; })\u003C\u002Fcode> is a method call. It runs in JavaScript, against a live DOM node, in a live document. There has never been an HTML syntax for &quot;this element has a shadow root containing this markup&quot; — the shadow tree simply doesn&#39;t exist as text you can send over the wire. A server can stringify your component&#39;s attributes and its regular children all day. The encapsulated part — the actual point of using Shadow DOM — has nowhere to go in that string.\u003C\u002Fp>",{"id":119,"html":120,"type":84},"b11","\u003Cp>So &quot;server-rendered web component&quot; quietly meant: server-rendered \u003Cem>light DOM\u003C\u002Fem>, client-attached shadow DOM, and a component that isn&#39;t really done rendering until JavaScript shows up and calls a method. For a design system trying to sell itself on being framework-agnostic and usable anywhere — including a no-JS environment, a crawler, or a slow device — that&#39;s a real gap, not a nitpick.\u003C\u002Fp>",{"id":122,"html":123,"text":123,"type":98,"level":31},"b12","The fix: give the parser something to attach",{"id":125,"html":126,"type":84},"b13","\u003Cp>\u003Ca href=\"https:\u002F\u002Fweb.dev\u002Farticles\u002Fdeclarative-shadow-dom\">Declarative Shadow DOM\u003C\u002Fa> closes the gap by teaching the HTML parser a new trick instead of teaching JavaScript a new one. Put a \u003Ccode>&lt;template&gt;\u003C\u002Fcode> with a \u003Ccode>shadowrootmode\u003C\u002Fcode> attribute as the \u003Cem>first child\u003C\u002Fem> of an element, and the parser attaches its contents as a real, live shadow root the instant it finishes reading the closing tag — before any \u003Ccode>&lt;script&gt;\u003C\u002Fcode> further down the page has run:\u003C\u002Fp>",{"id":128,"code":129,"type":130,"language":73,"highlight":131},"b14","\u003C!-- the old way: an empty host, waiting for JS -->\n\u003Cmy-badge>\u003C\u002Fmy-badge>\n\u003Cscript>\n  customElements.get(\"my-badge\") ||\n    customElements.define(\"my-badge\", class extends HTMLElement {\n      connectedCallback() {\n        this.attachShadow({ mode: \"open\" }).innerHTML =\n          `\u003Cstyle>span{color:hotpink}\u003C\u002Fstyle>\u003Cspan>New\u003C\u002Fspan>`;\n      }\n    });\n\u003C\u002Fscript>","code",[],{"id":133,"code":134,"type":130,"language":73,"highlight":135},"b15","\u003C!-- the new way: the shadow root exists the moment HTML parses -->\n\u003Cmy-badge>\n  \u003Ctemplate shadowrootmode=\"open\">\n    \u003Cstyle>span { color: hotpink; }\u003C\u002Fstyle>\n    \u003Cspan>New\u003C\u002Fspan>\n  \u003C\u002Ftemplate>\n\u003C\u002Fmy-badge>",[],{"id":137,"html":138,"type":84},"b16","\u003Cp>No script needed to make the second version render — \u003Ccode>document.querySelector(&quot;my-badge&quot;).shadowRoot\u003C\u002Fcode> is already non-null before your JavaScript has had a chance to run at all. If you \u003Cem>do\u003C\u002Fem> still want to progressively enhance it with a custom element class, that&#39;s fine: attach-shadow-if-not-already-attached is the standard guard, since a browser that already declaratively created the shadow root won&#39;t create a second one for you.\u003C\u002Fp>",{"id":140,"html":141,"type":84},"b17","\u003Cp>A couple of details worth knowing before you reach for it:\u003C\u002Fp>",{"id":143,"type":104,"items":144,"ordered":18},"b18",[145,146,147],"\u003Cstrong>\u003Ccode>shadowrootmode\u003C\u002Fcode> is \u003Ccode>&quot;open&quot;\u003C\u002Fcode> or \u003Ccode>&quot;closed&quot;\u003C\u002Fcode>\u003C\u002Fstrong>, mirroring \u003Ccode>attachShadow()\u003C\u002Fcode>&#39;s mode option — \u003Ccode>&quot;closed&quot;\u003C\u002Fcode> still renders the content, it just means \u003Ccode>element.shadowRoot\u003C\u002Fcode> returns \u003Ccode>null\u003C\u002Fcode> to outside script, same as the imperative API.","\u003Cstrong>Only the first matching \u003Ccode>&lt;template&gt;\u003C\u002Fcode> counts.\u003C\u002Fstrong> If a parent already got a declarative shadow root, a second \u003Ccode>shadowrootmode\u003C\u002Fcode> template in the same position is left alone as a plain, inert \u003Ccode>&lt;template&gt;\u003C\u002Fcode> element rather than throwing — a browsers-are-forgiving default, but not one to rely on if you&#39;re generating this HTML yourself.","\u003Cstrong>Round-tripping it needs an extra flag.\u003C\u002Fstrong> If you want to read a component&#39;s \u003Cem>current\u003C\u002Fem> shadow content back out as a string (say, to re-serialize it after a client-side update), add \u003Ccode>shadowrootserializable\u003C\u002Fcode> to the template and call \u003Ccode>getHTML({ serializableShadowRoots: true })\u003C\u002Fcode> — without it, \u003Ccode>outerHTML\u003C\u002Fcode> and friends skip shadow content entirely, same as before.",{"id":149,"html":150,"type":84},"b19","\u003C!-- playground:start -->",{"id":152,"html":153,"text":153,"type":98,"level":31},"b20","🎮 Try it yourself",{"id":155,"html":156,"type":84},"b21","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fdeclarative-shadow-dom-ssr-web-components\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":158,"html":159,"type":84},"b22","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":161,"html":162,"type":84},"b23","\u003C!-- playground:end -->",{"id":164,"html":165,"text":165,"type":98,"level":31},"b24","Where it stands today",{"id":167,"html":168,"type":84},"b25","\u003Cp>This isn&#39;t a &quot;wait five years&quot; feature. Declarative Shadow DOM is Baseline: Chrome and Edge since version 111 (with the full attribute set standardized by 124), Firefox since 123, Safari since 16.4. If your SSR framework or web component library doesn&#39;t emit \u003Ccode>shadowrootmode\u003C\u002Fcode> templates yet, that&#39;s a library gap, not a browser one — and it&#39;s worth filing an issue instead of writing another skeleton loader.\u003C\u002Fp>",{"id":170,"html":171,"text":171,"type":98,"level":31},"b26","The one thing worth remembering",{"id":173,"html":174,"type":84},"b27","\u003Cp>An empty custom element with a \u003Ccode>&lt;template&gt;\u003C\u002Fcode> next to it isn&#39;t broken HTML — it&#39;s HTML written for a browser that hasn&#39;t decided to help yet. Declarative Shadow DOM is the browser deciding to help: the parser does at read-time what a \u003Ccode>&lt;script&gt;\u003C\u002Fcode> tag used to have to do at run-time, and the gap between &quot;the server sent HTML&quot; and &quot;the component actually rendered&quot; gets to close for good.\u003C\u002Fp>",{"id":176,"html":177,"type":84},"b28","\u003Cp>Do you SSR web components today — and if so, are you shipping the skeleton-loader workaround or has your framework already picked this up? I&#39;d like to know which libraries got there first.\u003C\u002Fp>",{"id":179,"html":180,"type":84},"b29","\u003C!-- quiz:start -->",{"id":182,"html":183,"text":183,"type":98,"level":31},"b30","🧠 Test yourself",{"id":185,"html":186,"type":84},"b31","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fdeclarative-shadow-dom-ssr-web-components\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":188,"html":189,"type":84},"b32","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":191,"html":192,"type":84},"b33","\u003C!-- quiz:end -->",{"id":194,"type":195},"b34","divider",{"id":197,"html":198,"type":84},"b35","\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":200,"html":201,"type":84},"b36","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":203,"type":104,"items":204,"ordered":18},"b37",[205,206,207],"⭐ \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>","You curl your own page. The one with the fancy web component — the accordion, the tabs, whatever you built to be reusable everywhere. You SSR'd it. You're proud of it. You grep the response for the text inside it.\n\nIt's not there.\n\nWhat's there instead is a custom element with nothing inside it, and if you squint at the raw HTML you might spot a `\u003Ctemplate>` tag nearby, doing nothing. The content — the styled, encapsulated, actually-rendered thing a user sees — doesn't exist yet. It shows up a beat later, after JavaScript runs, which on a slow connection or a busy main thread is long enough to see the host element sit empty, then pop.\n\nHere's the part that takes people longer to accept than it should: this was never a bug in your SSR setup. Shadow DOM itself had no way to be server-rendered. Not \"hard\" — structurally absent.\n\n## The obvious fixes that don't touch the real problem\n\nThe instinct, once you notice the flash, is to attack it like a loading-performance problem:\n\n- **Inline the critical CSS** so at least the *page* doesn't flash unstyled. Doesn't help — the shadow tree's content isn't unstyled, it's *absent*. There's nothing to style yet.\n- **Defer less, preload the script earlier.** Shaves milliseconds. The gap doesn't close, it shrinks — and on a throttled connection or a busy tab it's still visible.\n- **Skeleton-load it**, show a placeholder shape until hydration. This works, and it's honestly the standard answer today — but notice what you just admitted: you're not server-rendering the component, you're server-rendering an apology for the component and rendering the real thing later, client-side, same as if you'd shipped no HTML at all.\n\nAll three treat the symptom. None of them explain *why* a server-rendered custom element can serialize its attributes and its light-DOM children into HTML, but not its shadow tree.\n\n## The real diagnosis: there was no way to write it down\n\n`element.attachShadow({ mode: \"open\" })` is a method call. It runs in JavaScript, against a live DOM node, in a live document. There has never been an HTML syntax for \"this element has a shadow root containing this markup\" — the shadow tree simply doesn't exist as text you can send over the wire. A server can stringify your component's attributes and its regular children all day. The encapsulated part — the actual point of using Shadow DOM — has nowhere to go in that string.\n\nSo \"server-rendered web component\" quietly meant: server-rendered *light DOM*, client-attached shadow DOM, and a component that isn't really done rendering until JavaScript shows up and calls a method. For a design system trying to sell itself on being framework-agnostic and usable anywhere — including a no-JS environment, a crawler, or a slow device — that's a real gap, not a nitpick.\n\n## The fix: give the parser something to attach\n\n[Declarative Shadow DOM](https:\u002F\u002Fweb.dev\u002Farticles\u002Fdeclarative-shadow-dom) closes the gap by teaching the HTML parser a new trick instead of teaching JavaScript a new one. Put a `\u003Ctemplate>` with a `shadowrootmode` attribute as the *first child* of an element, and the parser attaches its contents as a real, live shadow root the instant it finishes reading the closing tag — before any `\u003Cscript>` further down the page has run:\n\n```html\n\u003C!-- the old way: an empty host, waiting for JS -->\n\u003Cmy-badge>\u003C\u002Fmy-badge>\n\u003Cscript>\n  customElements.get(\"my-badge\") ||\n    customElements.define(\"my-badge\", class extends HTMLElement {\n      connectedCallback() {\n        this.attachShadow({ mode: \"open\" }).innerHTML =\n          `\u003Cstyle>span{color:hotpink}\u003C\u002Fstyle>\u003Cspan>New\u003C\u002Fspan>`;\n      }\n    });\n\u003C\u002Fscript>\n```\n\n```html\n\u003C!-- the new way: the shadow root exists the moment HTML parses -->\n\u003Cmy-badge>\n  \u003Ctemplate shadowrootmode=\"open\">\n    \u003Cstyle>span { color: hotpink; }\u003C\u002Fstyle>\n    \u003Cspan>New\u003C\u002Fspan>\n  \u003C\u002Ftemplate>\n\u003C\u002Fmy-badge>\n```\n\nNo script needed to make the second version render — `document.querySelector(\"my-badge\").shadowRoot` is already non-null before your JavaScript has had a chance to run at all. If you *do* still want to progressively enhance it with a custom element class, that's fine: attach-shadow-if-not-already-attached is the standard guard, since a browser that already declaratively created the shadow root won't create a second one for you.\n\nA couple of details worth knowing before you reach for it:\n\n- **`shadowrootmode` is `\"open\"` or `\"closed\"`**, mirroring `attachShadow()`'s mode option — `\"closed\"` still renders the content, it just means `element.shadowRoot` returns `null` to outside script, same as the imperative API.\n- **Only the first matching `\u003Ctemplate>` counts.** If a parent already got a declarative shadow root, a second `shadowrootmode` template in the same position is left alone as a plain, inert `\u003Ctemplate>` element rather than throwing — a browsers-are-forgiving default, but not one to rely on if you're generating this HTML yourself.\n- **Round-tripping it needs an extra flag.** If you want to read a component's *current* shadow content back out as a string (say, to re-serialize it after a client-side update), add `shadowrootserializable` to the template and call `getHTML({ serializableShadowRoots: true })` — without it, `outerHTML` and friends skip shadow content entirely, same as before.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fdeclarative-shadow-dom-ssr-web-components\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Where it stands today\n\nThis isn't a \"wait five years\" feature. Declarative Shadow DOM is Baseline: Chrome and Edge since version 111 (with the full attribute set standardized by 124), Firefox since 123, Safari since 16.4. If your SSR framework or web component library doesn't emit `shadowrootmode` templates yet, that's a library gap, not a browser one — and it's worth filing an issue instead of writing another skeleton loader.\n\n## The one thing worth remembering\n\nAn empty custom element with a `\u003Ctemplate>` next to it isn't broken HTML — it's HTML written for a browser that hasn't decided to help yet. Declarative Shadow DOM is the browser deciding to help: the parser does at read-time what a `\u003Cscript>` tag used to have to do at run-time, and the gap between \"the server sent HTML\" and \"the component actually rendered\" gets to close for good.\n\nDo you SSR web components today — and if so, are you shipping the skeleton-loader workaround or has your framework already picked this up? I'd like to know which libraries got there first.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fdeclarative-shadow-dom-ssr-web-components\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":47,"canonical":210,"description":211},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fdeclarative-shadow-dom-ssr-web-components","attachShadow() is a JavaScript-only API with no HTML serialization, so a server-rendered web component ships an empty host element until the client hydrates it. Declarative Shadow ","01a0499d-bb9f-709b-9e8a-a21f15ddc7ea",{"id":214,"locked":18},"01a0499d-bbc3-762d-aed6-2ef231244c7d",[216],{"id":217,"slug":46,"title":218,"_count":219},"01a0499d-bbef-7265-b700-197b471158fa","Declarative Shadow DOM",{"questions":220},8,[222],{"locale":13,"slug":46},{"id":217,"slug":46,"title":218,"_count":224,"questionCount":220},{"questions":220},[226,228,230,232,236,240,244,248,251,255,256,259],{"slug":65,"name":66,"articles":227},78,{"slug":68,"name":69,"articles":229},66,{"slug":61,"name":71,"articles":231},65,{"slug":233,"name":234,"articles":235},"css","Css",29,{"slug":237,"name":238,"articles":239},"tutorial","Tutorial",16,{"slug":241,"name":242,"articles":243},"typescript","Typescript",11,{"slug":245,"name":246,"articles":247},"performance","Performance",10,{"slug":249,"name":250,"articles":220},"react","React",{"slug":252,"name":253,"articles":254},"grammar","Grammar",6,{"slug":73,"name":74,"articles":51},{"slug":257,"name":258,"articles":51},"node","Node",{"slug":260,"name":261,"articles":262},"programming","Programming",4,{"items":264,"meta":394},[265,277,298,318,336,356,375],{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":266,"likeCount":19,"commentCount":19,"author":267,"vertical":268,"topic":269,"tags":270,"_count":275,"playground":276,"hasQuiz":17,"hasPlayground":17},28,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[271,272,273,274],{"slug":65,"name":66,"color":48},{"slug":61,"name":71,"color":48},{"slug":73,"name":74,"color":48},{"slug":68,"name":69,"color":48},{"assessments":76},{"slug":46},{"id":278,"slug":279,"title":280,"subtitle":48,"excerpt":281,"coverUrl":282,"locale":13,"readingMinutes":283,"publishedAt":284,"viewCount":285,"likeCount":19,"commentCount":19,"author":286,"vertical":287,"topic":288,"tags":291,"_count":296,"playground":297,"hasQuiz":17,"hasPlayground":17},"01a0499d-b520-778f-8286-8221ec03b8b5","nextjs-weekly-cache-components-explained","Next.js Cache Components Explained (with Cheat Sheet)","How Next.js Cache Components decide what's static, what's cached, and what streams — the use cache directive, cacheLife, and Suspense explained.","\u002Fmedia\u002Fcovers\u002Fnextjs-weekly-cache-components-explained.png",14,"2026-09-01T11:24:15.087Z",45,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":289,"name":290},"nextjs","Nextjs",[292,293,294,295],{"slug":289,"name":290,"color":48},{"slug":249,"name":250,"color":48},{"slug":65,"name":66,"color":48},{"slug":237,"name":238,"color":48},{"assessments":76},{"slug":279},{"id":299,"slug":300,"title":301,"subtitle":48,"excerpt":302,"coverUrl":303,"locale":13,"readingMinutes":51,"publishedAt":304,"viewCount":305,"likeCount":19,"commentCount":19,"author":306,"vertical":307,"topic":308,"tags":309,"_count":316,"playground":317,"hasQuiz":17,"hasPlayground":17},"01a04446-4fc4-73e1-b763-eda8c9cf631f","navigation-api-intercept-navigation","Your Router Doesn't Intercept Navigation. It Reacts To It.","Double-click a link during a slow route change and most SPA routers render two pages and settle on the wrong one — because they're listening for navigation after it already happened. The Navigation API lets you stop it before it starts.","\u002Fmedia\u002Fcovers\u002Fnavigation-api-intercept-navigation.png","2026-09-01T11:23:43.875Z",49,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[310,311,312,315],{"slug":61,"name":71,"color":48},{"slug":65,"name":66,"color":48},{"slug":313,"name":314,"color":48},"browser","Browser",{"slug":68,"name":69,"color":48},{"assessments":76},{"slug":300},{"id":319,"slug":320,"title":321,"subtitle":48,"excerpt":322,"coverUrl":323,"locale":13,"readingMinutes":283,"publishedAt":324,"viewCount":325,"likeCount":19,"commentCount":19,"author":326,"vertical":327,"topic":328,"tags":329,"_count":334,"playground":335,"hasQuiz":17,"hasPlayground":17},"019ff73a-fb5b-73ce-b725-2912167a7dde","weekly-debounce-and-throttle","Debounce and Throttle in JavaScript: The Complete Guide","Learn debounce and throttle in JavaScript step by step, with worked examples, edge cases, and a copy-paste cheat sheet for search, scroll, and resize.","\u002Fmedia\u002Fcovers\u002Fweekly-debounce-and-throttle.png","2026-08-31T13:12:14.139Z",99,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[330,331,332,333],{"slug":61,"name":71,"color":48},{"slug":65,"name":66,"color":48},{"slug":237,"name":238,"color":48},{"slug":245,"name":246,"color":48},{"assessments":76},{"slug":320},{"id":337,"slug":338,"title":339,"subtitle":48,"excerpt":340,"coverUrl":341,"locale":13,"readingMinutes":254,"publishedAt":342,"viewCount":343,"likeCount":19,"commentCount":19,"author":344,"vertical":345,"topic":346,"tags":347,"_count":354,"playground":355,"hasQuiz":17,"hasPlayground":17},"01a03cdb-3743-73ef-ab9d-21f03158ca35","server-sent-events-eventsource-live-updates","You Don't Need a WebSocket for That Live Feed","A live notification badge, a progress bar, a dashboard counter — you reached for socket.io and started hand-rolling reconnect logic. The browser already ships a simpler API that does it for you.","\u002Fmedia\u002Fcovers\u002Fserver-sent-events-eventsource-live-updates.png","2026-08-31T13:11:42.385Z",63,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[348,349,350,351],{"slug":61,"name":71,"color":48},{"slug":65,"name":66,"color":48},{"slug":237,"name":238,"color":48},{"slug":352,"name":353,"color":48},"apis","Apis",{"assessments":76},{"slug":338},{"id":357,"slug":358,"title":359,"subtitle":48,"excerpt":360,"coverUrl":361,"locale":13,"readingMinutes":254,"publishedAt":362,"viewCount":363,"likeCount":19,"commentCount":19,"author":364,"vertical":365,"topic":366,"tags":368,"_count":373,"playground":374,"hasQuiz":17,"hasPlayground":17},"01a037b3-f71a-755b-9820-9f2489daaebd","css-aspect-ratio-replace-padding-hack","Delete Your padding-bottom Aspect-Ratio Hack","For a decade the only way to keep a responsive box's shape was a padding percentage and an absolutely positioned wrapper. One CSS property replaces it — with one gotcha on real images nobody warns you about.","\u002Fmedia\u002Fcovers\u002Fcss-aspect-ratio-replace-padding-hack.png","2026-08-30T11:36:40.014Z",96,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":233,"name":367},"CSS",[369,370,371,372],{"slug":233,"name":234,"color":48},{"slug":65,"name":66,"color":48},{"slug":68,"name":69,"color":48},{"slug":237,"name":238,"color":48},{"assessments":76},{"slug":358},{"id":376,"slug":377,"title":378,"subtitle":48,"excerpt":379,"coverUrl":380,"locale":13,"readingMinutes":381,"publishedAt":382,"viewCount":383,"likeCount":19,"commentCount":19,"author":384,"vertical":385,"topic":386,"tags":387,"_count":392,"playground":393,"hasQuiz":17,"hasPlayground":17},"01a04446-373a-775b-8aaf-b72c8952123d","react-weekly-rerender-vs-remount","React Re-render vs Remount: What Actually Triggers Each","A practical guide to React re-render vs remount: what type, position, and key decide, why state resets unexpectedly, and how to force a remount.","\u002Fmedia\u002Fcovers\u002Freact-weekly-rerender-vs-remount.png",13,"2026-08-29T12:32:41.200Z",172,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":249,"name":250},[388,389,390,391],{"slug":249,"name":250,"color":48},{"slug":61,"name":71,"color":48},{"slug":237,"name":238,"color":48},{"slug":65,"name":66,"color":48},{"assessments":76},{"slug":377},{"page":76,"perPage":395,"total":227,"totalPages":396},7,12,"\u003Cdiv class=\"shj shj-lang-html shj-multiline\" data-lang=\"html\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">&lt;!-- the old way: an empty host, waiting for JS --&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">my-badge\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">my-badge\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n  customElements\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">get\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"my-badge\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">||\u003C\u002Fspan>\n    customElements\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">define\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"my-badge\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">class\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">extends\u003C\u002Fspan> \u003Cspan class=\"shj-class\">HTMLElement\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      \u003Cspan class=\"shj-func\">connectedCallback\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n        \u003Cspan class=\"shj-kwd\">this\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">attachShadow\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> mode\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"open\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>innerHTML \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\n          \u003Cspan class=\"shj-str\">`&lt;style&gt;span{color:hotpink}&lt;\u002Fstyle&gt;&lt;span&gt;New&lt;\u002Fspan&gt;`\u003C\u002Fspan>;\n      \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-html shj-multiline\" data-lang=\"html\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-cmnt\">&lt;!-- the new way: the shadow root exists the moment HTML parses --&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">my-badge\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">template\u003C\u002Fspan> \u003Cspan class=\"shj-class\">shadowrootmode\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"open\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">style\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">span\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> \u003Cspan class=\"shj-var\">color\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">hotpink\u003C\u002Fspan>; \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">style\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n    \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">span\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>New\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">span\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">template\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-var\">my-badge\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&gt;\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":400},[]]