[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-css-individual-transform-properties":44,"search-suggestions":254,"path-membership-css-individual-transform-properties":296,"related-css-individual-transform-properties":297,"code:js:true:sumixo":426,"code:css:true:14uzfqd":427,"code:js:true:5zbpch":428,"code:css:true:85cjfk":429,"code:js:true:6df178":430,"code:css:true:1brp1dg":431,"code:js:true:1apfr50":432,"comments-01a149cb-71f3-71ba-bb39-4f894fa2f1a8":433},[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":237,"seo":238,"translationGroupId":241,"series":48,"podcastUrl":48,"verticalId":5,"thread":242,"assessments":244,"translations":250,"quiz":252},"01a149cb-71f3-71ba-bb39-4f894fa2f1a8","css-individual-transform-properties","Stop Jamming Translate, Rotate, and Scale Into One `transform`",null,"A CSS :hover scale and a JS drag handler both write to `transform` — and the moment JS sets it, the hover rule silently stops working, no error, no warning. The `translate`, `rotate`, and `scale` properties exist so this never happens.","\u002Fmedia\u002Fcovers\u002Fcss-individual-transform-properties.png",5,"2026-10-22T06:10:24.373Z",37,{"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},"css","CSS",[64,66,69,72],{"slug":61,"name":65,"color":48},"Css",{"slug":67,"name":68,"color":48},"javascript","Javascript",{"slug":70,"name":71,"color":48},"webdev","Webdev",{"slug":73,"name":74,"color":48},"animation","Animation",{"assessments":76},1,{"slug":46,"title":78},"translate \u002F rotate \u002F scale — interactive playground",{"blocks":80,"version":76},[81,85,91,94,97,101,104,108,112,115,118,122,125,129,132,135,139,142,146,149,152,156,160,163,166,169,172,175,178,181,184,187,194,197,200,203,206,209,212,216,219,222,225,228,231],{"id":82,"html":83,"type":84},"b1","\u003Cp>You build a product card. On hover, it should lift a little — \u003Ccode>transform: scale(1.03)\u003C\u002Fcode>, a transition, done in five minutes. Then the ticket comes in: cards need to be draggable, so you add a \u003Ccode>pointermove\u003C\u002Fcode> handler that keeps the card glued to the cursor:\u003C\u002Fp>","paragraph",{"id":86,"code":87,"type":88,"language":89,"highlight":90},"b2","card.style.transform = `translate(${x}px, ${y}px)`;","code","js",[],{"id":92,"html":93,"type":84},"b3","\u003Cp>You test the drag. It works. You test the hover — on a card you haven&#39;t dragged yet — and it works too. Then you drag a card, let go, and move your mouse back over it.\u003C\u002Fp>",{"id":95,"html":96,"type":84},"b4","\u003Cp>Nothing happens. The lift is just gone. No console error, no warning, and the line that broke it isn&#39;t even in your CSS file.\u003C\u002Fp>",{"id":98,"html":99,"text":99,"type":100,"level":31},"b5","The bug nobody writes on purpose","heading",{"id":102,"html":103,"type":84},"b6","\u003Cp>Here&#39;s the part that makes this one sneaky: both rules are completely correct on their own.\u003C\u002Fp>",{"id":105,"code":106,"type":88,"language":61,"highlight":107},"b7",".card {\n  transition: transform 150ms ease;\n}\n.card:hover {\n  transform: scale(1.03);\n}",[],{"id":109,"code":110,"type":88,"language":89,"highlight":111},"b8","function onDrag(card, x, y) {\n  card.style.transform = `translate(${x}px, ${y}px)`;\n}",[],{"id":113,"html":114,"type":84},"b9","\u003Cp>Nobody misspelled a selector. The specificity math even looks like it should favor the pseudo-class most of the time. But specificity isn&#39;t what decides this fight — an element&#39;s own \u003Ccode>style\u003C\u002Fcode> attribute beats \u003Cem>any\u003C\u002Fem> selector-based rule for the same property, \u003Ccode>:hover\u003C\u002Fcode> included, \u003Ccode>!important\u003C\u002Fcode> aside. The instant \u003Ccode>onDrag\u003C\u002Fcode> runs once, \u003Ccode>card.style.transform\u003C\u002Fcode> is set, and from that point forward your stylesheet&#39;s \u003Ccode>.card:hover { transform: scale(1.03) }\u003C\u002Fcode> has nothing left to write to. It still matches. It still &quot;wins&quot; by every specificity rule you learned. The browser just has nowhere to put its answer, because the inline declaration already owns the property.\u003C\u002Fp>",{"id":116,"html":117,"type":84},"b10","\u003Cp>Open DevTools on a dragged card and look at the Styles panel: you&#39;ll see the \u003Ccode>:hover\u003C\u002Fcode> rule listed, not crossed out, not overridden by anything visible — and still not applied. That&#39;s what makes it brutal to debug. Nothing in the inspector tells you an inline style from a completely unrelated JS file is the reason.\u003C\u002Fp>",{"id":119,"html":120,"text":121,"type":100,"level":31},"b11","The fixes that don&#39;t actually fix it","The fixes that don't actually fix it",{"id":123,"html":124,"type":84},"b12","\u003Cp>The first instinct is \u003Ccode>!important\u003C\u002Fcode>:\u003C\u002Fp>",{"id":126,"code":127,"type":88,"language":61,"highlight":128},"b13",".card:hover {\n  transform: scale(1.03) !important;\n}",[],{"id":130,"html":131,"type":84},"b14","\u003Cp>Now hover wins again — but it wins by \u003Cem>replacing\u003C\u002Fem> the transform outright, not by adding to it. Hover a card mid-drag and it snaps back to wherever \u003Ccode>scale(1.03)\u003C\u002Fcode> alone would place it, then snaps away again on mouseout. You didn&#39;t fix the conflict, you just picked a different single winner for a property that was only ever built to hold one value at a time.\u003C\u002Fp>",{"id":133,"html":134,"type":84},"b15","\u003Cp>The next instinct is to stop fighting CSS and do it all in JS: track \u003Ccode>mouseenter\u003C\u002Fcode>\u002F\u003Ccode>mouseleave\u003C\u002Fcode>, keep the current drag offset and the current hover state in two variables, and rebuild the whole string on every change:\u003C\u002Fp>",{"id":136,"code":137,"type":88,"language":89,"highlight":138},"b16","function render() {\n  card.style.transform = `translate(${x}px, ${y}px) scale(${hovering ? 1.03 : 1})`;\n}",[],{"id":140,"html":141,"type":84},"b17","\u003Cp>This works. It also means you&#39;ve reimplemented \u003Ccode>:hover\u003C\u002Fcode> in JavaScript, with your own event listeners, your own state, and your own transition timing — for a one-line CSS feature the browser already does for free. Add a focus style or a \u003Ccode>prefers-reduced-motion\u003C\u002Fcode> check and the string-building function keeps growing.\u003C\u002Fp>",{"id":143,"html":144,"text":145,"type":100,"level":31},"b18","\u003Ccode>transform\u003C\u002Fcode> was never built to share","transform was never built to share",{"id":147,"html":148,"type":84},"b19","\u003Cp>The actual problem is one level up: \u003Ccode>transform\u003C\u002Fcode> is a single property, and a single property can only hold one value. Whoever writes to it last — JS or CSS — wins the whole thing, not just their part of it. There was no way to say &quot;I only care about position, someone else can own the scale&quot; because position and scale lived inside the exact same string.\u003C\u002Fp>",{"id":150,"html":151,"type":84},"b20","\u003Cp>That stopped being true in 2022. \u003Ccode>translate\u003C\u002Fcode>, \u003Ccode>rotate\u003C\u002Fcode>, and \u003Ccode>scale\u003C\u002Fcode> are now their own CSS properties, independent of \u003Ccode>transform\u003C\u002Fcode> and independent of each other, supported in every major engine (Chrome and Edge 104+, Firefox 72+, Safari 14.1+) — which by now means every browser your analytics will show you. Give the drag handler one property and the hover rule a different one:\u003C\u002Fp>",{"id":153,"code":154,"type":88,"language":61,"highlight":155},"b21",".card {\n  scale: 1;\n  transition: scale 150ms ease;\n}\n.card:hover {\n  scale: 1.03;\n}",[],{"id":157,"code":158,"type":88,"language":89,"highlight":159},"b22","function onDrag(card, x, y) {\n  card.style.translate = `${x}px ${y}px`;\n}",[],{"id":161,"html":162,"type":84},"b23","\u003Cp>No shared string, no state to track, no \u003Ccode>!important\u003C\u002Fcode>. JS writes \u003Ccode>translate\u003C\u002Fcode> every pointermove; the browser handles \u003Ccode>:hover\u003C\u002Fcode> writing \u003Ccode>scale\u003C\u002Fcode> the way it always has. Drag a card and its hover lift still fires, because for the first time the two concerns were never fighting for the same property to begin with.\u003C\u002Fp>",{"id":164,"html":165,"type":84},"b24","\u003C!-- playground:start -->",{"id":167,"html":168,"text":168,"type":100,"level":31},"b25","🎮 Try it yourself",{"id":170,"html":171,"type":84},"b26","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-individual-transform-properties\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":173,"html":174,"type":84},"b27","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":176,"html":177,"type":84},"b28","\u003C!-- playground:end -->",{"id":179,"html":180,"type":84},"b29","\u003Cp>The browser applies these in one fixed order — \u003Ccode>translate\u003C\u002Fcode>, then \u003Ccode>rotate\u003C\u002Fcode>, then \u003Ccode>scale\u003C\u002Fcode>, then whatever&#39;s left in \u003Ccode>transform\u003C\u002Fcode> — no matter what order you declare them in your code. That&#39;s worth knowing if you still reach for \u003Ccode>transform\u003C\u002Fcode> for something the individual properties don&#39;t cover, like \u003Ccode>skew()\u003C\u002Fcode> or a raw \u003Ccode>matrix()\u003C\u002Fcode>: it always applies last, on top of the other three, not wherever you happened to write it.\u003C\u002Fp>",{"id":182,"html":183,"text":183,"type":100,"level":31},"b30","Where this hides beyond drag-and-drop",{"id":185,"html":186,"type":84},"b31","\u003Cp>Dragging is the clearest version, but the same collision shows up anywhere JS and CSS both think they own motion on the same element:\u003C\u002Fp>",{"id":188,"type":189,"items":190,"ordered":18},"b32","list",[191,192,193],"A parallax or scroll-linked effect that sets \u003Ccode>transform\u003C\u002Fcode> every frame, on a card that also has a CSS \u003Ccode>:focus\u003C\u002Fcode> or \u003Ccode>:active\u003C\u002Fcode> micro-interaction.","A FLIP-style animation library that writes a starting \u003Ccode>transform\u003C\u002Fcode> via JS, racing a stylesheet transition meant to animate it away.","A carousel or swiper that positions slides with inline \u003Ccode>transform\u003C\u002Fcode>, on a component whose design system also wants a CSS-only hover state on each slide.",{"id":195,"html":196,"type":84},"b33","\u003Cp>The symptom is always the same: a CSS rule that&#39;s &quot;obviously&quot; correct, matches, shows up in DevTools — and just doesn&#39;t apply, because something else got to the property first.\u003C\u002Fp>",{"id":198,"html":199,"type":84},"b34","\u003C!-- quiz:start -->",{"id":201,"html":202,"text":202,"type":100,"level":31},"b35","🧠 Test yourself",{"id":204,"html":205,"type":84},"b36","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-individual-transform-properties\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":207,"html":208,"type":84},"b37","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":210,"html":211,"type":84},"b38","\u003C!-- quiz:end -->",{"id":213,"html":214,"text":215,"type":100,"level":31},"b39","What this doesn&#39;t fix","What this doesn't fix",{"id":217,"html":218,"type":84},"b40","\u003Cp>Individual transform properties don&#39;t replace \u003Ccode>transform\u003C\u002Fcode> — \u003Ccode>skew()\u003C\u002Fcode>, \u003Ccode>matrix()\u003C\u002Fcode>, and \u003Ccode>matrix3d()\u003C\u002Fcode> still only exist inside it, and if two different things both need to animate the \u003Cem>same\u003C\u002Fem> axis (two JS handlers both wanting to own \u003Ccode>translate\u003C\u002Fcode>, say), you&#39;re back to needing one source of truth for that value. What they fix is the specific, common case of position\u002Frotation\u002Fscale each belonging to a different piece of code — which covers most of the JS-meets-CSS collisions you&#39;ll actually hit.\u003C\u002Fp>",{"id":220,"html":221,"type":84},"b41","\u003Cp>Next time a hover or focus style &quot;randomly&quot; stops working on an element some other script touches, check whether that script is writing to \u003Ccode>transform\u003C\u002Fcode> — and if it is, see if giving it \u003Ccode>translate\u003C\u002Fcode> instead buys your CSS room to breathe again. What&#39;s the weirdest &quot;it matches in DevTools but doesn&#39;t apply&quot; bug you&#39;ve chased down? I&#39;d like to hear it.\u003C\u002Fp>",{"id":223,"type":224},"b42","divider",{"id":226,"html":227,"type":84},"b43","\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":229,"html":230,"type":84},"b44","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":232,"type":189,"items":233,"ordered":18},"b45",[234,235,236],"⭐ \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 product card. On hover, it should lift a little — `transform: scale(1.03)`, a transition, done in five minutes. Then the ticket comes in: cards need to be draggable, so you add a `pointermove` handler that keeps the card glued to the cursor:\n\n```js\ncard.style.transform = `translate(${x}px, ${y}px)`;\n```\n\nYou test the drag. It works. You test the hover — on a card you haven't dragged yet — and it works too. Then you drag a card, let go, and move your mouse back over it.\n\nNothing happens. The lift is just gone. No console error, no warning, and the line that broke it isn't even in your CSS file.\n\n## The bug nobody writes on purpose\n\nHere's the part that makes this one sneaky: both rules are completely correct on their own.\n\n```css\n.card {\n  transition: transform 150ms ease;\n}\n.card:hover {\n  transform: scale(1.03);\n}\n```\n\n```js\nfunction onDrag(card, x, y) {\n  card.style.transform = `translate(${x}px, ${y}px)`;\n}\n```\n\nNobody misspelled a selector. The specificity math even looks like it should favor the pseudo-class most of the time. But specificity isn't what decides this fight — an element's own `style` attribute beats *any* selector-based rule for the same property, `:hover` included, `!important` aside. The instant `onDrag` runs once, `card.style.transform` is set, and from that point forward your stylesheet's `.card:hover { transform: scale(1.03) }` has nothing left to write to. It still matches. It still \"wins\" by every specificity rule you learned. The browser just has nowhere to put its answer, because the inline declaration already owns the property.\n\nOpen DevTools on a dragged card and look at the Styles panel: you'll see the `:hover` rule listed, not crossed out, not overridden by anything visible — and still not applied. That's what makes it brutal to debug. Nothing in the inspector tells you an inline style from a completely unrelated JS file is the reason.\n\n## The fixes that don't actually fix it\n\nThe first instinct is `!important`:\n\n```css\n.card:hover {\n  transform: scale(1.03) !important;\n}\n```\n\nNow hover wins again — but it wins by *replacing* the transform outright, not by adding to it. Hover a card mid-drag and it snaps back to wherever `scale(1.03)` alone would place it, then snaps away again on mouseout. You didn't fix the conflict, you just picked a different single winner for a property that was only ever built to hold one value at a time.\n\nThe next instinct is to stop fighting CSS and do it all in JS: track `mouseenter`\u002F`mouseleave`, keep the current drag offset and the current hover state in two variables, and rebuild the whole string on every change:\n\n```js\nfunction render() {\n  card.style.transform = `translate(${x}px, ${y}px) scale(${hovering ? 1.03 : 1})`;\n}\n```\n\nThis works. It also means you've reimplemented `:hover` in JavaScript, with your own event listeners, your own state, and your own transition timing — for a one-line CSS feature the browser already does for free. Add a focus style or a `prefers-reduced-motion` check and the string-building function keeps growing.\n\n## `transform` was never built to share\n\nThe actual problem is one level up: `transform` is a single property, and a single property can only hold one value. Whoever writes to it last — JS or CSS — wins the whole thing, not just their part of it. There was no way to say \"I only care about position, someone else can own the scale\" because position and scale lived inside the exact same string.\n\nThat stopped being true in 2022. `translate`, `rotate`, and `scale` are now their own CSS properties, independent of `transform` and independent of each other, supported in every major engine (Chrome and Edge 104+, Firefox 72+, Safari 14.1+) — which by now means every browser your analytics will show you. Give the drag handler one property and the hover rule a different one:\n\n```css\n.card {\n  scale: 1;\n  transition: scale 150ms ease;\n}\n.card:hover {\n  scale: 1.03;\n}\n```\n\n```js\nfunction onDrag(card, x, y) {\n  card.style.translate = `${x}px ${y}px`;\n}\n```\n\nNo shared string, no state to track, no `!important`. JS writes `translate` every pointermove; the browser handles `:hover` writing `scale` the way it always has. Drag a card and its hover lift still fires, because for the first time the two concerns were never fighting for the same property to begin with.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-individual-transform-properties\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\nThe browser applies these in one fixed order — `translate`, then `rotate`, then `scale`, then whatever's left in `transform` — no matter what order you declare them in your code. That's worth knowing if you still reach for `transform` for something the individual properties don't cover, like `skew()` or a raw `matrix()`: it always applies last, on top of the other three, not wherever you happened to write it.\n\n## Where this hides beyond drag-and-drop\n\nDragging is the clearest version, but the same collision shows up anywhere JS and CSS both think they own motion on the same element:\n\n- A parallax or scroll-linked effect that sets `transform` every frame, on a card that also has a CSS `:focus` or `:active` micro-interaction.\n- A FLIP-style animation library that writes a starting `transform` via JS, racing a stylesheet transition meant to animate it away.\n- A carousel or swiper that positions slides with inline `transform`, on a component whose design system also wants a CSS-only hover state on each slide.\n\nThe symptom is always the same: a CSS rule that's \"obviously\" correct, matches, shows up in DevTools — and just doesn't apply, because something else got to the property first.\n\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-individual-transform-properties\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## What this doesn't fix\n\nIndividual transform properties don't replace `transform` — `skew()`, `matrix()`, and `matrix3d()` still only exist inside it, and if two different things both need to animate the *same* axis (two JS handlers both wanting to own `translate`, say), you're back to needing one source of truth for that value. What they fix is the specific, common case of position\u002Frotation\u002Fscale each belonging to a different piece of code — which covers most of the JS-meets-CSS collisions you'll actually hit.\n\nNext time a hover or focus style \"randomly\" stops working on an element some other script touches, check whether that script is writing to `transform` — and if it is, see if giving it `translate` instead buys your CSS room to breathe again. What's the weirdest \"it matches in DevTools but doesn't apply\" bug you've chased down? I'd like to hear it.\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":239,"description":240},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-individual-transform-properties","A CSS :hover scale and a JS drag handler both write to `transform` — and the moment JS sets it, the hover rule silently stops working, no error, no warning. The `translate`, `rotat","01a149cb-71f4-7712-aa77-bad8c60fb61d",{"id":243,"locked":18},"01a149cb-721a-769a-837b-f68a0687b8f9",[245],{"id":246,"slug":46,"title":247,"_count":248},"01a149cb-7232-7776-b777-f97c0e816ee7","translate, rotate, and scale vs. one shared transform",{"questions":249},7,[251],{"locale":13,"slug":46},{"id":246,"slug":46,"title":247,"_count":253,"questionCount":249},{"questions":249},[255,257,259,263,267,269,273,277,281,285,289,292],{"slug":70,"name":71,"articles":256},150,{"slug":67,"name":68,"articles":258},123,{"slug":260,"name":261,"articles":262},"frontend","Frontend",87,{"slug":264,"name":265,"articles":266},"tutorial","Tutorial",57,{"slug":61,"name":65,"articles":268},45,{"slug":270,"name":271,"articles":272},"performance","Performance",25,{"slug":274,"name":275,"articles":276},"typescript","Typescript",20,{"slug":278,"name":279,"articles":280},"react","React",18,{"slug":282,"name":283,"articles":284},"browser","Browser",14,{"slug":286,"name":287,"articles":288},"programming","Programming",12,{"slug":290,"name":291,"articles":288},"node","Node",{"slug":293,"name":294,"articles":295},"accessibility","Accessibility",10,[],{"items":298,"meta":425},[299,311,329,348,366,384,405],{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":300,"likeCount":19,"commentCount":19,"author":301,"vertical":302,"topic":303,"tags":304,"_count":309,"playground":310,"hasQuiz":17,"hasPlayground":17},38,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[305,306,307,308],{"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":312,"slug":313,"title":314,"subtitle":48,"excerpt":315,"coverUrl":316,"locale":13,"readingMinutes":249,"publishedAt":317,"viewCount":318,"likeCount":19,"commentCount":19,"author":319,"vertical":320,"topic":321,"tags":322,"_count":327,"playground":328,"hasQuiz":17,"hasPlayground":17},"01a133e2-3e89-757c-ae01-38a8ad8aa448","forced-colors-high-contrast-mode","Stop Shipping Custom UI That Disappears in `forced-colors` Mode","Windows High Contrast Mode doesn't just boost contrast — it strips your backgrounds, shadows, and colors outright. Here's what `forced-colors` actually does, why a styled \u003Cdiv> vanishes while a real \u003Cbutton> survives, and the fix.","\u002Fmedia\u002Fcovers\u002Fforced-colors-high-contrast-mode.png","2026-10-18T10:15:03.062Z",132,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[323,324,325,326],{"slug":61,"name":65,"color":48},{"slug":293,"name":294,"color":48},{"slug":70,"name":71,"color":48},{"slug":260,"name":261,"color":48},{"assessments":76},{"slug":313},{"id":330,"slug":331,"title":332,"subtitle":48,"excerpt":333,"coverUrl":334,"locale":13,"readingMinutes":335,"publishedAt":336,"viewCount":337,"likeCount":19,"commentCount":19,"author":338,"vertical":339,"topic":340,"tags":341,"_count":346,"playground":347,"hasQuiz":17,"hasPlayground":17},"01a12aed-3621-77cf-8827-f0fa7f687105","css-contain-rendering-isolation","Stop Letting One Widget Reflow the Whole Page. `contain` Scopes It.","A live counter or chat widget shouldn't make the rest of your page recompute layout every time it updates. The CSS contain property draws a hard boundary the browser can actually trust — here's what it promises, and the one gotcha that breaks pages when people misuse it.","\u002Fmedia\u002Fcovers\u002Fcss-contain-rendering-isolation.png",6,"2026-10-16T09:23:09.005Z",180,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[342,343,344,345],{"slug":61,"name":65,"color":48},{"slug":270,"name":271,"color":48},{"slug":70,"name":71,"color":48},{"slug":260,"name":261,"color":48},{"assessments":76},{"slug":331},{"id":349,"slug":350,"title":351,"subtitle":48,"excerpt":352,"coverUrl":353,"locale":13,"readingMinutes":51,"publishedAt":354,"viewCount":355,"likeCount":19,"commentCount":19,"author":356,"vertical":357,"topic":358,"tags":359,"_count":364,"playground":365,"hasQuiz":17,"hasPlayground":17},"01a10aaf-5ec4-700b-bbd2-5fc17c27d4e0","css-will-change-performance-hint","You Added `will-change` to Fix the Jank. You Made It Worse.","will-change: transform looks like a free performance switch. Applied broadly or left on permanently, it does the opposite — here's the narrow, temporary pattern that actually works.","\u002Fmedia\u002Fcovers\u002Fcss-will-change-performance-hint.png","2026-10-10T12:12:00.282Z",249,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[360,361,362,363],{"slug":61,"name":65,"color":48},{"slug":270,"name":271,"color":48},{"slug":70,"name":71,"color":48},{"slug":260,"name":261,"color":48},{"assessments":76},{"slug":350},{"id":367,"slug":368,"title":369,"subtitle":48,"excerpt":370,"coverUrl":371,"locale":13,"readingMinutes":335,"publishedAt":372,"viewCount":373,"likeCount":19,"commentCount":19,"author":374,"vertical":375,"topic":376,"tags":377,"_count":382,"playground":383,"hasQuiz":17,"hasPlayground":17},"01a106cb-f870-70ac-892c-2f9a55db26b3","prefers-reduced-motion-respect-the-setting","You're Animating for Everyone. `prefers-reduced-motion` Fixes That.","Most animated UIs never check whether the visitor asked for less motion. `prefers-reduced-motion` is the CSS media feature that tells you — and a fail-closed pattern keeps you honest even when a browser doesn't support it.","\u002Fmedia\u002Fcovers\u002Fprefers-reduced-motion-respect-the-setting.png","2026-10-09T06:10:28.564Z",468,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[378,379,380,381],{"slug":61,"name":65,"color":48},{"slug":293,"name":294,"color":48},{"slug":70,"name":71,"color":48},{"slug":260,"name":261,"color":48},{"assessments":76},{"slug":368},{"id":385,"slug":386,"title":387,"subtitle":48,"excerpt":388,"coverUrl":389,"locale":13,"readingMinutes":335,"publishedAt":390,"viewCount":391,"likeCount":19,"commentCount":19,"author":392,"vertical":393,"topic":394,"tags":397,"_count":403,"playground":404,"hasQuiz":17,"hasPlayground":17},"01a0ec73-289b-72dc-b408-cb3deb2a38a9","details-name-exclusive-accordion","Stop Hand-Rolling an Exclusive Accordion. `\u003Cdetails name>` Does It","You've probably shipped an accordion with a hand-rolled 'close the other panels' loop in JavaScript. The `name` attribute on \u003Cdetails> does it natively — no JS, no ARIA plumbing.","\u002Fmedia\u002Fcovers\u002Fdetails-name-exclusive-accordion.png","2026-10-04T12:03:17.379Z",470,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":395,"name":396},"html","HTML",[398,400,401,402],{"slug":395,"name":399,"color":48},"Html",{"slug":61,"name":65,"color":48},{"slug":70,"name":71,"color":48},{"slug":293,"name":294,"color":48},{"assessments":76},{"slug":386},{"id":406,"slug":407,"title":408,"subtitle":48,"excerpt":409,"coverUrl":410,"locale":13,"readingMinutes":51,"publishedAt":411,"viewCount":412,"likeCount":19,"commentCount":19,"author":413,"vertical":414,"topic":415,"tags":416,"_count":423,"playground":424,"hasQuiz":17,"hasPlayground":17},"01a0ec73-1ead-73da-8d85-b6b797fafb03","css-text-box-trim-headline-gap","Stop Fixing Headline Gaps With Negative Margins. Use `text-box-trim`","Line-height reserves space for accent marks your headline never uses — a hand-tuned negative margin only fixes it for one font at one size. text-box-trim does, natively, from the font's own metrics.","\u002Fmedia\u002Fcovers\u002Fcss-text-box-trim-headline-gap.png","2026-10-02T06:10:31.028Z",418,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[417,418,419,420],{"slug":61,"name":65,"color":48},{"slug":70,"name":71,"color":48},{"slug":260,"name":261,"color":48},{"slug":421,"name":422,"color":48},"typography","Typography",{"assessments":76},{"slug":407},{"page":76,"perPage":249,"total":268,"totalPages":249},"\u003Cdiv class=\"shj shj-lang-js shj-oneline\" data-lang=\"js\">card\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>style\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>transform \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-str\">`translate(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">${\u003C\u002Fspan>x\u003Cspan class=\"shj-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-str\">px, \u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">${\u003C\u002Fspan>y\u003Cspan class=\"shj-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-str\">px)`\u003C\u002Fspan>;\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\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>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-var\">.card\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">transition\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">transform\u003C\u002Fspan> \u003Cspan class=\"shj-num\">150\u003C\u002Fspan>ms \u003Cspan class=\"shj-num\">ease\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">.card\u003C\u002Fspan>\u003Cspan class=\"shj-var\">:hover\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">transform\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">scale\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">1.03\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\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>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">onDrag\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>card\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> x\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> y\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  card\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>style\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>transform \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-str\">`translate(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">${\u003C\u002Fspan>x\u003Cspan class=\"shj-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-str\">px, \u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">${\u003C\u002Fspan>y\u003Cspan class=\"shj-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-str\">px)`\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\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\">\u003Cspan class=\"shj-var\">.card\u003C\u002Fspan>\u003Cspan class=\"shj-var\">:hover\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">transform\u003C\u002Fspan>: \u003Cspan class=\"shj-func\">scale\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">1.03\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> !\u003Cspan class=\"shj-num\">important\u003C\u002Fspan>;\n\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>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">render\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  card\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>style\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>transform \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-str\">`translate(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">${\u003C\u002Fspan>x\u003Cspan class=\"shj-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-str\">px, \u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">${\u003C\u002Fspan>y\u003Cspan class=\"shj-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-str\">px) scale(\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">${\u003C\u002Fspan>hovering \u003Cspan class=\"shj-oper\">?\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1.03\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-str\">)`\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-css shj-multiline\" data-lang=\"css\">\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-var\">.card\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">scale\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">1\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-var\">transition\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">scale\u003C\u002Fspan> \u003Cspan class=\"shj-num\">150\u003C\u002Fspan>ms \u003Cspan class=\"shj-num\">ease\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-var\">.card\u003C\u002Fspan>\u003Cspan class=\"shj-var\">:hover\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-var\">scale\u003C\u002Fspan>: \u003Cspan class=\"shj-num\">1.03\u003C\u002Fspan>;\n\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>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">onDrag\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>card\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> x\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> y\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  card\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>style\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>translate \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-str\">`\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">${\u003C\u002Fspan>x\u003Cspan class=\"shj-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-str\">px \u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">${\u003C\u002Fspan>y\u003Cspan class=\"shj-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-str\">px`\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":434},[]]