[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-web-animations-api-imperative-control":44,"search-suggestions":60,"quiz-article-web-animations-api-imperative-control":107},[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,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a01df1-d737-71d5-ac0f-2c3ca934d3bf","web-animations-api-imperative-control","PRACTICE_QUIZ","Web Animations API — imperative control","Eight questions on element.animate() and the Animation object it returns: pausing, reversing, seeking, fill modes, and the difference between cancel() and finish().",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},8,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,101,104],{"slug":62,"name":63,"articles":64},"webdev","Webdev",67,{"slug":66,"name":67,"articles":68},"frontend","Frontend",58,{"slug":70,"name":71,"articles":72},"javascript","Javascript",57,{"slug":74,"name":75,"articles":76},"css","Css",25,{"slug":78,"name":79,"articles":80},"typescript","Typescript",10,{"slug":82,"name":83,"articles":84},"performance","Performance",9,{"slug":86,"name":87,"articles":88},"tutorial","Tutorial",7,{"slug":90,"name":91,"articles":92},"grammar","Grammar",6,{"slug":94,"name":95,"articles":96},"react","React",5,{"slug":98,"name":99,"articles":100},"html","Html",4,{"slug":102,"name":103,"articles":100},"node","Node",{"slug":105,"name":106,"articles":100},"programming","Programming",{"id":108,"slug":46,"title":109,"subtitle":52,"excerpt":110,"coverUrl":111,"locale":13,"readingMinutes":96,"publishedAt":112,"viewCount":113,"likeCount":19,"commentCount":19,"author":114,"vertical":119,"topic":120,"tags":122,"_count":127,"playground":129,"body":131,"bodyMd":268,"seo":269,"translationGroupId":272,"series":52,"verticalId":5,"thread":273,"assessments":275,"translations":278,"quiz":280},"01a01df1-d6d5-71e8-bba6-1242e581730b","CSS Animations Can't Pause. The Web Animations API Can.","Toggling a class to restart a CSS animation works — until you need to pause it, reverse it mid-flight, or ask how far through it is. element.animate() returns an object with those methods built in, natively, in every browser you already support.","\u002Fmedia\u002Fcovers\u002Fweb-animations-api-imperative-control.png","2026-08-25T06:55:40.108Z",62,{"id":115,"name":116,"username":117,"avatarUrl":52,"headline":118},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":70,"name":121},"JavaScript",[123,124,125,126],{"slug":70,"name":71,"color":52},{"slug":74,"name":75,"color":52},{"slug":62,"name":63,"color":52},{"slug":66,"name":67,"color":52},{"assessments":128},1,{"slug":46,"title":130},"Web Animations API — the pause button CSS never had",{"blocks":132,"version":128},[133,137,143,146,151,154,157,161,164,167,170,173,177,180,184,187,190,193,196,199,202,205,208,212,215,219,222,225,228,231,234,237,240,243,246,249,252,255,258,261],{"id":134,"html":135,"type":136},"b1","\u003Cp>You built a shake animation for invalid form fields. Nice touch — the field jitters, the user notices, everyone&#39;s happy. Then someone on your team clicks &quot;submit&quot; three times fast, and the shake either doesn&#39;t restart, or restarts in a visibly broken half-state. You go find the fix online. It&#39;s this:\u003C\u002Fp>","paragraph",{"id":138,"code":139,"type":140,"language":141,"highlight":142},"b2","function shake(field) {\n  field.classList.remove('shake');\n  void field.offsetWidth; \u002F\u002F force a reflow so the browser \"forgets\" the animation ran\n  field.classList.add('shake');\n}","code","js",[],{"id":144,"html":145,"type":136},"b3","\u003Cp>That \u003Ccode>void field.offsetWidth\u003C\u002Fcode> line is a real, documented, widely-used trick. It works. It&#39;s also the moment you should notice something: \u003Cstrong>you&#39;re forcing a synchronous layout recalculation just to convince the browser your animation is allowed to run again.\u003C\u002Fstrong> That&#39;s not a small workaround. That&#39;s the entire toolkit CSS gives you for controlling an animation from JavaScript — restart it, or don&#39;t touch it.\u003C\u002Fp>",{"id":147,"html":148,"text":149,"type":150,"level":31},"b4","What the classList trick can&#39;t do","What the classList trick can't do","heading",{"id":152,"html":153,"type":136},"b5","\u003Cp>The reflow hack gets you &quot;restart from the top.&quot; Try to go one step further and CSS stops helping.\u003C\u002Fp>",{"id":155,"html":156,"type":136},"b6","\u003Cp>Say product wants the shake to pause if the user is still typing, resume if they stop, and — this is the one that breaks people — reverse smoothly if they fix the field before the shake finishes. With a CSS class, here&#39;s what you actually have access to: \u003Ccode>animation-play-state: paused\u003C\u002Fcode>, and that&#39;s it. No reverse. No &quot;jump to 40% through.&quot; No way to ask &quot;how many milliseconds into this animation are we right now&quot; without reaching for \u003Ccode>getComputedStyle()\u003C\u002Fcode> and parsing a matrix transform out of a string.\u003C\u002Fp>",{"id":158,"code":159,"type":140,"language":74,"highlight":160},"b7",".field.shake {\n  animation: shake 400ms ease;\n}\n.field.shake.held {\n  animation-play-state: paused;\n}",[],{"id":162,"html":163,"type":136},"b8","\u003Cp>Pausing works. Now try to resume from wherever it paused, then reverse from \u003Cem>there\u003C\u002Fem> — not from the start, from mid-shake — and you&#39;re out of CSS. The animation is a black box with a play\u002Fpause switch soldered to the outside. You never get a handle to the thing itself.\u003C\u002Fp>",{"id":165,"html":166,"type":136},"b9","\u003Cp>Before you scroll: think about how you&#39;d even represent &quot;which part of the animation are we in&quot; if you had to write it by hand. A timer? A percentage you track separately and hope stays in sync? That&#39;s the shape of the problem.\u003C\u002Fp>",{"id":168,"html":169,"text":169,"type":150,"level":31},"b10","The object CSS never gave you",{"id":171,"html":172,"type":136},"b11","\u003Cp>Here&#39;s the same shake, written with the DOM method that&#39;s been sitting in every major browser for years:\u003C\u002Fp>",{"id":174,"code":175,"type":140,"language":141,"highlight":176},"b12","const anim = field.animate(\n  [\n    { transform: 'translateX(0)' },\n    { transform: 'translateX(-8px)' },\n    { transform: 'translateX(8px)' },\n    { transform: 'translateX(0)' }\n  ],\n  { duration: 400, easing: 'ease' }\n);",[],{"id":178,"html":179,"type":136},"b13","\u003Cp>\u003Ccode>field.animate()\u003C\u002Fcode> doesn&#39;t just start an animation. It \u003Cstrong>returns one\u003C\u002Fstrong> — an \u003Ccode>Animation\u003C\u002Fcode> object, live, with a full set of methods and properties on it: \u003Ccode>.play()\u003C\u002Fcode>, \u003Ccode>.pause()\u003C\u002Fcode>, \u003Ccode>.reverse()\u003C\u002Fcode>, \u003Ccode>.finish()\u003C\u002Fcode>, \u003Ccode>.cancel()\u003C\u002Fcode>, \u003Ccode>.currentTime\u003C\u002Fcode>, \u003Ccode>.playbackRate\u003C\u002Fcode>, and a \u003Ccode>.finished\u003C\u002Fcode> property that&#39;s an actual \u003Ccode>Promise\u003C\u002Fcode>, resolving when the animation completes.\u003C\u002Fp>",{"id":181,"code":182,"type":140,"language":141,"highlight":183},"b14","anim.pause();                 \u002F\u002F freeze exactly where it is\nanim.currentTime = 150;       \u002F\u002F jump to 150ms in, no math required\nanim.reverse();               \u002F\u002F now play backward from right here\nanim.playbackRate = 0.25;     \u002F\u002F slow it to a crawl\nawait anim.finished;          \u002F\u002F do something once it's actually done",[],{"id":185,"html":186,"type":136},"b15","\u003Cp>No class removed. No forced reflow. No parsing a \u003Ccode>matrix()\u003C\u002Fcode> string to figure out where a transform currently sits. The thing you&#39;d have had to hand-build — a tracked &quot;how far through are we&quot; state — is just \u003Ccode>.currentTime\u003C\u002Fcode>, maintained by the browser, always correct, in milliseconds.\u003C\u002Fp>",{"id":188,"html":189,"type":136},"b16","\u003C!-- playground:start -->",{"id":191,"html":192,"text":192,"type":150,"level":31},"b17","🎮 Try it yourself",{"id":194,"html":195,"type":136},"b18","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweb-animations-api-imperative-control\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":197,"html":198,"type":136},"b19","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":200,"html":201,"type":136},"b20","\u003C!-- playground:end -->",{"id":203,"html":204,"text":204,"type":150,"level":31},"b21","The gotcha that bites people moving from CSS",{"id":206,"html":207,"type":136},"b22","\u003Cp>There&#39;s one behavior that trips up almost everyone the first time: when the animation finishes, the element snaps back to its pre-animation style. Not a bug — it&#39;s the default. CSS has the exact same gotcha (\u003Ccode>animation-fill-mode\u003C\u002Fcode> defaults to \u003Ccode>none\u003C\u002Fcode>), it&#39;s just less visible because most CSS animations end where they started.\u003C\u002Fp>",{"id":209,"code":210,"type":140,"language":141,"highlight":211},"b23","const slideIn = card.animate(\n  [{ transform: 'translateY(20px)', opacity: 0 }, { transform: 'translateY(0)', opacity: 1 }],\n  { duration: 300, fill: 'forwards' } \u002F\u002F \u003C- without this, it plays, then instantly reverts\n);",[],{"id":213,"html":214,"type":136},"b24","\u003Cp>\u003Ccode>fill: &#39;forwards&#39;\u003C\u002Fcode> tells it to hold the last keyframe&#39;s values after playback ends, instead of letting the element&#39;s real styles show back through. Forget it, and your &quot;slide in and stay&quot; animation slides in and then visibly pops back to invisible the instant it finishes — a bug that only shows up on the \u003Cem>last\u003C\u002Fem> frame, which is exactly the frame nobody&#39;s watching for in a code review.\u003C\u002Fp>",{"id":216,"html":217,"text":218,"type":150,"level":31},"b25","Cancel and finish aren&#39;t the same undo","Cancel and finish aren't the same undo",{"id":220,"html":221,"type":136},"b26","\u003Cp>Two methods look like they do the same thing and don&#39;t. \u003Ccode>anim.cancel()\u003C\u002Fcode> rips the effect out entirely — \u003Ccode>currentTime\u003C\u002Fcode> resets to \u003Ccode>null\u003C\u002Fcode>, \u003Ccode>playState\u003C\u002Fcode> becomes \u003Ccode>&quot;idle&quot;\u003C\u002Fcode>, and the element&#39;s styles revert to whatever they&#39;d be with no animation running at all, \u003Ccode>fill\u003C\u002Fcode> mode or not. \u003Ccode>anim.finish()\u003C\u002Fcode> instead jumps straight to the end of the animation and \u003Cem>applies\u003C\u002Fem> whatever fill behavior you set, same as if it had played all the way through in real time.\u003C\u002Fp>",{"id":223,"html":224,"type":136},"b27","\u003Cp>Use \u003Ccode>cancel()\u003C\u002Fcode> when you want the animation to have never happened. Use \u003Ccode>finish()\u003C\u002Fcode> when you want to skip to the result. Mixing them up is how a &quot;skip animation&quot; button quietly turns into a &quot;delete the thing the animation was building toward&quot; button.\u003C\u002Fp>",{"id":226,"html":227,"text":227,"type":150,"level":31},"b28","What this buys you that a library used to",{"id":229,"html":230,"type":136},"b29","\u003Cp>None of this needs GSAP, none of it needs a state machine you built yourself. The interactive pieces — pause-on-hover, scrub-to-a-point, reverse-on-condition, speed controls — used to be exactly the reason teams reached for an animation library on top of CSS. \u003Ccode>element.animate()\u003C\u002Fcode> puts a real handle on the animation instead, and it&#39;s been shipping without a vendor prefix in Chrome, Firefox, and Safari for years. If your bundle includes an animation library today, it&#39;s worth checking whether it&#39;s earning its weight or just wrapping this.\u003C\u002Fp>",{"id":232,"html":233,"type":136},"b30","\u003Cp>The shake-on-invalid-field from the top of this post, done properly, is one call: keyframes, a duration, and — if you want it to hold its shape instead of just its motion — a \u003Ccode>fill\u003C\u002Fcode> mode. Everything else is a method call on the object it hands back, not a class name you cross your fingers over.\u003C\u002Fp>",{"id":235,"html":236,"type":136},"b31","\u003Cp>Go find the CSS animation in your codebase with the ugliest JavaScript wrapped around it — the one with a reflow hack, or a \u003Ccode>setTimeout\u003C\u002Fcode> guessing at when \u003Ccode>animationend\u003C\u002Fcode> should have fired. Try rewriting it as one \u003Ccode>animate()\u003C\u002Fcode> call. What did you get to delete?\u003C\u002Fp>",{"id":238,"html":239,"type":136},"b32","\u003C!-- quiz:start -->",{"id":241,"html":242,"text":242,"type":150,"level":31},"b33","🧠 Test yourself",{"id":244,"html":245,"type":136},"b34","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweb-animations-api-imperative-control\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":247,"html":248,"type":136},"b35","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":250,"html":251,"type":136},"b36","\u003C!-- quiz:end -->",{"id":253,"type":254},"b37","divider",{"id":256,"html":257,"type":136},"b38","\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":259,"html":260,"type":136},"b39","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":262,"type":263,"items":264,"ordered":18},"b40","list",[265,266,267],"⭐ \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 built a shake animation for invalid form fields. Nice touch — the field jitters, the user notices, everyone's happy. Then someone on your team clicks \"submit\" three times fast, and the shake either doesn't restart, or restarts in a visibly broken half-state. You go find the fix online. It's this:\n\n```js\nfunction shake(field) {\n  field.classList.remove('shake');\n  void field.offsetWidth; \u002F\u002F force a reflow so the browser \"forgets\" the animation ran\n  field.classList.add('shake');\n}\n```\n\nThat `void field.offsetWidth` line is a real, documented, widely-used trick. It works. It's also the moment you should notice something: **you're forcing a synchronous layout recalculation just to convince the browser your animation is allowed to run again.** That's not a small workaround. That's the entire toolkit CSS gives you for controlling an animation from JavaScript — restart it, or don't touch it.\n\n## What the classList trick can't do\n\nThe reflow hack gets you \"restart from the top.\" Try to go one step further and CSS stops helping.\n\nSay product wants the shake to pause if the user is still typing, resume if they stop, and — this is the one that breaks people — reverse smoothly if they fix the field before the shake finishes. With a CSS class, here's what you actually have access to: `animation-play-state: paused`, and that's it. No reverse. No \"jump to 40% through.\" No way to ask \"how many milliseconds into this animation are we right now\" without reaching for `getComputedStyle()` and parsing a matrix transform out of a string.\n\n```css\n.field.shake {\n  animation: shake 400ms ease;\n}\n.field.shake.held {\n  animation-play-state: paused;\n}\n```\n\nPausing works. Now try to resume from wherever it paused, then reverse from *there* — not from the start, from mid-shake — and you're out of CSS. The animation is a black box with a play\u002Fpause switch soldered to the outside. You never get a handle to the thing itself.\n\nBefore you scroll: think about how you'd even represent \"which part of the animation are we in\" if you had to write it by hand. A timer? A percentage you track separately and hope stays in sync? That's the shape of the problem.\n\n## The object CSS never gave you\n\nHere's the same shake, written with the DOM method that's been sitting in every major browser for years:\n\n```js\nconst anim = field.animate(\n  [\n    { transform: 'translateX(0)' },\n    { transform: 'translateX(-8px)' },\n    { transform: 'translateX(8px)' },\n    { transform: 'translateX(0)' }\n  ],\n  { duration: 400, easing: 'ease' }\n);\n```\n\n`field.animate()` doesn't just start an animation. It **returns one** — an `Animation` object, live, with a full set of methods and properties on it: `.play()`, `.pause()`, `.reverse()`, `.finish()`, `.cancel()`, `.currentTime`, `.playbackRate`, and a `.finished` property that's an actual `Promise`, resolving when the animation completes.\n\n```js\nanim.pause();                 \u002F\u002F freeze exactly where it is\nanim.currentTime = 150;       \u002F\u002F jump to 150ms in, no math required\nanim.reverse();               \u002F\u002F now play backward from right here\nanim.playbackRate = 0.25;     \u002F\u002F slow it to a crawl\nawait anim.finished;          \u002F\u002F do something once it's actually done\n```\n\nNo class removed. No forced reflow. No parsing a `matrix()` string to figure out where a transform currently sits. The thing you'd have had to hand-build — a tracked \"how far through are we\" state — is just `.currentTime`, maintained by the browser, always correct, in milliseconds.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweb-animations-api-imperative-control\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## The gotcha that bites people moving from CSS\n\nThere's one behavior that trips up almost everyone the first time: when the animation finishes, the element snaps back to its pre-animation style. Not a bug — it's the default. CSS has the exact same gotcha (`animation-fill-mode` defaults to `none`), it's just less visible because most CSS animations end where they started.\n\n```js\nconst slideIn = card.animate(\n  [{ transform: 'translateY(20px)', opacity: 0 }, { transform: 'translateY(0)', opacity: 1 }],\n  { duration: 300, fill: 'forwards' } \u002F\u002F \u003C- without this, it plays, then instantly reverts\n);\n```\n\n`fill: 'forwards'` tells it to hold the last keyframe's values after playback ends, instead of letting the element's real styles show back through. Forget it, and your \"slide in and stay\" animation slides in and then visibly pops back to invisible the instant it finishes — a bug that only shows up on the *last* frame, which is exactly the frame nobody's watching for in a code review.\n\n## Cancel and finish aren't the same undo\n\nTwo methods look like they do the same thing and don't. `anim.cancel()` rips the effect out entirely — `currentTime` resets to `null`, `playState` becomes `\"idle\"`, and the element's styles revert to whatever they'd be with no animation running at all, `fill` mode or not. `anim.finish()` instead jumps straight to the end of the animation and *applies* whatever fill behavior you set, same as if it had played all the way through in real time.\n\nUse `cancel()` when you want the animation to have never happened. Use `finish()` when you want to skip to the result. Mixing them up is how a \"skip animation\" button quietly turns into a \"delete the thing the animation was building toward\" button.\n\n## What this buys you that a library used to\n\nNone of this needs GSAP, none of it needs a state machine you built yourself. The interactive pieces — pause-on-hover, scrub-to-a-point, reverse-on-condition, speed controls — used to be exactly the reason teams reached for an animation library on top of CSS. `element.animate()` puts a real handle on the animation instead, and it's been shipping without a vendor prefix in Chrome, Firefox, and Safari for years. If your bundle includes an animation library today, it's worth checking whether it's earning its weight or just wrapping this.\n\nThe shake-on-invalid-field from the top of this post, done properly, is one call: keyframes, a duration, and — if you want it to hold its shape instead of just its motion — a `fill` mode. Everything else is a method call on the object it hands back, not a class name you cross your fingers over.\n\nGo find the CSS animation in your codebase with the ugliest JavaScript wrapped around it — the one with a reflow hack, or a `setTimeout` guessing at when `animationend` should have fired. Try rewriting it as one `animate()` call. What did you get to delete?\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweb-animations-api-imperative-control\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":109,"canonical":270,"description":271},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fweb-animations-api-imperative-control","Toggling a class to restart a CSS animation works — until you need to pause it, reverse it mid-flight, or ask how far through it is. element.animate() returns an object with those ","01a01df1-d6d5-71e8-bba6-1492f9b39f13",{"id":274,"locked":18},"01a01df1-d70c-7599-a5d6-bef3a7fb8bac",[276],{"id":45,"slug":46,"title":48,"_count":277},{"questions":51},[279],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":281,"questionCount":51},{"questions":51}]