{"slug": "partial-keyframes", "title": "Partial Keyframes", "summary": "A CSS keyframe animation can still function even if the `from` or `to` block is omitted. When the starting point is missing, the animation inherits the element's current property value (like opacity) rather than snapping to a predefined starting point. This technique makes keyframe animations dynamic and composable, allowing them to adapt to an element's existing styles or state.", "body_md": "The most common way to write a CSS keyframe animation is to specify a starting point and an ending point, using from\nand to\n:\n@keyframes fadeOut {\nfrom {\nopacity: 1;\n}\nto {\nopacity: 0;\n}\n}\nWhat do you suppose happens if we omit one of these blocks? For example, what if we only specify the ending point?\n@keyframes fadeToTransparent {\nto {\nopacity: 0;\n}\n}\nThis still works! Even without defining a starting opacity, our element will still fade out when this keyframe animation is applied.\nWhen I discovered this pattern a few years ago, I thought it was a neat little trick to shave a few bytes off my bundles, a more concise way to write keyframe animations. But it turns out, there’s a way more significant and exciting benefit to this pattern! This lil’ trick unlocks a hidden capability within keyframe animations that makes them dynamic and composable. ✨\nIn this tutorial, I’ll show you how this trick works, and we’ll explore some of the cool things we can do with it. There’s also a bonus tip at the end, showcasing how modern CSS makes keyframe animations even more powerful! 😄\nLink to this headingInherited values\nWhen we omit the from\nblock from our keyframe animation, the animation’s starting values will be inherited from context.\nThis’ll be easier to explain with a demo. Check this out:\n@keyframes fadeOut {\nfrom {\nopacity: 1;\n}\nto {\nopacity: 0;\n}\n}\nOur “traditional” setup really only works for elements that are fully opaque by default, like that first yellow ball. The others immediately snap to full opacity before gradually fading out:\nBy contrast, when we omit the from\npart of the keyframe, the animation will inherit the element’s current opacity and start fading from there. 🤯\nWe can visualize this difference with a graph:\nHere’s a playground with the full code, in case you wanted to poke at this a bit:\nCode Playground\n<style> @keyframes fadeToTransparent { to { opacity: 0; } } .to-transparent { animation: fadeToTransparent 1000ms forwards; } </style> <div class=\"row\"> <div class=\"ball\"></div> <div class=\"ball\" style=\"opacity: 0.6\"></div> <div class=\"ball\" style=\"opacity: 0.3\"></div> </div> <button> Toggle keyframe </button>\nIn this playground, the resting opacity is set via inline style, <div style=\"opacity: 0.6\">\n, but that isn’t a requirement for this pattern. It’ll still work if the resting opacity is set with a CSS class, or however else you typically apply CSS!\nAnd if an element doesn’t set the opacity\nproperty at all, like that first .ball\nin the playground, the default value of 1\nwill be used. This is awesome, since it means that our fadeToTransparent\nkeyframe is just as easy-to-use as a traditional fadeOut\nanimation. We don’t have to explicitly set opacity: 1\nin order to fade stuff out with this technique!\nLink to this headingOmitting the destination\nThe same trick works in the other direction. If you omit the to\nvalue, the animation will animate from a specified value to whatever value it’s currently set to:\n<style>\n@keyframes fadeFromTransparent {\nfrom {\nopacity: 0;\n}\n}\n.ball {\nanimation: fadeFromTransparent 1000ms;\n}\n</style>\nIn this demo, we animate from 0\nto whatever the element’s specified opacity is. If the element doesn’t have an explicit opacity set, it will default to 1\n, acting like a regular fade-in animation.\nWhat’s the use case for this? This trick is handy when working with elements that aren’t fully opaque by default, or have state-based opacity. Here’s a real-world example:\n<style>\n@keyframes fadeFromTransparent {\nfrom {\nopacity: 0;\n}\n}\n.icon-btn {\nopacity: 0.7;\nanimation: fadeFromTransparent 1000ms;\n&:hover, &:active, &:focus-visible {\nopacity: 1;\n}\n}\n</style>\nThis button has a resting opacity of 0.7\n, but when the user hovers over it, the opacity flips to 1\n. This is a useful UX pattern because it helps convey that this element is interactive.This is known as “affordance” in UX design circles. This button already has pretty good affordance, since the cursor flips to a pointer on hover, but I find the experience is even better with an additional indication.\n(I’ve also set it up here to rise to full opacity on focus and tap, so that you can experience this even if you’re not using a mouse, but in practice I think it’s fine for this particular detail to be mouse-only.)\nThis button also fades in on mount, and so I’m using the partial keyframes trick to ensure that it always fades to the correct value. If the user happens to be hovering over it when it mounts, it’ll fade to 1\n. Otherwise, it’ll fade to 0.7\n. ✨\nLink to this headingAnimating to a dynamic value\nThis next bit really blew my mind when I discovered it. With partial keyframes, we can animate to a value specified by another keyframe animation!\nCheck this out:\n<style>\n@keyframes twinkle {\nfrom {\nopacity: 0.25;\n}\nto {\nopacity: 0.75;\n}\n}\n@keyframes fadeFromTransparent {\nfrom {\nopacity: 0;\n}\n}\n.ball {\nanimation:\ntwinkle 250ms alternate infinite,\nfadeFromTransparent 2000ms;\n}\n</style>\nLet me explain what’s going on here:\n- The\ntwinkle\nkeyframe animation causes the ball to oscillate between0.25\nand0.75\nopacity. It bounces back and forth thanks to thealternate\nkeyword, and runs forever thanks toinfinite\n. - Our\nfadeFromTransparent\nkeyframe sets an initial opacity of0\n, but doesn’t specify the target opacity. - When combined, we fade from\n0\nto the ever-changing value set withintwinkle\n. It essentially allows us to gradually introduce the flickeringtwinkle\nanimation.\nLet’s graph the opacity changes over time, so that we can really see what’s going on here. Toggle between the two values to see the effect of stacking these keyframe animations:\nPretty wild, right?? Multiple keyframe animations can modify the same property without one cancelling the other!\nLink to this headingWhimsical Animations ✨\nIf you found this tutorial useful, I have some good news for you!\nOver the past 18 months, I’ve been working on a new animations course. This lil’ trick was plucked straight from the course, and it’s just the tip of the iceberg. I’ll show you all of the techniques and strategies I use to design and build polished, next-level animations using CSS, JavaScript, SVG, and Canvas.\nIf you’ve ever wondered how I created some animation on this blog, there’s a very good chance we cover it in the course 😄. Check it out:\nLink to this headingBonus: dynamic values in keyframe definitions\nThere’s one more trick I want to share with you. 😄\nLet’s suppose we’re building the following tail-wagging animation:\nFor this sort of endless motion, CSS Keyframe animations are the best tool in the toolbox. But hm, keyframe animations require that we set specific, hardcoded values.\nSo, if we only had one ball, this wouldn’t be a big deal. We could set it up like this:\n@keyframes oscillate {\nfrom {\ntransform: translateX(-16px);\n}\nto {\ntransform: translateX(16px);\n}\n}\n.ball {\nanimation: oscillate 1000ms infinite alternate;\n}\nBut, we have four balls, and they each oscillate by a different amount. 🤔\nFor years, this was a thorn in my side. I had to either create four nearly-identical keyframe animations, each with a different hardcoded value, or I would wire it up using CSS transitions and JavaScript intervals. Both options were thoroughly unsatisfying.\nAnd then, I had a mindblowing realization.\nCheck this out:\n@keyframes oscillate {\nfrom {\ntransform: translateX(calc(var(--amount) * -1));\n}\nto {\ntransform: translateX(var(--amount));\n}\n}\nInstead of hardcoding a specific value like 16px\ninside our keyframe definition, we can access a CSS variable! With a little help from calc\n, we can flip that value to its negative counterpart, so that we can oscillate to/from a dynamic value.\nIn order for this to work, we need to define an --amount\nvalue on each element that is being animated. For example, we could do that with an inline style:\n<style>\n.ball {\nanimation: oscillate 1000ms infinite alternate;\n}\n</style>\n<div class=\"ball\" style=\"--amount: 8px\"></div>\n<div class=\"ball\" style=\"--amount: 16px\"></div>\n<div class=\"ball\" style=\"--amount: 32px\"></div>\n<div class=\"ball\" style=\"--amount: 64px\"></div>\nHow freaking cool is this?!\nWhen I first discovered this trick, it kinda blew my mind. I had no idea we could read CSS variables from within a keyframe animation! This was the final puzzle piece that fully unlocked keyframe animations for me, making them just as dynamic and flexible as CSS transitions.\nHere’s a full editable demo that showcases this technique:\nCode Playground\n<style> @keyframes oscillate { from { transform: translateX(calc(var(--amount) * -1)); } to { transform: translateX(var(--amount)); } } .ball { animation: oscillate 700ms ease-in-out alternate infinite; } </style> <!-- Edit these values to change the oscillation amount: --> <div class=\"ball\" style=\"--amount: 8px\"></div> <div class=\"ball\" style=\"--amount: 16px\"></div> <div class=\"ball\" style=\"--amount: 32px\"></div> <div class=\"ball\" style=\"--amount: 64px\"></div> <button>Play/pause animation</button>\nIn terms of browser support, I believe that this has been supported since CSS variables themselves were introduced. According to caniuse, support is sitting around 96%(opens in new tab).\nCSS keyframe animations can be pretty confusing, so if you’re not sure what keywords like alternate\nare doing, you can check out my interactive blog post on the subject.\nAnd if you’d like to learn about whimsical animation more broadly, don’t forget to sign up for updates about my course!(opens in new tab)\nLast updated on\nMay 5th, 2026", "url": "https://wpnews.pro/news/partial-keyframes", "canonical_source": "https://www.joshwcomeau.com/animation/partial-keyframes/", "published_at": "2025-06-10 11:00:00+00:00", "updated_at": "2026-05-22 14:54:13.239968+00:00", "lang": "en", "topics": [], "entities": [], "alternates": {"html": "https://wpnews.pro/news/partial-keyframes", "markdown": "https://wpnews.pro/news/partial-keyframes.md", "text": "https://wpnews.pro/news/partial-keyframes.txt", "jsonld": "https://wpnews.pro/news/partial-keyframes.jsonld"}}