[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-elementinternals-form-associated-custom-elements":44,"search-suggestions":210,"related-elementinternals-form-associated-custom-elements":252,"code:html:true:1k3bwrd":384,"code:js:true:11mmpqw":385,"code:js:true:1vi5skl":386,"comments-01a08af7-9462-760d-89bf-e4ef3c77bb6a":387},[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":193,"seo":194,"translationGroupId":197,"series":48,"podcastUrl":48,"verticalId":5,"thread":198,"assessments":200,"translations":206,"quiz":208},"01a08af7-9462-760d-89bf-e4ef3c77bb6a","elementinternals-form-associated-custom-elements","The Star Rating That Never Reaches Your Server",null,"A custom \u003Cstar-rating> element sits inside a \u003Cform>, looks like a form field, and gets left out of the submission entirely. The browser has a real fix for this — most teams reach for a hack instead.","\u002Fmedia\u002Fcovers\u002Felementinternals-form-associated-custom-elements.png",4,"2026-09-15T11:25:41.972Z",31,{"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,66,69,72],{"slug":61,"name":65,"color":48},"Javascript",{"slug":67,"name":68,"color":48},"webcomponents","Web Components",{"slug":70,"name":71,"color":48},"forms","Forms",{"slug":73,"name":74,"color":48},"webdev","Webdev",{"assessments":76},1,{"slug":46,"title":78},"ElementInternals — interactive playground",{"blocks":80,"version":76},[81,85,88,93,96,99,102,105,108,111,114,117,120,126,131,134,137,140,143,147,150,153,156,159,162,165,168,171,174,177,180,183,186],{"id":82,"html":83,"type":84},"b1","\u003Cp>You build a \u003Ccode>&lt;star-rating&gt;\u003C\u002Fcode> custom element. Five clickable stars, a nice hover state, a \u003Ccode>value\u003C\u002Fcode> property. You drop it into a signup form next to the real inputs. It looks identical to everything around it.\u003C\u002Fp>","paragraph",{"id":86,"html":87,"type":84},"b2","\u003Cp>You submit the form. \u003Ccode>FormData\u003C\u002Fcode> comes back with the email, the password, the checkbox — and no rating. Not empty. Missing. As far as the \u003Ccode>&lt;form&gt;\u003C\u002Fcode> is concerned, your custom element was never there.\u003C\u002Fp>",{"id":89,"html":90,"text":91,"type":92,"level":31},"b3","The form doesn&#39;t see what you think it sees","The form doesn't see what you think it sees","heading",{"id":94,"html":95,"type":84},"b4","\u003Cp>A \u003Ccode>&lt;form&gt;\u003C\u002Fcode> only collects values from elements it recognizes as form controls. That&#39;s a fixed list baked into the platform: \u003Ccode>&lt;input&gt;\u003C\u002Fcode>, \u003Ccode>&lt;select&gt;\u003C\u002Fcode>, \u003Ccode>&lt;textarea&gt;\u003C\u002Fcode>, \u003Ccode>&lt;button&gt;\u003C\u002Fcode>, a handful of others. A custom element — no matter how convincingly it renders, no matter what you name its property — isn&#39;t on that list. It can have a \u003Ccode>name\u003C\u002Fcode> attribute. It can have a \u003Ccode>value\u003C\u002Fcode> getter. The form still walks past it like it&#39;s a \u003Ccode>&lt;div&gt;\u003C\u002Fcode>, because as far as form submission is concerned, it is one.\u003C\u002Fp>",{"id":97,"html":98,"type":84},"b5","\u003Cp>Same story for the rest of the form contract you got for free with \u003Ccode>&lt;input&gt;\u003C\u002Fcode>: \u003Ccode>&lt;label for=&quot;rating&quot;&gt;\u003C\u002Fcode> won&#39;t focus it, \u003Ccode>required\u003C\u002Fcode> won&#39;t block submission on it, \u003Ccode>:invalid\u003C\u002Fcode> won&#39;t style it, \u003Ccode>form.reset()\u003C\u002Fcode> won&#39;t clear it. None of that is wired up — there was never a mechanism for a custom element to opt in.\u003C\u002Fp>",{"id":100,"html":101,"type":84},"b6","\u003C!-- playground:start -->",{"id":103,"html":104,"text":104,"type":92,"level":31},"b7","🎮 Try it yourself",{"id":106,"html":107,"type":84},"b8","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Felementinternals-form-associated-custom-elements\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":109,"html":110,"type":84},"b9","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":112,"html":113,"type":84},"b10","\u003C!-- playground:end -->",{"id":115,"html":116,"text":116,"type":92,"level":31},"b11","The fix everyone reaches for first",{"id":118,"html":119,"type":84},"b12","\u003Cp>The obvious move is a hidden mirror:\u003C\u002Fp>",{"id":121,"code":122,"type":123,"language":124,"highlight":125},"b13","\u003Cstar-rating id=\"stars\">\u003C\u002Fstar-rating>\n\u003Cinput type=\"hidden\" name=\"rating\" id=\"rating-mirror\" \u002F>","code","html",[],{"id":127,"code":128,"type":123,"language":129,"highlight":130},"b14","stars.addEventListener(\"change\", () => {\n  ratingMirror.value = stars.value;\n});","js",[],{"id":132,"html":133,"type":84},"b15","\u003Cp>This works, in the sense that a value now shows up in \u003Ccode>FormData\u003C\u002Fcode>. It also means you&#39;re maintaining a second element whose only job is being real where the first one is fake. Every place the rating can change — clicking a star, a keyboard shortcut, a preset button — needs to remember to update the mirror too, or the two drift apart silently.\u003C\u002Fp>",{"id":135,"html":136,"type":84},"b16","\u003Cp>\u003Ccode>required\u003C\u002Fcode> looks like the next box to tick, so someone adds it to the hidden input. It does nothing. \u003Ccode>&lt;input type=&quot;hidden&quot;&gt;\u003C\u002Fcode> is explicitly excluded from constraint validation — the spec calls it &quot;barred from constraint validation&quot; — so a required hidden field is always considered valid, full stop. What actually ships instead is a hand-rolled check bolted onto the submit handler: \u003Ccode>if (!ratingMirror.value) event.preventDefault()\u003C\u002Fcode>. That&#39;s \u003Ccode>required\u003C\u002Fcode>, reimplemented in application code, with no \u003Ccode>:invalid\u003C\u002Fcode> styling and no native &quot;please fill this out&quot; message — because the one place that logic could have lived natively was never a candidate for it. \u003Ccode>form.reset()\u003C\u002Fcode> clears the hidden input&#39;s value but not the visible stars, so that&#39;s another listener to write by hand. Every native behavior you&#39;re missing gets re-implemented as its own event listener, on its own element, that has to stay in sync with the one the user actually sees.\u003C\u002Fp>",{"id":138,"html":139,"text":139,"type":92,"level":31},"b17","The part of the platform built for exactly this",{"id":141,"html":142,"type":84},"b18","\u003Cp>Custom elements have been able to register as real form participants since \u003Ccode>ElementInternals\u003C\u002Fcode> shipped — no hidden input, no manual sync:\u003C\u002Fp>",{"id":144,"code":145,"type":123,"language":129,"highlight":146},"b19","class StarRating extends HTMLElement {\n  static formAssociated = true; \u002F\u002F opts into the form-control contract\n\n  constructor() {\n    super();\n    this._internals = this.attachInternals();\n    this._value = 0;\n  }\n\n  set value(v) {\n    this._value = v;\n    this._internals.setFormValue(String(v)); \u002F\u002F this is what FormData reads now\n    this._internals.setValidity(\n      this.required && v === 0 ? { valueMissing: true } : {},\n      \"Pick a rating.\",\n      this \u002F\u002F anchor for the native validation bubble\n    );\n  }\n\n  get value() {\n    return this._value;\n  }\n\n  formResetCallback() {\n    this.value = 0; \u002F\u002F form.reset() now clears the widget for free\n  }\n\n  formDisabledCallback(disabled) {\n    this.toggleAttribute(\"aria-disabled\", disabled);\n  }\n}\ncustomElements.define(\"star-rating\", StarRating);",[],{"id":148,"html":149,"type":84},"b20","\u003Cp>\u003Ccode>attachInternals()\u003C\u002Fcode> hands back an \u003Ccode>ElementInternals\u003C\u002Fcode> object that&#39;s the element&#39;s private line to the form machinery — \u003Ccode>setFormValue()\u003C\u002Fcode> is what \u003Ccode>FormData\u003C\u002Fcode> actually reads, and it&#39;s the only line in this class that touches submission. \u003Ccode>setValidity()\u003C\u002Fcode> plugs into the same constraint-validation system \u003Ccode>required\u003C\u002Fcode> and \u003Ccode>:invalid\u003C\u002Fcode> already use, so \u003Ccode>form.checkValidity()\u003C\u002Fcode> and \u003Ccode>reportValidity()\u003C\u002Fcode> see this element exactly like a native input. \u003Ccode>formResetCallback()\u003C\u002Fcode> and \u003Ccode>formDisabledCallback()\u003C\u002Fcode> are lifecycle hooks the browser calls on its own — no listener to attach, no sync to forget.\u003C\u002Fp>",{"id":151,"html":152,"type":84},"b21","\u003Cp>The hidden-input version and this version produce the same \u003Ccode>FormData\u003C\u002Fcode> on a good day. They stop matching the moment something goes wrong: an empty required field, a form reset mid-interaction, a disabled fieldset. That&#39;s exactly the code the mirror-input hack never got right, because nothing forced it to.\u003C\u002Fp>",{"id":154,"html":155,"type":84},"b22","\u003C!-- quiz:start -->",{"id":157,"html":158,"text":158,"type":92,"level":31},"b23","🧠 Test yourself",{"id":160,"html":161,"type":84},"b24","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Felementinternals-form-associated-custom-elements\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":163,"html":164,"type":84},"b25","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":166,"html":167,"type":84},"b26","\u003C!-- quiz:end -->",{"id":169,"html":170,"text":170,"type":92,"level":31},"b27","What this buys you",{"id":172,"html":173,"type":84},"b28","\u003Cp>This isn&#39;t just less code — it&#39;s less code that lies about being finished. The hidden-input version \u003Cem>looks\u003C\u002Fem> like a form field to a skim-read, right up until QA resets the form or a screen reader user tabs to the \u003Ccode>&lt;label&gt;\u003C\u002Fcode> expecting it to do something. \u003Ccode>ElementInternals\u003C\u002Fcode> doesn&#39;t make custom elements harder to write; it makes them stop pretending to be inputs and actually become one, using the exact validation and submission pipeline the browser already runs for everything else on the page.\u003C\u002Fp>",{"id":175,"html":176,"type":84},"b29","\u003Cp>If you&#39;ve got a design-system component masquerading as a form field with a hidden \u003Ccode>&lt;input&gt;\u003C\u002Fcode> glued to its side, that&#39;s a straightforward swap now. What&#39;s the ugliest hidden-input-sync hack you&#39;re still carrying?\u003C\u002Fp>",{"id":178,"type":179},"b30","divider",{"id":181,"html":182,"type":84},"b31","\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":184,"html":185,"type":84},"b32","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":187,"type":188,"items":189,"ordered":18},"b33","list",[190,191,192],"⭐ \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 build a `\u003Cstar-rating>` custom element. Five clickable stars, a nice hover state, a `value` property. You drop it into a signup form next to the real inputs. It looks identical to everything around it.\n\nYou submit the form. `FormData` comes back with the email, the password, the checkbox — and no rating. Not empty. Missing. As far as the `\u003Cform>` is concerned, your custom element was never there.\n\n## The form doesn't see what you think it sees\n\nA `\u003Cform>` only collects values from elements it recognizes as form controls. That's a fixed list baked into the platform: `\u003Cinput>`, `\u003Cselect>`, `\u003Ctextarea>`, `\u003Cbutton>`, a handful of others. A custom element — no matter how convincingly it renders, no matter what you name its property — isn't on that list. It can have a `name` attribute. It can have a `value` getter. The form still walks past it like it's a `\u003Cdiv>`, because as far as form submission is concerned, it is one.\n\nSame story for the rest of the form contract you got for free with `\u003Cinput>`: `\u003Clabel for=\"rating\">` won't focus it, `required` won't block submission on it, `:invalid` won't style it, `form.reset()` won't clear it. None of that is wired up — there was never a mechanism for a custom element to opt in.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Felementinternals-form-associated-custom-elements\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 fix everyone reaches for first\n\nThe obvious move is a hidden mirror:\n\n```html\n\u003Cstar-rating id=\"stars\">\u003C\u002Fstar-rating>\n\u003Cinput type=\"hidden\" name=\"rating\" id=\"rating-mirror\" \u002F>\n```\n\n```js\nstars.addEventListener(\"change\", () => {\n  ratingMirror.value = stars.value;\n});\n```\n\nThis works, in the sense that a value now shows up in `FormData`. It also means you're maintaining a second element whose only job is being real where the first one is fake. Every place the rating can change — clicking a star, a keyboard shortcut, a preset button — needs to remember to update the mirror too, or the two drift apart silently.\n\n`required` looks like the next box to tick, so someone adds it to the hidden input. It does nothing. `\u003Cinput type=\"hidden\">` is explicitly excluded from constraint validation — the spec calls it \"barred from constraint validation\" — so a required hidden field is always considered valid, full stop. What actually ships instead is a hand-rolled check bolted onto the submit handler: `if (!ratingMirror.value) event.preventDefault()`. That's `required`, reimplemented in application code, with no `:invalid` styling and no native \"please fill this out\" message — because the one place that logic could have lived natively was never a candidate for it. `form.reset()` clears the hidden input's value but not the visible stars, so that's another listener to write by hand. Every native behavior you're missing gets re-implemented as its own event listener, on its own element, that has to stay in sync with the one the user actually sees.\n\n## The part of the platform built for exactly this\n\nCustom elements have been able to register as real form participants since `ElementInternals` shipped — no hidden input, no manual sync:\n\n```js\nclass StarRating extends HTMLElement {\n  static formAssociated = true; \u002F\u002F opts into the form-control contract\n\n  constructor() {\n    super();\n    this._internals = this.attachInternals();\n    this._value = 0;\n  }\n\n  set value(v) {\n    this._value = v;\n    this._internals.setFormValue(String(v)); \u002F\u002F this is what FormData reads now\n    this._internals.setValidity(\n      this.required && v === 0 ? { valueMissing: true } : {},\n      \"Pick a rating.\",\n      this \u002F\u002F anchor for the native validation bubble\n    );\n  }\n\n  get value() {\n    return this._value;\n  }\n\n  formResetCallback() {\n    this.value = 0; \u002F\u002F form.reset() now clears the widget for free\n  }\n\n  formDisabledCallback(disabled) {\n    this.toggleAttribute(\"aria-disabled\", disabled);\n  }\n}\ncustomElements.define(\"star-rating\", StarRating);\n```\n\n`attachInternals()` hands back an `ElementInternals` object that's the element's private line to the form machinery — `setFormValue()` is what `FormData` actually reads, and it's the only line in this class that touches submission. `setValidity()` plugs into the same constraint-validation system `required` and `:invalid` already use, so `form.checkValidity()` and `reportValidity()` see this element exactly like a native input. `formResetCallback()` and `formDisabledCallback()` are lifecycle hooks the browser calls on its own — no listener to attach, no sync to forget.\n\nThe hidden-input version and this version produce the same `FormData` on a good day. They stop matching the moment something goes wrong: an empty required field, a form reset mid-interaction, a disabled fieldset. That's exactly the code the mirror-input hack never got right, because nothing forced it to.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Felementinternals-form-associated-custom-elements\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## What this buys you\n\nThis isn't just less code — it's less code that lies about being finished. The hidden-input version *looks* like a form field to a skim-read, right up until QA resets the form or a screen reader user tabs to the `\u003Clabel>` expecting it to do something. `ElementInternals` doesn't make custom elements harder to write; it makes them stop pretending to be inputs and actually become one, using the exact validation and submission pipeline the browser already runs for everything else on the page.\n\nIf you've got a design-system component masquerading as a form field with a hidden `\u003Cinput>` glued to its side, that's a straightforward swap now. What's the ugliest hidden-input-sync hack you're still carrying?\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":195,"description":196},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Felementinternals-form-associated-custom-elements","A custom \u003Cstar-rating> element sits inside a \u003Cform>, looks like a form field, and gets left out of the submission entirely. The browser has a real fix for this — most teams reach f","01a08af7-9462-760d-89bf-e99efa0702d1",{"id":199,"locked":18},"01a08af7-9487-7796-89a7-5e19d3ac25e8",[201],{"id":202,"slug":46,"title":203,"_count":204},"01a08af7-94a6-7638-a173-3ad8a079bd7f","ElementInternals — form-associated custom elements",{"questions":205},7,[207],{"locale":13,"slug":46},{"id":202,"slug":46,"title":203,"_count":209,"questionCount":205},{"questions":205},[211,213,215,219,223,227,231,235,238,242,245,249],{"slug":73,"name":74,"articles":212},97,{"slug":61,"name":65,"articles":214},81,{"slug":216,"name":217,"articles":218},"frontend","Frontend",70,{"slug":220,"name":221,"articles":222},"css","Css",32,{"slug":224,"name":225,"articles":226},"tutorial","Tutorial",27,{"slug":228,"name":229,"articles":230},"typescript","Typescript",14,{"slug":232,"name":233,"articles":234},"react","React",12,{"slug":236,"name":237,"articles":234},"performance","Performance",{"slug":239,"name":240,"articles":241},"browser","Browser",8,{"slug":243,"name":244,"articles":241},"node","Node",{"slug":246,"name":247,"articles":248},"grammar","Grammar",6,{"slug":250,"name":251,"articles":248},"programming","Programming",{"items":253,"meta":383},[254,265,285,306,328,347,365],{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":222,"likeCount":19,"commentCount":19,"author":255,"vertical":256,"topic":257,"tags":258,"_count":263,"playground":264,"hasQuiz":17,"hasPlayground":17},{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[259,260,261,262],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":73,"name":74,"color":48},{"assessments":76},{"slug":46},{"id":266,"slug":267,"title":268,"subtitle":48,"excerpt":269,"coverUrl":270,"locale":13,"readingMinutes":234,"publishedAt":271,"viewCount":272,"likeCount":19,"commentCount":19,"author":273,"vertical":274,"topic":275,"tags":278,"_count":283,"playground":284,"hasQuiz":17,"hasPlayground":17},"01a04446-5a00-7624-b775-c73c6c035e67","vue-weekly-nexttick-batching-dom-updates","Vue nextTick Explained: How DOM Updates Are Batched","How Vue batches reactive writes into one DOM update, why the DOM looks stale right after a change, and how nextTick and watch's flush option fix it.","\u002Fmedia\u002Fcovers\u002Fvue-weekly-nexttick-batching-dom-updates.png","2026-09-14T12:19:00.606Z",88,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":276,"name":277},"vue","Vue",[279,280,281,282],{"slug":276,"name":277,"color":48},{"slug":61,"name":65,"color":48},{"slug":224,"name":225,"color":48},{"slug":73,"name":74,"color":48},{"assessments":76},{"slug":267},{"id":286,"slug":287,"title":288,"subtitle":48,"excerpt":289,"coverUrl":290,"locale":13,"readingMinutes":291,"publishedAt":292,"viewCount":293,"likeCount":19,"commentCount":19,"author":294,"vertical":295,"topic":296,"tags":297,"_count":304,"playground":305,"hasQuiz":17,"hasPlayground":17},"01a085d3-d892-7065-956a-2f646cc7107e","speech-synthesis-api-browser-tts","The 'Read Aloud' Button That Only Works on the Second Click","A text-to-speech button that works perfectly every time you test it — and silently fails or picks the wrong voice for a real visitor's first click. The bug is a timing race baked into the Web Speech API that most code never accounts for.","\u002Fmedia\u002Fcovers\u002Fspeech-synthesis-api-browser-tts.png",5,"2026-09-14T12:18:29.717Z",50,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[298,299,300,303],{"slug":61,"name":65,"color":48},{"slug":73,"name":74,"color":48},{"slug":301,"name":302,"color":48},"accessibility","Accessibility",{"slug":224,"name":225,"color":48},{"assessments":76},{"slug":287},{"id":307,"slug":308,"title":309,"subtitle":48,"excerpt":310,"coverUrl":311,"locale":13,"readingMinutes":230,"publishedAt":312,"viewCount":313,"likeCount":19,"commentCount":19,"author":314,"vertical":315,"topic":316,"tags":319,"_count":326,"playground":327,"hasQuiz":17,"hasPlayground":17},"01a08c59-464c-7039-acf3-b32a0b9ee7ce","nuxt-weekly-useasyncdata-keys-dedupe","useAsyncData Keys in Nuxt: Caching, Dedupe & the Sharing Bug","Learn how Nuxt's useAsyncData and useFetch generate cache keys, how dedupe (cancel vs defer) really works, and why wrapper composables silently share data.","\u002Fmedia\u002Fcovers\u002Fnuxt-weekly-useasyncdata-keys-dedupe.png","2026-09-13T11:26:28.014Z",124,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":317,"name":318},"nuxt","Nuxt",[320,321,324,325],{"slug":317,"name":318,"color":48},{"slug":322,"name":323,"color":48},"ssr","SSR",{"slug":61,"name":65,"color":48},{"slug":224,"name":225,"color":48},{"assessments":76},{"slug":308},{"id":329,"slug":330,"title":331,"subtitle":48,"excerpt":332,"coverUrl":333,"locale":13,"readingMinutes":248,"publishedAt":334,"viewCount":335,"likeCount":19,"commentCount":19,"author":336,"vertical":337,"topic":338,"tags":340,"_count":345,"playground":346,"hasQuiz":17,"hasPlayground":17},"01a080a9-801c-727a-ad92-866c15063710","css-custom-highlight-api-no-dom-mutation","The Search Highlight That Deletes Your Selection","A search-highlight feature that rebuilds its DOM on every keystroke can wipe out a user's own text selection with no warning. The CSS Custom Highlight API fixes it by styling text ranges without ever touching a DOM node.","\u002Fmedia\u002Fcovers\u002Fcss-custom-highlight-api-no-dom-mutation.png","2026-09-13T11:25:57.014Z",158,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":220,"name":339},"CSS",[341,342,343,344],{"slug":220,"name":221,"color":48},{"slug":61,"name":65,"color":48},{"slug":73,"name":74,"color":48},{"slug":224,"name":225,"color":48},{"assessments":76},{"slug":330},{"id":348,"slug":349,"title":350,"subtitle":48,"excerpt":351,"coverUrl":352,"locale":13,"readingMinutes":291,"publishedAt":353,"viewCount":354,"likeCount":19,"commentCount":19,"author":355,"vertical":356,"topic":357,"tags":358,"_count":363,"playground":364,"hasQuiz":17,"hasPlayground":17},"01a0764b-4844-732e-99fe-3e0122793752","promise-try-synchronous-throw-safety","My .catch() Never Ran. The Bug Was Two Lines Above It","A function that sometimes throws synchronously instead of returning a promise will skip every .catch() chained to it — because the promise never gets created. Promise.try() fixes it, and does one thing try\u002Fcatch can't.","\u002Fmedia\u002Fcovers\u002Fpromise-try-synchronous-throw-safety.png","2026-09-13T08:41:22.072Z",108,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[359,360,361,362],{"slug":61,"name":65,"color":48},{"slug":73,"name":74,"color":48},{"slug":243,"name":244,"color":48},{"slug":250,"name":251,"color":48},{"assessments":76},{"slug":349},{"id":366,"slug":367,"title":368,"subtitle":48,"excerpt":369,"coverUrl":370,"locale":13,"readingMinutes":291,"publishedAt":371,"viewCount":372,"likeCount":19,"commentCount":19,"author":373,"vertical":374,"topic":375,"tags":376,"_count":381,"playground":382,"hasQuiz":17,"hasPlayground":17},"01a05275-0841-74b8-9d17-108d35eb9cd5","cookie-store-api-async-cookies","Stop Regex-Parsing document.cookie. Use CookieStore","document.cookie hands you one flat string and no way to know when it changes. The Cookie Store API gives you async get\u002Fset\u002Fdelete and a change event — but its set() defaults to a stricter SameSite than the one you're used to.","\u002Fmedia\u002Fcovers\u002Fcookie-store-api-async-cookies.png","2026-09-13T08:39:46.439Z",64,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[377,378,379,380],{"slug":73,"name":74,"color":48},{"slug":61,"name":65,"color":48},{"slug":239,"name":240,"color":48},{"slug":216,"name":217,"color":48},{"assessments":76},{"slug":367},{"page":76,"perPage":205,"total":214,"totalPages":234},"\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>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-var\">star-rating\u003C\u002Fspan> \u003Cspan class=\"shj-class\">id\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"stars\"\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\">star-rating\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\">input\u003C\u002Fspan> \u003Cspan class=\"shj-class\">type\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"hidden\"\u003C\u002Fspan> \u003Cspan class=\"shj-class\">name\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"rating\"\u003C\u002Fspan> \u003Cspan class=\"shj-class\">id\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"rating-mirror\"\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">\u002F&gt;\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">stars\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">addEventListener\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"change\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  ratingMirror\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>value \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> stars\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>value;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\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>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003Cdiv>16\u003C\u002Fdiv>\u003Cdiv>17\u003C\u002Fdiv>\u003Cdiv>18\u003C\u002Fdiv>\u003Cdiv>19\u003C\u002Fdiv>\u003Cdiv>20\u003C\u002Fdiv>\u003Cdiv>21\u003C\u002Fdiv>\u003Cdiv>22\u003C\u002Fdiv>\u003Cdiv>23\u003C\u002Fdiv>\u003Cdiv>24\u003C\u002Fdiv>\u003Cdiv>25\u003C\u002Fdiv>\u003Cdiv>26\u003C\u002Fdiv>\u003Cdiv>27\u003C\u002Fdiv>\u003Cdiv>28\u003C\u002Fdiv>\u003Cdiv>29\u003C\u002Fdiv>\u003Cdiv>30\u003C\u002Fdiv>\u003Cdiv>31\u003C\u002Fdiv>\u003Cdiv>32\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">class\u003C\u002Fspan> \u003Cspan class=\"shj-class\">StarRating\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-kwd\">static\u003C\u002Fspan> formAssociated \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bool\">true\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F opts into the form-control contract\n\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">constructor\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\">super\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>_internals \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">this\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">attachInternals\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>_value \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n  \u003Cspan class=\"shj-kwd\">set\u003C\u002Fspan> \u003Cspan class=\"shj-func\">value\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>v\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>_value \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> v;\n    \u003Cspan class=\"shj-kwd\">this\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>_internals\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">setFormValue\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-class\">String\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>v\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F this is what FormData reads now\n\u003C\u002Fspan>    \u003Cspan class=\"shj-kwd\">this\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>_internals\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">setValidity\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\n      \u003Cspan class=\"shj-kwd\">this\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>required \u003Cspan class=\"shj-oper\">&#38;&#38;\u003C\u002Fspan> v \u003Cspan class=\"shj-oper\">===\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">?\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> valueMissing\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bool\">true\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n      \u003Cspan class=\"shj-str\">\"Pick a rating.\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n      \u003Cspan class=\"shj-kwd\">this\u003C\u002Fspan> \u003Cspan class=\"shj-cmnt\">\u002F\u002F anchor for the native validation bubble\n\u003C\u002Fspan>    \u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n  \u003Cspan class=\"shj-kwd\">get\u003C\u002Fspan> \u003Cspan class=\"shj-func\">value\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\">return\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">this\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>_value;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n  \u003Cspan class=\"shj-func\">formResetCallback\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>value \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F form.reset() now clears the widget for free\n\u003C\u002Fspan>  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n  \u003Cspan class=\"shj-func\">formDisabledCallback\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>disabled\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\">toggleAttribute\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"aria-disabled\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> disabled\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\ncustomElements\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">define\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"star-rating\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-class\">StarRating\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":388},[]]