[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-eyedropper-api-native-color-picker":44,"search-suggestions":223,"related-eyedropper-api-native-color-picker":264,"code:js:true:1vob2c7":399,"code:js:true:2o71zg":400,"code:js:true:x00u0w":401,"comments-01a0a4d0-bf0a-73a8-a83e-7e87ab5df792":402},[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":207,"seo":208,"translationGroupId":210,"series":48,"podcastUrl":48,"verticalId":5,"thread":211,"assessments":213,"translations":219,"quiz":221},"01a0a4d0-bf0a-73a8-a83e-7e87ab5df792","eyedropper-api-native-color-picker","Chrome Already Has The Eyedropper You're Building",null,"You're about to build a canvas-based color picker with a magnifier loupe. Chrome shipped a native one years ago — and it can sample pixels your own canvas can't even see.","\u002Fmedia\u002Fcovers\u002Feyedropper-api-native-color-picker.png",5,"2026-09-20T16:25:34.048Z",106,{"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},"webdev","Webdev",{"slug":70,"name":71,"color":48},"api","API",{"slug":73,"name":74,"color":48},"tutorial","Tutorial",{"assessments":76},1,{"slug":46,"title":78},"EyeDropper API — pick a real screen color",{"blocks":80,"version":76},[81,85,88,91,95,98,104,107,110,113,116,119,123,126,129,132,135,138,142,145,148,151,154,157,160,164,167,170,173,176,179,182,185,188,191,194,197,200],{"id":82,"html":83,"type":84},"b1","\u003Cp>A designer messages you: &quot;Can we let people pick a color from anywhere on their screen? Like, an eyedropper — click it, then click on my logo, and it grabs that exact blue.&quot;\u003C\u002Fp>","paragraph",{"id":86,"html":87,"type":84},"b2","\u003Cp>Sure, you say. You&#39;ve done color pickers before. How hard can this be.\u003C\u002Fp>",{"id":89,"html":90,"type":84},"b3","\u003Cp>\u003Cstrong>Guess before you scroll: how many lines of code does the real answer take?\u003C\u002Fstrong>\u003C\u002Fp>",{"id":92,"html":93,"text":93,"type":94,"level":31},"b4","The canvas rabbit hole","heading",{"id":96,"html":97,"type":84},"b5","\u003Cp>The obvious plan: capture a snapshot of the page onto a \u003Ccode>&lt;canvas&gt;\u003C\u002Fcode>, track the mouse, and read the pixel under the cursor with \u003Ccode>getImageData\u003C\u002Fcode>.\u003C\u002Fp>",{"id":99,"code":100,"type":101,"language":102,"highlight":103},"b6","const ctx = canvas.getContext(\"2d\");\ncanvas.addEventListener(\"mousemove\", (e) => {\n  const { data } = ctx.getImageData(e.offsetX, e.offsetY, 1, 1);\n  const hex = \"#\" + [data[0], data[1], data[2]]\n    .map((c) => c.toString(16).padStart(2, \"0\"))\n    .join(\"\");\n  swatch.style.background = hex;\n});","code","js",[],{"id":105,"html":106,"type":84},"b7","\u003Cp>That much works. Then the designer actually tries it, and moves the cursor off the page — onto their open Figma tab, onto the taskbar, onto the desktop wallpaper behind everything — and nothing happens. Your loupe just shows whatever was already under the cursor the last time it was over your canvas.\u003C\u002Fp>",{"id":108,"html":109,"type":84},"b8","\u003Cp>It&#39;s not a bug you can patch. A \u003Ccode>&lt;canvas&gt;\u003C\u002Fcode> can only ever paint pixels \u003Cem>you\u003C\u002Fem> drew into it. It has no way to see the rest of the screen — no API reads &quot;whatever is currently under the OS cursor,&quot; because that would mean a random web page could screenshot your other tabs and your desktop on a whim. The sandbox that keeps that from happening is also the wall your eyedropper just hit.\u003C\u002Fp>",{"id":111,"html":112,"type":84},"b9","\u003Cp>So now you&#39;re looking at \u003Ccode>getDisplayMedia()\u003C\u002Fcode> — the screen-capture API — just to grab a frame of the whole display, decode it into a video element, draw \u003Cem>that\u003C\u002Fem> onto a canvas, and read pixels from the copy. It works, technically. It also means a permission prompt, a capture-in-progress browser banner, and maybe 150 lines of plumbing for something that, on your OS, is a single click in the system color picker.\u003C\u002Fp>",{"id":114,"html":115,"text":115,"type":94,"level":31},"b10","The four lines that replace all of it",{"id":117,"html":118,"type":84},"b11","\u003Cp>Chrome — and Edge, and other Chromium browsers — has shipped a real system color picker as a Web API since late 2021: \u003Ccode>window.EyeDropper\u003C\u002Fcode>. No canvas, no screen capture, no pixel math.\u003C\u002Fp>",{"id":120,"code":121,"type":101,"language":102,"highlight":122},"b12","async function pickColor() {\n  const eyeDropper = new EyeDropper();\n  const { sRGBHex } = await eyeDropper.open();\n  swatch.style.background = sRGBHex;\n}\n\npickButton.addEventListener(\"click\", pickColor);",[],{"id":124,"html":125,"type":84},"b13","\u003Cp>Click the button, the cursor turns into a genuine magnifier loupe, and it works \u003Cem>anywhere on the screen\u003C\u002Fem> — over the page, over another app, over the taskbar. Click a pixel, and \u003Ccode>open()\u003C\u002Fcode> resolves with \u003Ccode>{ sRGBHex: &quot;#3b82f6&quot; }\u003C\u002Fcode>. That&#39;s the whole feature. The OS is doing the screen-reading for you, outside the browser&#39;s sandbox entirely, because the browser itself asked the OS to hand back one color, not a frame of pixels your JavaScript could keep.\u003C\u002Fp>",{"id":127,"html":128,"type":84},"b14","\u003Cp>That &quot;outside the sandbox&quot; part is worth sitting with for a second: this is the one common case where a web page gets to see something \u003Cem>beyond its own tab\u003C\u002Fem> without a permission prompt, a video stream, or a capture banner — because the API is deliberately narrow. It hands back a single hex string and nothing else. There&#39;s no way to get a full-screen screenshot out of it, no way to sample a rectangle, no stream to record. That narrowness is the whole reason it doesn&#39;t need the heavyweight ceremony \u003Ccode>getDisplayMedia()\u003C\u002Fcode> does.\u003C\u002Fp>",{"id":130,"html":131,"text":131,"type":94,"level":31},"b15","Two things that will bite you if you skip them",{"id":133,"html":134,"type":84},"b16","\u003Cp>\u003Cstrong>It has to start from a real user gesture.\u003C\u002Fstrong> Call \u003Ccode>eyeDropper.open()\u003C\u002Fcode> from inside a \u003Ccode>setTimeout\u003C\u002Fcode>, a \u003Ccode>fetch().then()\u003C\u002Fcode>, or on page load, and it throws instead of opening — the same &quot;was this really a click&quot; gate that guards \u003Ccode>requestFullscreen()\u003C\u002Fcode> and autoplaying audio. Wire it to a \u003Ccode>click\u003C\u002Fcode> listener directly and you&#39;re fine; wire it behind an async chain and you&#39;ll see a rejected promise with no obvious cause until you check what actually triggered the call.\u003C\u002Fp>",{"id":136,"html":137,"type":84},"b17","\u003Cp>\u003Cstrong>The reader can bail, and your code has to expect that.\u003C\u002Fstrong> Press Escape while the loupe is active, and \u003Ccode>open()\u003C\u002Fcode> rejects with a \u003Ccode>DOMException\u003C\u002Fcode> named \u003Ccode>AbortError\u003C\u002Fcode> — not a resolved value with \u003Ccode>null\u003C\u002Fcode>, an actual rejection:\u003C\u002Fp>",{"id":139,"code":140,"type":101,"language":102,"highlight":141},"b18","async function pickColor() {\n  try {\n    const eyeDropper = new EyeDropper();\n    const { sRGBHex } = await eyeDropper.open();\n    swatch.style.background = sRGBHex;\n  } catch (err) {\n    if (err.name === \"AbortError\") return; \u002F\u002F user pressed Escape — not a bug\n    console.error(err);\n  }\n}",[],{"id":143,"html":144,"type":84},"b19","\u003Cp>Skip the \u003Ccode>try\u002Fcatch\u003C\u002Fcode> and one Escape key press turns your color picker into an uncaught promise rejection in the console, on the most ordinary possible user action.\u003C\u002Fp>",{"id":146,"html":147,"type":84},"b20","\u003C!-- playground:start -->",{"id":149,"html":150,"text":150,"type":94,"level":31},"b21","🎮 Try it yourself",{"id":152,"html":153,"type":84},"b22","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Feyedropper-api-native-color-picker\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":155,"html":156,"type":84},"b23","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":158,"html":159,"type":84},"b24","\u003C!-- playground:end -->",{"id":161,"html":162,"text":163,"type":94,"level":31},"b25","Where it doesn&#39;t reach yet","Where it doesn't reach yet",{"id":165,"html":166,"type":84},"b26","\u003Cp>Feature-detect before you use any of this — \u003Ccode>if (&quot;EyeDropper&quot; in window)\u003C\u002Fcode> — because Firefox and Safari don&#39;t ship it. There&#39;s no polyfill worth using either: the entire point is reading pixels outside the page, and nothing short of the OS itself can do that safely. Fall back to a plain \u003Ccode>&lt;input type=&quot;color&quot;&gt;\u003C\u002Fcode> (which every browser already renders with its own OS color picker, just scoped to that one input) rather than resurrecting the canvas-and-\u003Ccode>getDisplayMedia()\u003C\u002Fcode> approach as a shim — you&#39;d be rebuilding the exact complexity this API exists to delete.\u003C\u002Fp>",{"id":168,"html":169,"type":84},"b27","\u003Cp>It also only ever gives you one thing: a hex string. No alpha channel, no color name, no continuous &quot;drag to preview&quot; stream of values while you move — just the single pixel you clicked on, once. For most &quot;pick a color from my design&quot; features, that&#39;s the entire ask anyway.\u003C\u002Fp>",{"id":171,"html":172,"type":84},"b28","\u003Cp>So: the designer&#39;s request that sounded like a screen-capture project is four lines and a \u003Ccode>catch\u003C\u002Fcode> block, on the one browser engine that ships it. The canvas loupe isn&#39;t wrong, exactly — it&#39;s solving a problem the platform already solved for you, just not the way most people go looking for it.\u003C\u002Fp>",{"id":174,"html":175,"type":84},"b29","\u003Cp>What&#39;s the last browser API you built a workaround for, before finding out it already existed?\u003C\u002Fp>",{"id":177,"html":178,"type":84},"b30","\u003C!-- quiz:start -->",{"id":180,"html":181,"text":181,"type":94,"level":31},"b31","🧠 Test yourself",{"id":183,"html":184,"type":84},"b32","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Feyedropper-api-native-color-picker\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":186,"html":187,"type":84},"b33","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":189,"html":190,"type":84},"b34","\u003C!-- quiz:end -->",{"id":192,"type":193},"b35","divider",{"id":195,"html":196,"type":84},"b36","\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":198,"html":199,"type":84},"b37","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":201,"type":202,"items":203,"ordered":18},"b38","list",[204,205,206],"⭐ \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>","A designer messages you: \"Can we let people pick a color from anywhere on their screen? Like, an eyedropper — click it, then click on my logo, and it grabs that exact blue.\"\n\nSure, you say. You've done color pickers before. How hard can this be.\n\n**Guess before you scroll: how many lines of code does the real answer take?**\n\n## The canvas rabbit hole\n\nThe obvious plan: capture a snapshot of the page onto a `\u003Ccanvas>`, track the mouse, and read the pixel under the cursor with `getImageData`.\n\n```js\nconst ctx = canvas.getContext(\"2d\");\ncanvas.addEventListener(\"mousemove\", (e) => {\n  const { data } = ctx.getImageData(e.offsetX, e.offsetY, 1, 1);\n  const hex = \"#\" + [data[0], data[1], data[2]]\n    .map((c) => c.toString(16).padStart(2, \"0\"))\n    .join(\"\");\n  swatch.style.background = hex;\n});\n```\n\nThat much works. Then the designer actually tries it, and moves the cursor off the page — onto their open Figma tab, onto the taskbar, onto the desktop wallpaper behind everything — and nothing happens. Your loupe just shows whatever was already under the cursor the last time it was over your canvas.\n\nIt's not a bug you can patch. A `\u003Ccanvas>` can only ever paint pixels *you* drew into it. It has no way to see the rest of the screen — no API reads \"whatever is currently under the OS cursor,\" because that would mean a random web page could screenshot your other tabs and your desktop on a whim. The sandbox that keeps that from happening is also the wall your eyedropper just hit.\n\nSo now you're looking at `getDisplayMedia()` — the screen-capture API — just to grab a frame of the whole display, decode it into a video element, draw *that* onto a canvas, and read pixels from the copy. It works, technically. It also means a permission prompt, a capture-in-progress browser banner, and maybe 150 lines of plumbing for something that, on your OS, is a single click in the system color picker.\n\n## The four lines that replace all of it\n\nChrome — and Edge, and other Chromium browsers — has shipped a real system color picker as a Web API since late 2021: `window.EyeDropper`. No canvas, no screen capture, no pixel math.\n\n```js\nasync function pickColor() {\n  const eyeDropper = new EyeDropper();\n  const { sRGBHex } = await eyeDropper.open();\n  swatch.style.background = sRGBHex;\n}\n\npickButton.addEventListener(\"click\", pickColor);\n```\n\nClick the button, the cursor turns into a genuine magnifier loupe, and it works *anywhere on the screen* — over the page, over another app, over the taskbar. Click a pixel, and `open()` resolves with `{ sRGBHex: \"#3b82f6\" }`. That's the whole feature. The OS is doing the screen-reading for you, outside the browser's sandbox entirely, because the browser itself asked the OS to hand back one color, not a frame of pixels your JavaScript could keep.\n\nThat \"outside the sandbox\" part is worth sitting with for a second: this is the one common case where a web page gets to see something *beyond its own tab* without a permission prompt, a video stream, or a capture banner — because the API is deliberately narrow. It hands back a single hex string and nothing else. There's no way to get a full-screen screenshot out of it, no way to sample a rectangle, no stream to record. That narrowness is the whole reason it doesn't need the heavyweight ceremony `getDisplayMedia()` does.\n\n## Two things that will bite you if you skip them\n\n**It has to start from a real user gesture.** Call `eyeDropper.open()` from inside a `setTimeout`, a `fetch().then()`, or on page load, and it throws instead of opening — the same \"was this really a click\" gate that guards `requestFullscreen()` and autoplaying audio. Wire it to a `click` listener directly and you're fine; wire it behind an async chain and you'll see a rejected promise with no obvious cause until you check what actually triggered the call.\n\n**The reader can bail, and your code has to expect that.** Press Escape while the loupe is active, and `open()` rejects with a `DOMException` named `AbortError` — not a resolved value with `null`, an actual rejection:\n\n```js\nasync function pickColor() {\n  try {\n    const eyeDropper = new EyeDropper();\n    const { sRGBHex } = await eyeDropper.open();\n    swatch.style.background = sRGBHex;\n  } catch (err) {\n    if (err.name === \"AbortError\") return; \u002F\u002F user pressed Escape — not a bug\n    console.error(err);\n  }\n}\n```\n\nSkip the `try\u002Fcatch` and one Escape key press turns your color picker into an uncaught promise rejection in the console, on the most ordinary possible user action.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Feyedropper-api-native-color-picker\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 doesn't reach yet\n\nFeature-detect before you use any of this — `if (\"EyeDropper\" in window)` — because Firefox and Safari don't ship it. There's no polyfill worth using either: the entire point is reading pixels outside the page, and nothing short of the OS itself can do that safely. Fall back to a plain `\u003Cinput type=\"color\">` (which every browser already renders with its own OS color picker, just scoped to that one input) rather than resurrecting the canvas-and-`getDisplayMedia()` approach as a shim — you'd be rebuilding the exact complexity this API exists to delete.\n\nIt also only ever gives you one thing: a hex string. No alpha channel, no color name, no continuous \"drag to preview\" stream of values while you move — just the single pixel you clicked on, once. For most \"pick a color from my design\" features, that's the entire ask anyway.\n\nSo: the designer's request that sounded like a screen-capture project is four lines and a `catch` block, on the one browser engine that ships it. The canvas loupe isn't wrong, exactly — it's solving a problem the platform already solved for you, just not the way most people go looking for it.\n\nWhat's the last browser API you built a workaround for, before finding out it already existed?\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Feyedropper-api-native-color-picker\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":209,"description":49},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Feyedropper-api-native-color-picker","01a0a4d0-bf0a-73a8-a83e-80944f8d18d8",{"id":212,"locked":18},"01a0a4d0-bfcb-74eb-8580-c41181ff90f1",[214],{"id":215,"slug":46,"title":216,"_count":217},"01a0a4d0-bfe4-77d9-8797-de09bfc566f0","The EyeDropper API — pick a real screen color",{"questions":218},7,[220],{"locale":13,"slug":46},{"id":215,"slug":46,"title":216,"_count":222,"questionCount":218},{"questions":218},[224,226,228,232,236,238,242,246,250,254,257,261],{"slug":67,"name":68,"articles":225},103,{"slug":61,"name":65,"articles":227},87,{"slug":229,"name":230,"articles":231},"frontend","Frontend",71,{"slug":233,"name":234,"articles":235},"css","Css",34,{"slug":73,"name":74,"articles":237},32,{"slug":239,"name":240,"articles":241},"typescript","Typescript",15,{"slug":243,"name":244,"articles":245},"react","React",13,{"slug":247,"name":248,"articles":249},"performance","Performance",12,{"slug":251,"name":252,"articles":253},"browser","Browser",9,{"slug":255,"name":256,"articles":253},"node","Node",{"slug":258,"name":259,"articles":260},"html","Html",6,{"slug":262,"name":263,"articles":260},"grammar","Grammar",{"items":265,"meta":398},[266,285,303,325,337,355,375],{"id":267,"slug":268,"title":269,"subtitle":48,"excerpt":270,"coverUrl":271,"locale":13,"readingMinutes":272,"publishedAt":273,"viewCount":274,"likeCount":19,"commentCount":19,"author":275,"vertical":276,"topic":277,"tags":278,"_count":283,"playground":284,"hasQuiz":17,"hasPlayground":17},"01a0ab57-dda2-74dd-816b-a0ef7f77aeb3","react-weekly-derived-state-bug","React Derived State: Why That useState Is Probably a Bug","React derived state explained: why syncing a computed value into useState with an Effect causes extra renders and drift, and what to write instead.","\u002Fmedia\u002Fcovers\u002Freact-weekly-derived-state-bug.png",14,"2026-09-20T16:27:07.271Z",141,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":243,"name":244},[279,280,281,282],{"slug":243,"name":244,"color":48},{"slug":61,"name":65,"color":48},{"slug":73,"name":74,"color":48},{"slug":67,"name":68,"color":48},{"assessments":76},{"slug":268},{"id":286,"slug":287,"title":288,"subtitle":48,"excerpt":289,"coverUrl":290,"locale":13,"readingMinutes":260,"publishedAt":291,"viewCount":292,"likeCount":19,"commentCount":19,"author":293,"vertical":294,"topic":295,"tags":296,"_count":301,"playground":302,"hasQuiz":17,"hasPlayground":17},"01a09fda-b750-710a-9a40-8113c1d054b3","drag-drop-api-dragover-preventdefault-trap","Your Browser Is Rejecting Every Drop On Purpose","A drop zone with a correctly wired drop listener that never fires — no error, no typo, nothing in the console. The browser is silently refusing the drop by design, and only one specific line stops it.","\u002Fmedia\u002Fcovers\u002Fdrag-drop-api-dragover-preventdefault-trap.png","2026-09-20T16:26:35.914Z",113,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[297,298,299,300],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":258,"name":259,"color":48},{"slug":73,"name":74,"color":48},{"assessments":76},{"slug":287},{"id":304,"slug":305,"title":306,"subtitle":48,"excerpt":307,"coverUrl":308,"locale":13,"readingMinutes":272,"publishedAt":309,"viewCount":310,"likeCount":19,"commentCount":19,"author":311,"vertical":312,"topic":313,"tags":316,"_count":323,"playground":324,"hasQuiz":17,"hasPlayground":17},"01a0b07e-f66a-736f-a8f4-3c87da225d8f","nuxt-weekly-hydration-mismatch","Nuxt Hydration Mismatch: Why It Happens and How to Fix It","A Nuxt hydration mismatch happens when the server's HTML disagrees with the client's first render. Learn why, and the fixes that actually work.","\u002Fmedia\u002Fcovers\u002Fnuxt-weekly-hydration-mismatch.png","2026-09-20T16:26:05.191Z",130,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":314,"name":315},"nuxt","Nuxt",[317,318,321,322],{"slug":314,"name":315,"color":48},{"slug":319,"name":320,"color":48},"ssr","SSR",{"slug":61,"name":65,"color":48},{"slug":73,"name":74,"color":48},{"assessments":76},{"slug":305},{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":326,"likeCount":19,"commentCount":19,"author":327,"vertical":328,"topic":329,"tags":330,"_count":335,"playground":336,"hasQuiz":17,"hasPlayground":17},107,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[331,332,333,334],{"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":338,"slug":339,"title":340,"subtitle":48,"excerpt":341,"coverUrl":342,"locale":13,"readingMinutes":260,"publishedAt":343,"viewCount":344,"likeCount":19,"commentCount":19,"author":345,"vertical":346,"topic":347,"tags":348,"_count":353,"playground":354,"hasQuiz":17,"hasPlayground":17},"01a09525-10c5-714d-9021-2e375a15a5d3","matchmedia-watch-media-queries-from-js","Stop Debouncing Resize. The Browser Already Watches It.","A debounced resize handler comparing window.innerWidth to a magic number can't ever see dark mode or reduced motion — and it fires dozens of times for one thing matchMedia fires once for.","\u002Fmedia\u002Fcovers\u002Fmatchmedia-watch-media-queries-from-js.png","2026-09-17T11:19:53.454Z",316,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[349,350,351,352],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":233,"name":234,"color":48},{"slug":251,"name":252,"color":48},{"assessments":76},{"slug":339},{"id":356,"slug":357,"title":358,"subtitle":48,"excerpt":359,"coverUrl":360,"locale":13,"readingMinutes":51,"publishedAt":361,"viewCount":362,"likeCount":19,"commentCount":19,"author":363,"vertical":364,"topic":365,"tags":366,"_count":373,"playground":374,"hasQuiz":17,"hasPlayground":17},"01a0901c-7fee-7408-989d-4ff29be7c8f7","fetch-already-streams-readablestream","Your Fetch Already Streams. You're Buffering It Anyway.","A chat UI that types out its reply letter by letter isn't proof it's streaming — it's often proof the opposite. fetch() delivers a real ReadableStream body, and most code drains it into one string before showing a single character.","\u002Fmedia\u002Fcovers\u002Ffetch-already-streams-readablestream.png","2026-09-16T11:12:00.901Z",253,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[367,368,369,370],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":371,"name":372,"color":48},"streams","Streams",{"assessments":76},{"slug":357},{"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":396,"playground":397,"hasQuiz":17,"hasPlayground":17},"01a08af7-9462-760d-89bf-e4ef3c77bb6a","elementinternals-form-associated-custom-elements","The Star Rating That Never Reaches Your Server","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-15T17:50:16.210Z",244,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[388,389,392,395],{"slug":61,"name":65,"color":48},{"slug":390,"name":391,"color":48},"webcomponents","Web Components",{"slug":393,"name":394,"color":48},"forms","Forms",{"slug":67,"name":68,"color":48},{"assessments":76},{"slug":377},{"page":76,"perPage":218,"total":227,"totalPages":245},"\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>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> ctx \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> canvas\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">getContext\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"2d\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\ncanvas\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">addEventListener\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"mousemove\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>e\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> data \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> ctx\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">getImageData\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>e\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>offsetX\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> e\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>offsetY\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> hex \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"#\"\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">+\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>data\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> data\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-num\">1\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> data\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-num\">2\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\n    \u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">map\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>c\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> c\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">toString\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">16\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">padStart\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">2\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"0\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\n    \u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">join\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  swatch\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>style\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>background \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> hex;\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>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">pickColor\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\">const\u003C\u002Fspan> eyeDropper \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">EyeDropper\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> sRGBHex \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> eyeDropper\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">open\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  swatch\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>style\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>background \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> sRGBHex;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\npickButton\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">addEventListener\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"click\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> pickColor\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>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">pickColor\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\">try\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> eyeDropper \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">EyeDropper\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> sRGBHex \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> eyeDropper\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">open\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n    swatch\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>style\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>background \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> sRGBHex;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">catch\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>err\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>err\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>name \u003Cspan class=\"shj-oper\">===\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"AbortError\"\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F user pressed Escape — not a bug\n\u003C\u002Fspan>    console\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">error\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>err\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":403},[]]