[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"playground-declarative-shadow-dom-ssr-web-components":44,"search-suggestions":58},[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,{"slug":45,"title":46,"summary":47,"html":48,"css":49,"js":50,"config":51,"doc":53},"declarative-shadow-dom-ssr-web-components","Declarative Shadow DOM — the parse-time race",null,"\u003Cdiv class=\"pg\">\n    \u003Ch1 class=\"pg-title\">🎮 Declarative Shadow DOM: the parse-time race\u003C\u002Fh1>\n    \u003Cp class=\"pg-sub\">\n      Pick a JavaScript delay, hit run, and watch two ways of building the \"same\"\n      web component. One waits on \u003Ccode>&lt;script&gt;\u003C\u002Fcode> to attach its Shadow\n      DOM. The other's shadow root is already there — because the HTML parser\n      attached it, not JavaScript.\n    \u003C\u002Fp>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">1\u003C\u002Fspan> Controls\u003C\u002Fdiv>\n      \u003Cdiv class=\"field\">\n        \u003Clabel class=\"ctl-label\" for=\"delay\">Simulated JS delay: \u003Cspan id=\"delayLabel\" class=\"mono\">400ms\u003C\u002Fspan>\u003C\u002Flabel>\n        \u003Cinput type=\"range\" id=\"delay\" min=\"0\" max=\"3000\" step=\"100\" value=\"400\" \u002F>\n      \u003C\u002Fdiv>\n      \u003Cdiv class=\"btn-row\" style=\"margin-top:10px\">\n        \u003Cbutton class=\"btn primary\" id=\"run\">▶ Run page load\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn ghost\" id=\"reset\">Reset\u003C\u002Fbutton>\n        \u003Clabel class=\"toggle\" style=\"margin-left:6px\">\n          \u003Cinput type=\"checkbox\" id=\"neverLoads\" \u002F>\n          \u003Cspan class=\"switch\">\u003C\u002Fspan>\n          \u003Cspan>Script never loads (e.g. blocked, crashed)\u003C\u002Fspan>\n        \u003C\u002Flabel>\n      \u003C\u002Fdiv>\n      \u003Cdiv class=\"presets\" style=\"margin-top:14px\">\n        \u003Cbutton class=\"btn sm\" data-preset=\"0\">Fast connection (0ms)\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn sm\" data-preset=\"600\">Typical mobile (600ms)\u003C\u002Fbutton>\n        \u003Cbutton class=\"btn sm\" data-preset=\"2500\">Slow \u002F janky main thread (2500ms)\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n      \u003Cp class=\"hint\">The slider only controls the \u003Cstrong>imperative\u003C\u002Fstrong> panel's script. The declarative panel never reads it — that's the whole point.\u003C\u002Fp>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"panel\">\n      \u003Cdiv class=\"panel-label\">\u003Cspan class=\"n\">2\u003C\u002Fspan> Result — two \"SSR'd\" components, loaded at the same time\u003C\u002Fdiv>\n      \u003Cdiv class=\"split\">\n        \u003Cdiv class=\"frame-box\">\n          \u003Cdiv class=\"frame-head\">\n            Imperative \u003Cspan class=\"tag\">attachShadow() in &lt;script&gt;\u003C\u002Fspan>\n          \u003C\u002Fdiv>\n          \u003Ciframe class=\"demo-frame\" id=\"frameImperative\" title=\"imperative shadow dom demo\">\u003C\u002Fiframe>\n          \u003Cdiv class=\"frame-status\">\n            \u003Cspan class=\"dot\" id=\"dotImperative\">\u003C\u002Fspan>\n            \u003Cspan>Shadow content visible at: \u003Cspan class=\"ms\" id=\"msImperative\">—\u003C\u002Fspan>\u003C\u002Fspan>\n          \u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n        \u003Cdiv class=\"frame-box\">\n          \u003Cdiv class=\"frame-head\">\n            Declarative \u003Cspan class=\"tag\">&lt;template shadowrootmode&gt;\u003C\u002Fspan>\n          \u003C\u002Fdiv>\n          \u003Ciframe class=\"demo-frame\" id=\"frameDeclarative\" title=\"declarative shadow dom demo\">\u003C\u002Fiframe>\n          \u003Cdiv class=\"frame-status\">\n            \u003Cspan class=\"dot\" id=\"dotDeclarative\">\u003C\u002Fspan>\n            \u003Cspan>Shadow content visible at: \u003Cspan class=\"ms\" id=\"msDeclarative\">—\u003C\u002Fspan>\u003C\u002Fspan>\n          \u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n      \u003C\u002Fdiv>\n      \u003Cp class=\"hint\" id=\"verdict\" style=\"margin-top:12px\">Set a delay and click \"Run page load\" to start the race.\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n\n\u003Cscript>\n  \"use strict\";\n  const $ = (id) => document.getElementById(id);\n\n  const delayInput = $(\"delay\");\n  const delayLabel = $(\"delayLabel\");\n  const neverLoads = $(\"neverLoads\");\n\n  delayInput.addEventListener(\"input\", () => {\n    delayLabel.textContent = delayInput.value + \"ms\";\n  });\n\n  document.querySelectorAll(\"[data-preset]\").forEach((btn) => {\n    btn.addEventListener(\"click\", () => {\n      neverLoads.checked = false;\n      delayInput.value = btn.dataset.preset;\n      delayLabel.textContent = btn.dataset.preset + \"ms\";\n    });\n  });\n\n  function imperativeSrcdoc(delayMs, skip) {\n    return `\u003C!doctype html>\u003Chtml>\u003Chead>\u003C\u002Fhead>\u003Cbody>\n      \u003Cdiv id=\"host\" class=\"card\">waiting for JavaScript…\u003C\u002Fdiv>\n      \u003Cscript>\n        ${skip ? \"\u002F\u002F script intentionally never runs\" : `\n        setTimeout(function () {\n          var host = document.getElementById(\"host\");\n          host.textContent = \"\";\n          var root = host.attachShadow({ mode: \"open\" });\n          root.innerHTML = '\u003Cspan>👋 shadow content, attached by JS\u003C\u002Fspan>';\n          parent.postMessage({ kind: \"imperative\", ms: ${delayMs} }, \"*\");\n        }, ${delayMs});\n        `}\n      \u003C\\\u002Fscript>","\u002F*\n * Shared playground design system — daily-post\n * ============================================\n * Link it from any post's playground\u002Findex.html (no per-post boilerplate):\n *\n *   \u003Clink rel=\"stylesheet\" href=\"..\u002F..\u002F..\u002Fplayground\u002Fassets\u002Fbase.css\" \u002F>\n *\n * The relative path resolves the same locally (posts\u002F\u003Cbase>\u002Fplayground\u002F) and on\n * the deployed host (public\u002Fposts\u002F\u003Cbase>\u002Fplayground\u002F), because build-site.js\n * mirrors this file to public\u002Fplayground\u002Fassets\u002Fbase.css. For a CodeSandbox\n * --embed, playground.js inlines this file so the single-file sandbox still styles.\n *\n * Then write ONLY the demo-specific CSS in your inline \u003Cstyle>. Everything below\n * — tokens, layout, panels, buttons, toggles, sliders, code blocks, status\u002Flog,\n * banners, presets — comes for free and looks consistent across every post.\n *\n * Theme-aware: dark by default, light via prefers-color-scheme AND a\n * [data-theme] override on \u003Chtml> (so a toggle can force either). Accessible:\n * real focus rings, adequate contrast, reduced-motion honored.\n *\u002F\n\n\u002F* ---------- design tokens ---------- *\u002F\n:root {\n  --bg: #0d1117;\n  --bg-grid: #ffffff0a;\n  --panel: #161b22;\n  --panel-2: #0b0e14;\n  --card: #0b0e14;\n  --ink: #e8ebf2;\n  --muted: #9aa4b2;\n  --faint: #6b7688;\n  --line: #232a35;\n  --line-soft: #ffffff12;\n  --accent: #7c9cff;\n  --accent-ink: #0b0e14;\n  --good: #3fb950;\n  --warn: #f0883e;\n  --bad: #f85149;\n  --info: #58a6ff;\n  --radius: 14px;\n  --radius-sm: 9px;\n  --shadow: 0 8px 30px #0006;\n  --shadow-sm: 0 4px 14px #0005;\n  --mono: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n  --sans: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", sans-serif;\n  color-scheme: dark;\n}\n\n\u002F* Light theme — system preference, overridable by [data-theme] on \u003Chtml>. *\u002F\n@media (prefers-color-scheme: light) {\n  :root:not([data-theme=\"dark\"]) {\n    --bg: #f4f6fb;\n    --bg-grid: #00000008;\n    --panel: #ffffff;\n    --panel-2: #f0f3f9;\n    --card: #f7f9fc;\n    --ink: #131722;\n    --muted: #5b6472;\n    --faint: #8a93a3;\n    --line: #e2e7f0;\n    --line-soft: #00000010;\n    --accent: #2f6df0;\n    --accent-ink: #ffffff;\n    --good: #1a7f37;\n    --warn: #bc4c00;\n    --bad: #cf222e;\n    --info: #0969da;\n    --shadow: 0 8px 30px #0000001f;\n    --shadow-sm: 0 4px 14px #00000014;\n    color-scheme: light;\n  }\n}\n:root[data-theme=\"light\"] {\n  --bg: #f4f6fb;\n  --bg-grid: #00000008;\n  --panel: #ffffff;\n  --panel-2: #f0f3f9;\n  --card: #f7f9fc;\n  --ink: #131722;\n  --muted: #5b6472;\n  --faint: #8a93a3;\n  --line: #e2e7f0;\n  --line-soft: #00000010;\n  --accent: #2f6df0;\n  --accent-ink: #ffffff;\n  --good: #1a7f37;\n  --warn: #bc4c00;\n  --bad: #cf222e;\n  --info: #0969da;\n  --shadow: 0 8px 30px #0000001f;\n  --shadow-sm: 0 4px 14px #00000014;\n  color-scheme: light;\n}\n\n\u002F* ---------- reset & base ---------- *\u002F\n*, *::before, *::after { box-sizing: border-box; }\n\nbody {\n  margin: 0;\n  min-height: 100vh;\n  font-family: var(--sans);\n  color: var(--ink);\n  background:\n    radial-gradient(circle at 1px 1px, var(--bg-grid) 1px, transparent 0) 0 0 \u002F 24px 24px,\n    var(--bg);\n  display: flex;\n  justify-content: center;\n  align-items: flex-start;\n  padding: clamp(14px, 3vw, 26px);\n  line-height: 1.55;\n  -webkit-font-smoothing: antialiased;\n  text-rendering: optimizeLegibility;\n}\n\n\u002F* The single content column. *\u002F\n.pg {\n  width: 100%;\n  max-width: 860px;\n}\n\n\u002F* ---------- header ---------- *\u002F\n.pg-title {\n  font-size: clamp(1.15rem, 1rem + 1vw, 1.4rem);\n  font-weight: 800;\n  letter-spacing: -0.02em;\n  margin: 0 0 4px;\n  display: flex;\n  align-items: center;\n  gap: 9px;\n}\n.pg-sub {\n  color: var(--muted);\n  font-size: 0.92rem;\n  line-height: 1.5;\n  margin: 0 0 16px;\n  max-width: 66ch;\n}\n.pg-sub strong, .pg-sub b { color: var(--ink); font-weight: 700; }\n.pg-sub code { font-family: var(--mono); background: var(--line-soft); color: var(--accent);\n  padding: 1px 6px; border-radius: 6px; font-size: 0.85em; }\n\n\u002F* ---------- panels ---------- *\u002F\n.panel {\n  background: var(--panel);\n  border: 1px solid var(--line);\n  border-radius: var(--radius);\n  padding: 16px 18px;\n  margin-bottom: 14px;\n  box-shadow: var(--shadow-sm);\n}\n.panel-label {\n  font-size: 0.7rem;\n  text-transform: uppercase;\n  letter-spacing: 0.08em;\n  color: var(--muted);\n  font-weight: 800;\n  margin: 0 0 13px;\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n.panel-label .n {\n  display: inline-grid; place-items: center;\n  width: 18px; height: 18px; border-radius: 50%;\n  background: var(--accent); color: var(--accent-ink);\n  font-size: 0.66rem; font-weight: 800;\n}\n\n\u002F* ---------- layout helpers ---------- *\u002F\n.row { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }\n.row.between { justify-content: space-between; }\n.col { display: flex; flex-direction: column; gap: 10px; }\n.grow { flex: 1 1 0; min-width: 0; }\n.wrap-controls { display: flex; flex-wrap: wrap; gap: 16px 24px; align-items: flex-end; }\n.field { display: flex; flex-direction: column; gap: 7px; min-width: 0; }\n.field > .label, .ctl-label {\n  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em;\n  color: var(--muted); font-weight: 800;\n}\n\n\u002F* ---------- buttons ---------- *\u002F\n.btn {\n  font: inherit; font-weight: 650; font-size: 0.88rem;\n  cursor: pointer; user-select: none;\n  border: 1px solid var(--line);\n  background: var(--card); color: var(--ink);\n  padding: 9px 15px; border-radius: var(--radius-sm);\n  display: inline-flex; align-items: center; gap: 8px;\n  transition: background .15s ease, border-color .15s ease, transform .05s ease, color .15s ease;\n}\n.btn:hover { background: var(--line-soft); border-color: var(--muted); }\n.btn:active { transform: translateY(1px); }\n.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n.btn[disabled] { opacity: 0.45; cursor: not-allowed; }\n.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }\n.btn.primary:hover { filter: brightness(1.08); background: var(--accent); }\n.btn.good { background: var(--good); border-color: var(--good); color: #04140a; }\n.btn.bad  { background: var(--bad);  border-color: var(--bad);  color: #fff; }\n.btn.ghost { background: transparent; }\n.btn.sm { padding: 6px 11px; font-size: 0.82rem; }\n\n.btn-row { display: flex; flex-wrap: wrap; gap: 9px; }\n\n\u002F* Segmented control — a row of buttons where one is active (aria-pressed). *\u002F\n.seg { display: inline-flex; background: var(--card); border: 1px solid var(--line);\n  border-radius: var(--radius-sm); padding: 3px; gap: 3px; }\n.seg button {\n  font: inherit; font-weight: 650; font-size: 0.84rem; cursor: pointer;\n  border: none; background: transparent; color: var(--muted);\n  padding: 7px 13px; border-radius: 7px;\n  transition: background .15s ease, color .15s ease;\n}\n.seg button:hover { color: var(--ink); }\n.seg button[aria-pressed=\"true\"] { background: var(--accent); color: var(--accent-ink); }\n.seg button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n\n\u002F* Preset chips — worked-example shortcuts. *\u002F\n.presets { display: flex; flex-wrap: wrap; gap: 8px; }\n.presets .btn { background: var(--panel-2); }\n\n\u002F* ---------- toggle switch ---------- *\u002F\n.toggle { display: inline-flex; align-items: center; gap: 10px; cursor: pointer;\n  font-size: 0.88rem; font-weight: 600; user-select: none; }\n.toggle input { position: absolute; opacity: 0; width: 0; height: 0; }\n.switch { position: relative; width: 44px; height: 25px; border-radius: 999px; flex: none;\n  background: var(--line); border: 1px solid var(--line); transition: background .18s ease; }\n.switch::after { content: \"\"; position: absolute; top: 2px; left: 2px; width: 19px; height: 19px;\n  border-radius: 50%; background: #cfd6e4; transition: transform .18s ease; box-shadow: 0 1px 3px #0006; }\n.toggle input:checked + .switch { background: var(--good); border-color: var(--good); }\n.toggle input:checked + .switch::after { transform: translateX(19px); background: #fff; }\n.toggle input:focus-visible + .switch { outline: 2px solid var(--accent); outline-offset: 2px; }\n\n\u002F* ---------- range slider ---------- *\u002F\ninput[type=\"range\"] {\n  -webkit-appearance: none; appearance: none; width: 100%; height: 6px;\n  background: var(--line); border-radius: 999px; outline: none; cursor: pointer; margin: 8px 0;\n}\ninput[type=\"range\"]::-webkit-slider-thumb {\n  -webkit-appearance: none; appearance: none; width: 19px; height: 19px; border-radius: 50%;\n  background: var(--accent); border: 2px solid var(--panel); box-shadow: var(--shadow-sm); cursor: pointer;\n}\ninput[type=\"range\"]::-moz-range-thumb {\n  width: 19px; height: 19px; border-radius: 50%; background: var(--accent);\n  border: 2px solid var(--panel); box-shadow: var(--shadow-sm); cursor: pointer;\n}\ninput[type=\"range\"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }\n\n\u002F* ---------- text inputs \u002F selects ---------- *\u002F\ninput[type=\"text\"], input[type=\"number\"], input[type=\"search\"], select, textarea {\n  font: inherit; color: var(--ink); background: var(--card);\n  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 8px 11px; width: 100%;\n}\ntextarea { font-family: var(--mono); font-size: 0.85rem; line-height: 1.6; resize: vertical; }\ninput:focus-visible, select:focus-visible, textarea:focus-visible {\n  outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent);\n}\n\n\u002F* ---------- status \u002F output \u002F log ---------- *\u002F\n.status {\n  display: flex; align-items: center; gap: 9px; flex-wrap: wrap;\n  font-size: 0.9rem; font-weight: 600;\n  padding: 11px 14px; border-radius: var(--radius-sm);\n  background: var(--panel-2); border: 1px solid var(--line); color: var(--ink);\n}\n.status.good { border-color: color-mix(in srgb, var(--good) 55%, var(--line)); }\n.status.warn { border-color: color-mix(in srgb, var(--warn) 55%, var(--line)); }\n.status.bad  { border-color: color-mix(in srgb, var(--bad) 55%, var(--line)); }\n.dot { width: 9px; height: 9px; border-radius: 50%; background: var(--muted); flex: none; }\n.dot.good { background: var(--good); } .dot.warn { background: var(--warn); }\n.dot.bad { background: var(--bad); } .dot.info { background: var(--info); }\n\n.output { background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius-sm);\n  padding: 13px 15px; }\n.log { font-family: var(--mono); font-size: 0.8rem; line-height: 1.7; background: var(--card);\n  border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 11px 14px;\n  max-height: 220px; overflow-y: auto; white-space: pre-wrap; }\n\n\u002F* ---------- code block + syntax tokens ---------- *\u002F\n.code {\n  font-family: var(--mono); font-size: 0.82rem; line-height: 1.7;\n  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--radius-sm);\n  padding: 13px 15px; overflow-x: auto; white-space: pre; tab-size: 2;\n}\n.code .tok-sel  { color: var(--accent); }\n.code .tok-prop { color: #86e29b; }\n.code .tok-kw   { color: #ff9bcb; }\n.code .tok-str  { color: #f0c674; }\n.code .tok-num  { color: #f0883e; }\n.code .tok-cmt  { color: var(--faint); font-style: italic; }\n.code .hl { background: color-mix(in srgb, var(--good) 22%, transparent); border-radius: 4px; padding: 0 3px; }\n\n\u002F* ---------- hint \u002F caption ---------- *\u002F\n.hint { font-size: 0.83rem; color: var(--muted); line-height: 1.55; margin: 10px 0 0; }\n.hint code { font-family: var(--mono); color: var(--ink); background: var(--line-soft);\n  padding: 1px 5px; border-radius: 5px; font-size: 0.86em; }\n.hint strong { color: var(--ink); }\n\n\u002F* ---------- banner (feature-support \u002F notices) ---------- *\u002F\n.banner {\n  display: none; margin-bottom: 14px; padding: 11px 14px; border-radius: 11px;\n  font-size: 0.85rem; line-height: 1.5;\n  color: var(--ink); background: color-mix(in srgb, var(--warn) 12%, var(--panel));\n  border: 1px solid color-mix(in srgb, var(--warn) 45%, var(--line));\n}\n.banner.show { display: block; }\n.banner.info { background: color-mix(in srgb, var(--info) 12%, var(--panel));\n  border-color: color-mix(in srgb, var(--info) 45%, var(--line)); }\n.banner code { font-family: var(--mono); background: var(--line-soft); padding: 1px 5px; border-radius: 5px; }\n\n\u002F* ---------- misc atoms ---------- *\u002F\n.pill { display: inline-flex; align-items: center; gap: 6px; font-size: 0.75rem; font-weight: 700;\n  padding: 3px 9px; border-radius: 999px; background: var(--line-soft); color: var(--muted); }\n.pill.good { color: var(--good); background: color-mix(in srgb, var(--good) 16%, transparent); }\n.pill.bad  { color: var(--bad);  background: color-mix(in srgb, var(--bad) 16%, transparent); }\n.mono { font-family: var(--mono); }\n.muted { color: var(--muted); }\n.big-num { font-family: var(--mono); font-size: 1.6rem; font-weight: 800; letter-spacing: -0.02em; }\n\n\u002F* Optional theme toggle button (top-right). Wire it in JS if you want it. *\u002F\n.theme-btn { position: fixed; top: 12px; right: 12px; z-index: 20; }\n\n\u002F* ---------- responsive ---------- *\u002F\n@media (max-width: 560px) {\n  .panel { padding: 14px; }\n  .wrap-controls { gap: 14px; }\n  .btn { padding: 9px 13px; }\n}\n\n\u002F* ---------- reduced motion ---------- *\u002F\n@media (prefers-reduced-motion: reduce) {\n  *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }\n}\n\n\n.split {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: 14px;\n    margin-top: 14px;\n  }\n  @media (max-width: 640px) {\n    .split { grid-template-columns: 1fr; }\n  }\n  .frame-box {\n    border: 1px solid var(--line);\n    border-radius: var(--radius-sm);\n    background: var(--panel-2);\n    overflow: hidden;\n  }\n  .frame-head {\n    padding: 8px 12px;\n    font-size: 0.8rem;\n    font-weight: 600;\n    letter-spacing: 0.02em;\n    border-bottom: 1px solid var(--line);\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n  }\n  .frame-head .tag {\n    font-weight: 500;\n    font-size: 0.7rem;\n    opacity: 0.7;\n  }\n  iframe.demo-frame {\n    width: 100%;\n    height: 130px;\n    border: 0;\n    display: block;\n    background: #0b0f14;\n  }\n  .frame-status {\n    padding: 8px 12px;\n    font-size: 0.82rem;\n    border-top: 1px solid var(--line);\n    display: flex;\n    align-items: center;\n    gap: 8px;\n  }\n  .ms {\n    font-variant-numeric: tabular-nums;\n    font-weight: 700;\n  }\n\nbody{margin:0;font:14px\u002F1.4 -apple-system,sans-serif;background:#0b0f14;color:#e7edf3;\n        display:flex;align-items:center;justify-content:center;height:130px}\n      .card{padding:10px 16px;border-radius:8px;background:#161c24}\n\nspan{color:#ff6ec7;font-weight:700}\n\nbody{margin:0;font:14px\u002F1.4 -apple-system,sans-serif;background:#0b0f14;color:#e7edf3;\n        display:flex;align-items:center;justify-content:center;height:130px}\n      .card{padding:10px 16px;border-radius:8px;background:#161c24}\n\nspan{color:#7cf7c4;font-weight:700}","\"use strict\";\n  const $ = (id) => document.getElementById(id);\n\n  const delayInput = $(\"delay\");\n  const delayLabel = $(\"delayLabel\");\n  const neverLoads = $(\"neverLoads\");\n\n  delayInput.addEventListener(\"input\", () => {\n    delayLabel.textContent = delayInput.value + \"ms\";\n  });\n\n  document.querySelectorAll(\"[data-preset]\").forEach((btn) => {\n    btn.addEventListener(\"click\", () => {\n      neverLoads.checked = false;\n      delayInput.value = btn.dataset.preset;\n      delayLabel.textContent = btn.dataset.preset + \"ms\";\n    });\n  });\n\n  function imperativeSrcdoc(delayMs, skip) {\n    return `\u003C!doctype html>\u003Chtml>\u003Chead>\u003Cstyle>\n      body{margin:0;font:14px\u002F1.4 -apple-system,sans-serif;background:#0b0f14;color:#e7edf3;\n        display:flex;align-items:center;justify-content:center;height:130px}\n      .card{padding:10px 16px;border-radius:8px;background:#161c24}\n    \u003C\u002Fstyle>\u003C\u002Fhead>\u003Cbody>\n      \u003Cdiv id=\"host\" class=\"card\">waiting for JavaScript…\u003C\u002Fdiv>\n      \u003Cscript>\n        ${skip ? \"\u002F\u002F script intentionally never runs\" : `\n        setTimeout(function () {\n          var host = document.getElementById(\"host\");\n          host.textContent = \"\";\n          var root = host.attachShadow({ mode: \"open\" });\n          root.innerHTML = '\u003Cstyle>span{color:#ff6ec7;font-weight:700}\u003C\u002Fstyle>\u003Cspan>👋 shadow content, attached by JS\u003C\u002Fspan>';\n          parent.postMessage({ kind: \"imperative\", ms: ${delayMs} }, \"*\");\n        }, ${delayMs});\n        `}\n      \u003C\\\u002Fscript>\n    \u003C\u002Fbody>\u003C\u002Fhtml>`;\n  }\n\n  function declarativeSrcdoc() {\n    return `\u003C!doctype html>\u003Chtml>\u003Chead>\u003Cstyle>\n      body{margin:0;font:14px\u002F1.4 -apple-system,sans-serif;background:#0b0f14;color:#e7edf3;\n        display:flex;align-items:center;justify-content:center;height:130px}\n      .card{padding:10px 16px;border-radius:8px;background:#161c24}\n    \u003C\u002Fstyle>\u003C\u002Fhead>\u003Cbody>\n      \u003Cdiv id=\"host\" class=\"card\">\n        \u003Ctemplate shadowrootmode=\"open\">\n          \u003Cstyle>span{color:#7cf7c4;font-weight:700}\u003C\u002Fstyle>\n          \u003Cspan>👋 shadow content, attached by the parser\u003C\u002Fspan>\n        \u003C\u002Ftemplate>\n      \u003C\u002Fdiv>\n      \u003Cscript>\n        \u002F\u002F No delay here on purpose: this runs the instant the parser reaches it,\n        \u002F\u002F and by then the shadow root above already exists. We're only using\n        \u002F\u002F this script to REPORT that fact, not to create it.\n        var host = document.getElementById(\"host\");\n        parent.postMessage({ kind: \"declarative\", ms: 0, attached: !!host.shadowRoot }, \"*\");\n      \u003C\\\u002Fscript>\n    \u003C\u002Fbody>\u003C\u002Fhtml>`;\n  }\n\n  let startTime = 0;\n  let resultsIn = 0;\n\n  window.addEventListener(\"message\", (event) => {\n    const data = event.data;\n    if (!data || !data.kind) return;\n    const elapsed = Math.round(performance.now() - startTime);\n\n    if (data.kind === \"imperative\") {\n      $(\"msImperative\").textContent = elapsed + \"ms\";\n      $(\"dotImperative\").className = \"dot warn\";\n      resultsIn++;\n    } else if (data.kind === \"declarative\") {\n      $(\"msDeclarative\").textContent = elapsed + \"ms\";\n      $(\"dotDeclarative\").className = \"dot good\";\n      resultsIn++;\n    }\n    checkDone();\n  });\n\n  function checkDone() {\n    if (resultsIn >= 1 && $(\"msDeclarative\").textContent !== \"—\") {\n      const impText = $(\"msImperative\").textContent;\n      if (impText === \"—\" && neverLoads.checked) {\n        $(\"verdict\").textContent =\n          \"The declarative panel rendered without waiting on anything. The imperative panel is stuck forever — its content only ever existed as an instruction for a script that never ran.\";\n      } else if (impText !== \"—\") {\n        $(\"verdict\").textContent =\n          \"Same HTML sent at the same time. The declarative shadow root was already attached before either \u003Cscript> ran — the imperative one had to wait out the full delay you set.\";\n      }\n    }\n  }\n\n  $(\"run\").addEventListener(\"click\", () => {\n    $(\"msImperative\").textContent = \"—\";\n    $(\"msDeclarative\").textContent = \"—\";\n    $(\"dotImperative\").className = \"dot\";\n    $(\"dotDeclarative\").className = \"dot\";\n    $(\"verdict\").textContent = \"Running…\";\n    resultsIn = 0;\n\n    const delayMs = Number(delayInput.value);\n    startTime = performance.now();\n    $(\"frameImperative\").srcdoc = imperativeSrcdoc(delayMs, neverLoads.checked);\n    $(\"frameDeclarative\").srcdoc = declarativeSrcdoc();\n  });\n\n  $(\"reset\").addEventListener(\"click\", () => {\n    delayInput.value = 400;\n    delayLabel.textContent = \"400ms\";\n    neverLoads.checked = false;\n    $(\"msImperative\").textContent = \"—\";\n    $(\"msDeclarative\").textContent = \"—\";\n    $(\"dotImperative\").className = \"dot\";\n    $(\"dotDeclarative\").className = \"dot\";\n    $(\"verdict\").textContent = \"Set a delay and click \\\"Run page load\\\" to start the race.\";\n    $(\"frameImperative\").srcdoc = \"\";\n    $(\"frameDeclarative\").srcdoc = \"\";\n  });\n\n  \u002F\u002F Prime an initial run so the demo isn't blank on load.\n  $(\"run\").click();",{"height":52,"autorun":17},520,{"slug":45,"title":54,"excerpt":55,"coverUrl":56,"vertical":57},"Shadow DOM Needed JavaScript to Exist. Now It Doesn't.","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",{"slug":6},[59,63,67,71,75,79,83,87,91,95,99,102],{"slug":60,"name":61,"articles":62},"webdev","Webdev",78,{"slug":64,"name":65,"articles":66},"frontend","Frontend",66,{"slug":68,"name":69,"articles":70},"javascript","Javascript",65,{"slug":72,"name":73,"articles":74},"css","Css",29,{"slug":76,"name":77,"articles":78},"tutorial","Tutorial",16,{"slug":80,"name":81,"articles":82},"typescript","Typescript",11,{"slug":84,"name":85,"articles":86},"performance","Performance",10,{"slug":88,"name":89,"articles":90},"react","React",8,{"slug":92,"name":93,"articles":94},"grammar","Grammar",6,{"slug":96,"name":97,"articles":98},"html","Html",5,{"slug":100,"name":101,"articles":98},"node","Node",{"slug":103,"name":104,"articles":105},"programming","Programming",4]