[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-css-starting-style-entry-animations":32,"quiz-article-css-starting-style-entry-animations":48},[4,20],{"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","Build. Learn. Ship.","Practical courses, engineering-grade articles and open-source tools for people who ship.","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.","Structured paths, listening drills and spaced repetition that actually sticks.","amber-400","⌘",[13,15,29],"es",{"courses":17,"paths":17,"articles":18,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":17},2,{"id":33,"slug":34,"kind":35,"title":36,"description":37,"config":38,"verticalId":5,"vertical":43,"course":40,"_count":44,"access":45,"attempts":47,"questionCount":39},"019fe776-7919-7118-9b6a-a4301943455f","css-starting-style-entry-animations","PRACTICE_QUIZ","@starting-style entry animations","The @starting-style at-rule gives CSS transitions a \"before\" state for elements that have never rendered before, replacing the double-rAF and setTimeout hacks developers used to trigger enter animations. Know what it actually applies to, what it needs alongside it, and where its limits are.",{"questionCount":39,"timeLimitSec":40,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":41,"maxAttempts":40,"revealAnswers":42,"allowFlagging":18,"allowBacktracking":17},6,null,70,"IMMEDIATE",{"slug":6,"name":7},{"questions":39},{"allowed":17,"reason":46},"FREE",[],{"id":49,"slug":34,"title":50,"subtitle":40,"excerpt":51,"coverUrl":52,"locale":13,"readingMinutes":53,"publishedAt":54,"viewCount":55,"likeCount":19,"commentCount":19,"author":56,"vertical":61,"topic":62,"tags":65,"_count":77,"playground":79,"body":81,"bodyMd":205,"seo":206,"translationGroupId":210,"thread":211,"assessments":213,"translations":216,"quiz":218},"019fe660-7d17-745b-812d-b1e81a1b2b85","You use a setTimeout to trigger CSS entry animations. `@starting-style` makes it unnecessary.","Before `@starting-style`, the moment an element entered the DOM was invisible to CSS — transitions had no 'before' state to interpolate from. The at-rule gives you one, natively, with no JavaScript required.","\u002Fmedia\u002Fcovers\u002Fcss-starting-style-entry-animations.png",4,"2026-07-25T08:13:02.298Z",54,{"id":57,"name":58,"username":59,"avatarUrl":40,"headline":60},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":63,"name":64},"css","CSS",[66,68,71,74],{"slug":63,"name":67,"color":40},"Css",{"slug":69,"name":70,"color":40},"webdev","Webdev",{"slug":72,"name":73,"color":40},"frontend","Frontend",{"slug":75,"name":76,"color":40},"javascript","Javascript",{"assessments":78},1,{"slug":34,"title":80},"@starting-style — interactive playground",{"blocks":82,"version":78},[83,87,93,96,99,104,107,111,114,117,120,123,126,129,133,136,139,143,146,150,153,157,160,163,166,169,172,175,178,181,184,187,190,193,196],{"id":84,"html":85,"type":86},"b1","\u003Cp>Here is a pattern almost every frontend developer has written:\u003C\u002Fp>","paragraph",{"id":88,"code":89,"type":90,"language":91,"highlight":92},"b2","el.style.opacity = '0';\nel.style.transform = 'translateY(8px)';\ndocument.body.appendChild(el);\n\nrequestAnimationFrame(() => {\n  requestAnimationFrame(() => {\n    el.style.opacity = '1';\n    el.style.transform = 'translateY(0)';\n  });\n});","code","js",[],{"id":94,"html":95,"type":86},"b3","\u003Cp>Two \u003Ccode>requestAnimationFrame\u003C\u002Fcode> calls. Both are a hack. The first lets the browser commit the initial state to the render tree; the second triggers the transition. You may know the alternatives — a zero-millisecond \u003Ccode>setTimeout\u003C\u002Fcode>, a \u003Ccode>void el.offsetWidth\u003C\u002Fcode> reflow, or adding a class one tick after mount — but all of them are working around the same limitation.\u003C\u002Fp>",{"id":97,"html":98,"type":86},"b4","\u003Cp>CSS \u003Ccode>transition\u003C\u002Fcode> interpolates between a \u003Cem>before\u003C\u002Fem> state and an \u003Cem>after\u003C\u002Fem> state. When an element first appears in the DOM, there is no before state — the element simply did not exist. \u003Ccode>@starting-style\u003C\u002Fcode> was introduced to fill that gap.\u003C\u002Fp>",{"id":100,"html":101,"text":102,"type":103,"level":31},"b5","What \u003Ccode>@starting-style\u003C\u002Fcode> does","What @starting-style does","heading",{"id":105,"html":106,"type":86},"b6","\u003Cp>\u003Ccode>@starting-style\u003C\u002Fcode> is an at-rule that lets you declare styles for an element \u003Cem>before its first rendered frame\u003C\u002Fem>. The browser uses those styles as the starting point for transitions when the element enters the page.\u003C\u002Fp>",{"id":108,"code":109,"type":90,"language":63,"highlight":110},"b7",".notification {\n  opacity: 1;\n  transform: translateY(0);\n  transition: opacity 0.3s ease, transform 0.3s ease;\n}\n\n@starting-style {\n  .notification {\n    opacity: 0;\n    transform: translateY(12px);\n  }\n}",[],{"id":112,"html":113,"type":86},"b8","\u003Cp>When \u003Ccode>.notification\u003C\u002Fcode> is inserted into the DOM, the browser reads \u003Ccode>@starting-style\u003C\u002Fcode> for the initial values and immediately transitions to the regular computed styles. The result is a smooth fade-and-slide-up — with zero JavaScript. No double rAF, no class toggle, no forced reflow.\u003C\u002Fp>",{"id":115,"html":116,"type":86},"b9","\u003C!-- playground:start -->",{"id":118,"html":119,"text":119,"type":103,"level":31},"b10","🎮 Try it yourself",{"id":121,"html":122,"type":86},"b11","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fposts\u002F2026-07-25-css-starting-style-entry-animations\u002Fplayground\u002F\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":124,"html":125,"type":86},"b12","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":127,"html":128,"type":86},"b13","\u003C!-- playground:end -->",{"id":130,"html":131,"text":132,"type":103,"level":31},"b14","Animating elements out of \u003Ccode>display: none\u003C\u002Fcode>","Animating elements out of display: none",{"id":134,"html":135,"type":86},"b15","\u003Cp>The second major use case is toggling visibility with \u003Ccode>display: none\u003C\u002Fcode>. Setting an element to \u003Ccode>display: block\u003C\u002Fcode> used to be incompatible with transitions: the element jumped from &quot;not painted&quot; to &quot;painted&quot; in a single frame, giving the transition engine nothing to work with.\u003C\u002Fp>",{"id":137,"html":138,"type":86},"b16","\u003Cp>With \u003Ccode>@starting-style\u003C\u002Fcode> and the new \u003Ccode>allow-discrete\u003C\u002Fcode> keyword, you can animate both the enter and exit:\u003C\u002Fp>",{"id":140,"code":141,"type":90,"language":63,"highlight":142},"b17",".drawer {\n  display: none;\n  opacity: 0;\n  transform: translateX(-16px);\n  transition:\n    opacity 0.25s ease,\n    transform 0.25s ease,\n    display 0.25s allow-discrete;\n}\n\n.drawer.open {\n  display: block;\n  opacity: 1;\n  transform: translateX(0);\n}\n\n@starting-style {\n  .drawer.open {\n    opacity: 0;\n    transform: translateX(-16px);\n  }\n}",[],{"id":144,"html":145,"type":86},"b18","\u003Cp>\u003Ccode>allow-discrete\u003C\u002Fcode> tells the browser that \u003Ccode>display\u003C\u002Fcode> is being deliberately transitioned: on enter it switches to \u003Ccode>block\u003C\u002Fcode> immediately so the element is visible during the animation; on exit it holds \u003Ccode>block\u003C\u002Fcode> until the transition completes before switching to \u003Ccode>none\u003C\u002Fcode>. Combined with \u003Ccode>@starting-style\u003C\u002Fcode> for the enter side, you get matching enter and exit animations from purely declarative CSS.\u003C\u002Fp>",{"id":147,"html":148,"text":149,"type":103,"level":31},"b19","With native \u003Ccode>popover\u003C\u002Fcode> and \u003Ccode>&lt;dialog&gt;\u003C\u002Fcode>","With native popover and \u003Cdialog>",{"id":151,"html":152,"type":86},"b20","\u003Cp>\u003Ccode>@starting-style\u003C\u002Fcode> pairs especially well with the Popover API and \u003Ccode>&lt;dialog&gt;\u003C\u002Fcode>, which toggle between top-layer states. There&#39;s one extra property to handle: \u003Ccode>overlay\u003C\u002Fcode>, which controls whether the element stays in the top layer while an exit animation runs.\u003C\u002Fp>",{"id":154,"code":155,"type":90,"language":63,"highlight":156},"b21","[popover] {\n  opacity: 0;\n  transform: scale(0.95);\n  transition:\n    opacity 0.2s ease,\n    transform 0.2s ease,\n    display 0.2s allow-discrete,\n    overlay 0.2s allow-discrete;\n}\n\n[popover]:popover-open {\n  opacity: 1;\n  transform: scale(1);\n}\n\n@starting-style {\n  [popover]:popover-open {\n    opacity: 0;\n    transform: scale(0.95);\n  }\n}",[],{"id":158,"html":159,"type":86},"b22","\u003Cp>Without transitioning \u003Ccode>overlay\u003C\u002Fcode>, the popover is pulled out of the top layer before the exit transition finishes — it disappears instantly. With \u003Ccode>allow-discrete\u003C\u002Fcode> on both \u003Ccode>display\u003C\u002Fcode> and \u003Ccode>overlay\u003C\u002Fcode>, the element stays rendered and on top until the animation completes, then the browser cleans it up. You get a polished open\u002Fclose pair with no JavaScript involvement at all.\u003C\u002Fp>",{"id":161,"html":162,"text":162,"type":103,"level":31},"b23","Browser support and graceful degradation",{"id":164,"html":165,"type":86},"b24","\u003Cp>\u003Ccode>@starting-style\u003C\u002Fcode> shipped in \u003Cstrong>Chrome 117\u003C\u002Fstrong> (September 2023), \u003Cstrong>Firefox 129\u003C\u002Fstrong> (August 2024), and \u003Cstrong>Safari 17.5\u003C\u002Fstrong> (May 2024). It has broadly available baseline support as of mid-2025.\u003C\u002Fp>",{"id":167,"html":168,"type":86},"b25","\u003Cp>For older browsers, the \u003Ccode>@starting-style\u003C\u002Fcode> block is silently ignored — elements appear without an entry animation rather than breaking. That is exactly the right degradation: the animation is an enhancement, not a requirement. You do not need a polyfill.\u003C\u002Fp>",{"id":170,"html":171,"type":86},"b26","\u003C!-- quiz:start -->",{"id":173,"html":174,"text":174,"type":103,"level":31},"b27","🧠 Test yourself",{"id":176,"html":177,"type":86},"b28","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fquiz\u002Ftake.html?post=2026-07-25-css-starting-style-entry-animations\">Take the 6-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":179,"html":180,"type":86},"b29","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":182,"html":183,"type":86},"b30","\u003C!-- quiz:end -->",{"id":185,"html":186,"text":186,"type":103,"level":31},"b31","The takeaway",{"id":188,"html":189,"type":86},"b32","\u003Cp>The double-rAF trick and the zero-millisecond \u003Ccode>setTimeout\u003C\u002Fcode> exist because CSS transitions need a &quot;before&quot; state, and there was no native way to provide one for elements entering the DOM. \u003Ccode>@starting-style\u003C\u002Fcode> is that native way. If your code applies a class one tick after mount, or toggles a data attribute to kick off an enter animation, look at whether \u003Ccode>@starting-style\u003C\u002Fcode> can replace it. The browser handles the timing correctly by definition; your JavaScript approximation might not.\u003C\u002Fp>",{"id":191,"type":192},"b33","divider",{"id":194,"html":195,"type":86},"b34","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":197,"type":198,"items":199,"ordered":18},"b35","list",[200,201,202,203,204],"⭐ \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>","💼 \u003Cstrong>LinkedIn\u003C\u002Fstrong> — \u003Ca href=\"https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F\">linkedin.com\u002Fin\u002Fparsa-jiravand\u003C\u002Fa>","✉️ \u003Cstrong>Email\u003C\u002Fstrong> (work &amp; contract inquiries): \u003Ca href=\"mailto:bestpractice2026@gmail.com\">bestpractice2026@gmail.com\u003C\u002Fa>","Here is a pattern almost every frontend developer has written:\n\n```js\nel.style.opacity = '0';\nel.style.transform = 'translateY(8px)';\ndocument.body.appendChild(el);\n\nrequestAnimationFrame(() => {\n  requestAnimationFrame(() => {\n    el.style.opacity = '1';\n    el.style.transform = 'translateY(0)';\n  });\n});\n```\n\nTwo `requestAnimationFrame` calls. Both are a hack. The first lets the browser commit the initial state to the render tree; the second triggers the transition. You may know the alternatives — a zero-millisecond `setTimeout`, a `void el.offsetWidth` reflow, or adding a class one tick after mount — but all of them are working around the same limitation.\n\nCSS `transition` interpolates between a *before* state and an *after* state. When an element first appears in the DOM, there is no before state — the element simply did not exist. `@starting-style` was introduced to fill that gap.\n\n## What `@starting-style` does\n\n`@starting-style` is an at-rule that lets you declare styles for an element *before its first rendered frame*. The browser uses those styles as the starting point for transitions when the element enters the page.\n\n```css\n.notification {\n  opacity: 1;\n  transform: translateY(0);\n  transition: opacity 0.3s ease, transform 0.3s ease;\n}\n\n@starting-style {\n  .notification {\n    opacity: 0;\n    transform: translateY(12px);\n  }\n}\n```\n\nWhen `.notification` is inserted into the DOM, the browser reads `@starting-style` for the initial values and immediately transitions to the regular computed styles. The result is a smooth fade-and-slide-up — with zero JavaScript. No double rAF, no class toggle, no forced reflow.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fposts\u002F2026-07-25-css-starting-style-entry-animations\u002Fplayground\u002F)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Animating elements out of `display: none`\n\nThe second major use case is toggling visibility with `display: none`. Setting an element to `display: block` used to be incompatible with transitions: the element jumped from \"not painted\" to \"painted\" in a single frame, giving the transition engine nothing to work with.\n\nWith `@starting-style` and the new `allow-discrete` keyword, you can animate both the enter and exit:\n\n```css\n.drawer {\n  display: none;\n  opacity: 0;\n  transform: translateX(-16px);\n  transition:\n    opacity 0.25s ease,\n    transform 0.25s ease,\n    display 0.25s allow-discrete;\n}\n\n.drawer.open {\n  display: block;\n  opacity: 1;\n  transform: translateX(0);\n}\n\n@starting-style {\n  .drawer.open {\n    opacity: 0;\n    transform: translateX(-16px);\n  }\n}\n```\n\n`allow-discrete` tells the browser that `display` is being deliberately transitioned: on enter it switches to `block` immediately so the element is visible during the animation; on exit it holds `block` until the transition completes before switching to `none`. Combined with `@starting-style` for the enter side, you get matching enter and exit animations from purely declarative CSS.\n\n## With native `popover` and `\u003Cdialog>`\n\n`@starting-style` pairs especially well with the Popover API and `\u003Cdialog>`, which toggle between top-layer states. There's one extra property to handle: `overlay`, which controls whether the element stays in the top layer while an exit animation runs.\n\n```css\n[popover] {\n  opacity: 0;\n  transform: scale(0.95);\n  transition:\n    opacity 0.2s ease,\n    transform 0.2s ease,\n    display 0.2s allow-discrete,\n    overlay 0.2s allow-discrete;\n}\n\n[popover]:popover-open {\n  opacity: 1;\n  transform: scale(1);\n}\n\n@starting-style {\n  [popover]:popover-open {\n    opacity: 0;\n    transform: scale(0.95);\n  }\n}\n```\n\nWithout transitioning `overlay`, the popover is pulled out of the top layer before the exit transition finishes — it disappears instantly. With `allow-discrete` on both `display` and `overlay`, the element stays rendered and on top until the animation completes, then the browser cleans it up. You get a polished open\u002Fclose pair with no JavaScript involvement at all.\n\n## Browser support and graceful degradation\n\n`@starting-style` shipped in **Chrome 117** (September 2023), **Firefox 129** (August 2024), and **Safari 17.5** (May 2024). It has broadly available baseline support as of mid-2025.\n\nFor older browsers, the `@starting-style` block is silently ignored — elements appear without an entry animation rather than breaking. That is exactly the right degradation: the animation is an enhancement, not a requirement. You do not need a polyfill.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 6-question quiz →](https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fquiz\u002Ftake.html?post=2026-07-25-css-starting-style-entry-animations)**\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## The takeaway\n\nThe double-rAF trick and the zero-millisecond `setTimeout` exist because CSS transitions need a \"before\" state, and there was no native way to provide one for elements entering the DOM. `@starting-style` is that native way. If your code applies a class one tick after mount, or toggles a data attribute to kick off an enter animation, look at whether `@starting-style` can replace it. The browser handles the timing correctly by definition; your JavaScript approximation might not.\n\n---\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)\n- 💼 **LinkedIn** — [linkedin.com\u002Fin\u002Fparsa-jiravand](https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F)\n- ✉️ **Email** (work & contract inquiries): [bestpractice2026@gmail.com](mailto:bestpractice2026@gmail.com)",{"title":207,"canonical":208,"description":209},"You use a setTimeout to trigger CSS entry animations. `@starting-style","https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-starting-style-entry-animations","Before `@starting-style`, the moment an element entered the DOM was invisible to CSS — transitions had no 'before' state to interpolate from. The at-rule gives you one, natively, w","019fe660-7d17-745b-812d-b78630e94999",{"id":212,"locked":18},"019fe660-83fc-741c-90a8-b674c86aabbc",[214],{"id":33,"slug":34,"title":36,"_count":215},{"questions":39},[217],{"locale":13,"slug":34},{"id":33,"slug":34,"title":36,"_count":219,"questionCount":39},{"questions":39}]