Close Menu
    DevStackTipsDevStackTips
    • Home
    • News & Updates
      1. Tech & Work
      2. View All

      This week in AI dev tools: Gemini 2.5 Pro and Flash GA, GitHub Copilot Spaces, and more (June 20, 2025)

      June 20, 2025

      Gemini 2.5 Pro and Flash are generally available and Gemini 2.5 Flash-Lite preview is announced

      June 19, 2025

      CSS Cascade Layers Vs. BEM Vs. Utility Classes: Specificity Control

      June 19, 2025

      IBM launches new integration to help unify AI security and governance

      June 18, 2025

      I used Lenovo’s latest dual-screen OLED laptop for a month and it wouldn’t be my first choice — here’s why

      June 22, 2025

      Here’s how I fixed a dead Steam Deck screen — with Valve proving they still have the best customer service in gaming

      June 22, 2025

      Borderlands 4 drops stunning new story trailer

      June 22, 2025

      DistroWatch Weekly, Issue 1127

      June 22, 2025
    • Development
      1. Algorithms & Data Structures
      2. Artificial Intelligence
      3. Back-End Development
      4. Databases
      5. Front-End Development
      6. Libraries & Frameworks
      7. Machine Learning
      8. Security
      9. Software Engineering
      10. Tools & IDEs
      11. Web Design
      12. Web Development
      13. Web Security
      14. Programming Languages
        • PHP
        • JavaScript
      Featured

      Exploring Lakebase: Databricks’ Next-Gen AI-Native OLTP Database

      June 22, 2025
      Recent

      Exploring Lakebase: Databricks’ Next-Gen AI-Native OLTP Database

      June 22, 2025

      Understanding JavaScript Promise

      June 22, 2025

      Lakeflow: Revolutionizing SCD2 Pipelines with Change Data Capture (CDC)

      June 21, 2025
    • Operating Systems
      1. Windows
      2. Linux
      3. macOS
      Featured

      I used Lenovo’s latest dual-screen OLED laptop for a month and it wouldn’t be my first choice — here’s why

      June 22, 2025
      Recent

      I used Lenovo’s latest dual-screen OLED laptop for a month and it wouldn’t be my first choice — here’s why

      June 22, 2025

      Here’s how I fixed a dead Steam Deck screen — with Valve proving they still have the best customer service in gaming

      June 22, 2025

      Borderlands 4 drops stunning new story trailer

      June 22, 2025
    • Learning Resources
      • Books
      • Cheatsheets
      • Tutorials & Guides
    Home»News & Updates»Orbital Mechanics (or How I Optimized a CSS Keyframes Animation)

    Orbital Mechanics (or How I Optimized a CSS Keyframes Animation)

    May 8, 2025

    I recently updated my portfolio at johnrhea.com. (If you’re looking to add a CSS or front-end engineer with storytelling and animation skills to your team, I’m your guy.) I liked the look of a series of planets I’d created for another personal project and decided to reuse them on my new site. Part of that was also reusing an animation I’d built circa 2019, where a moon orbited around the planet.

    Initially, I just plopped the animations into the new site, only changing the units (em units to viewport units using some complicated math that I was very, very proud of) so that they would scale properly because I’m… efficient with my time. However, on mobile, the planet would move up a few pixels and down a few pixels as the moons orbited around it. I suspected the plopped-in animation was the culprit (it wasn’t, but at least I got some optimized animation and an article out of the deal).

    Here’s the original animation:

    CodePen Embed Fallback

    My initial animation for the moon ran for 60 seconds. I’m folding it inside a disclosure widget because, at 141 lines, it’s stupid long (and, as we’ll see, emphasis on the stupid). Here it is in all its “glory”:

    Open code
    #moon1 {
      animation: moon-one 60s infinite;
    }
    
    @keyframes moon-one {
      0% {
        transform: translate(0, 0) scale(1);
        z-index: 2;
        animation-timing-function: ease-in;
      }
      5% {
        transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5);
        z-index: 2;
        animation-timing-function: ease-out;
      }
      9.9% {
        z-index: 2;
      }
      10% {
        transform: translate(-5.01043478vw, 6.511304348vw) scale(1);
        z-index: -1;
        animation-timing-function: ease-in;
      }
      15% {
        transform: translate(1.003478261vw, 2.50608696vw) scale(0.25);
        z-index: -1;
        animation-timing-function: ease-out;
      }
      19.9% {
        z-index: -1;
      }
      20% {
        transform: translate(0, 0) scale(1);
        z-index: 2;
        animation-timing-function: ease-in;
      }
      25% {
        transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5);
        z-index: 2;
        animation-timing-function: ease-out;
      }
      29.9% {
        z-index: 2;
      }
      30% {
        transform: translate(-5.01043478vw, 6.511304348vw) scale(1);
        z-index: -1;
        animation-timing-function: ease-in;
      }
      35% {
        transform: translate(1.003478261vw, 2.50608696vw) scale(0.25);
        z-index: -1;
        animation-timing-function: ease-out;
      }
      39.9% {
        z-index: -1;
      }
      40% {
        transform: translate(0, 0) scale(1);
        z-index: 2;
        animation-timing-function: ease-in;
      }
      45% {
        transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5);
        z-index: 2;
        animation-timing-function: ease-out;
      }
      49.9% {
        z-index: 2;
      }
      50% {
        transform: translate(-5.01043478vw, 6.511304348vw) scale(1);
        z-index: -1;
        animation-timing-function: ease-in;
      }
      55% {
        transform: translate(1.003478261vw, 2.50608696vw) scale(0.25);
        z-index: -1;
        animation-timing-function: ease-out;
      }
      59.9% {
        z-index: -1;
      }
      60% {
        transform: translate(0, 0) scale(1);
        z-index: 2;
        animation-timing-function: ease-in;
      }
      65% {
        transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5);
        z-index: 2;
        animation-timing-function: ease-out;
      }
      69.9% {
        z-index: 2;
      }
      70% {
        transform: translate(-5.01043478vw, 6.511304348vw) scale(1);
        z-index: -1;
        animation-timing-function: ease-in;
      }
      75% {
        transform: translate(1.003478261vw, 2.50608696vw) scale(0.25);
        z-index: -1;
        animation-timing-function: ease-out;
      }
      79.9% {
        z-index: -1;
      }
      80% {
        transform: translate(0, 0) scale(1);
        z-index: 2;
        animation-timing-function: ease-in;
      }
      85% {
        transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5);
        z-index: 2;
        animation-timing-function: ease-out;
      }
      89.9% {
        z-index: 2;
      }
      90% {
        transform: translate(-5.01043478vw, 6.511304348vw) scale(1);
        z-index: -1;
        animation-timing-function: ease-in;
      }
      95% {
        transform: translate(1.003478261vw, 2.50608696vw) scale(0.25);
        z-index: -1;
        animation-timing-function: ease-out;
      }
      99.9% {
        z-index: -1;
      }
      100% {
        transform: translate(0, 0) scale(1);
        z-index: 2;
        animation-timing-function: ease-in;
      }
    }

    If you look at the keyframes in that code, you’ll notice that the 0% to 20% keyframes are exactly the same as 20% to 40% and so on up through 100%. Why I decided to repeat the keyframes five times infinitely instead of just repeating one set infinitely is a decision lost to antiquity, like six years ago in web time. We can also drop the duration to 12 seconds (one-fifth of sixty) if we were doing our due diligence.

    I could thus delete everything from 20% on, instantly dropping the code down to 36 lines. And yes, I realize gains like this are unlikely to be possible on most sites, but this is the first step for optimizing things.

    #moon1 {
      animation: moon-one 12s infinite;
    }
    
    @keyframes moon-one {
      0% {
        transform: translate(0, 0) scale(1);
        z-index: 2;
        animation-timing-function: ease-in;
      }
      5% {
        transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5);
        z-index: 2;
        animation-timing-function: ease-out;
      }
      9.9% {
        z-index: 2;
      }
      10% {
        transform: translate(-5.01043478vw, 6.511304348vw) scale(1);
        z-index: -1;
        animation-timing-function: ease-in;
      }
      15% {
        transform: translate(1.003478261vw, 2.50608696vw) scale(0.25);
        z-index: -1;
        animation-timing-function: ease-out;
      }
      19.9% {
        z-index: -1;
      }
      20% {
        transform: translate(0, 0) scale(1);
        z-index: 2;
        animation-timing-function: ease-in;
      }
    }

    Now that we’ve gotten rid of 80% of the overwhelming bits, we can see that there are five main keyframes and two additional ones that set the z-index close to the middle and end of the animation (these prevent the moon from dropping behind the planet or popping out from behind the planet too early). We can change these five points from 0%, 5%, 10%, 15%, and 20% to 0%, 25%, 50%, 75%, and 100% (and since the 0% and the former 20% are the same, we can remove that one, too). Also, since the 10% keyframe above is switching to 50%, the 9.9% keyframe can move to 49.9%, and the 19.9% keyframe can switch to 99.9%, giving us this:

    #moon1 {
      animation: moon-one 12s infinite;
    }
    
    @keyframes moon-one {
      0%, 100% {
        transform: translate(0, 0) scale(1);
        z-index: 2;
        animation-timing-function: ease-in;
      }
      25% {
        transform: translate(-3.51217391vw, 3.50608696vw) scale(1.5);
        z-index: 2;
        animation-timing-function: ease-out;
      }
      49.9% {
        z-index: 2;
      }
      50% {
        transform: translate(-5.01043478vw, 6.511304348vw) scale(1);
        z-index: -1;
        animation-timing-function: ease-in;
      }
      75% {
        transform: translate(1.003478261vw, 2.50608696vw) scale(0.25);
        z-index: -1;
        animation-timing-function: ease-out;
      }
      99.9% {
        z-index: -1;
      }
    }

    Though I was very proud of myself for my math wrangling, numbers like -3.51217391vw are really, really unnecessary. If a screen was one thousand pixels wide, -3.51217391vw would be 35.1217391 pixels. No one ever needs to go down to the precision of a ten-millionth of a pixel. So, let’s round everything to the tenth place (and if it’s a 0, we’ll just drop it). We can also skip z-index in the 75% and 25% keyframes since it doesn’t change.

    Here’s where that gets us in the code:

    #moon1 {
      animation: moon-one 12s infinite;
    }
    
    @keyframes moon-one {
      0%, 100% {
        transform: translate(0, 0) scale(1);
        z-index: 2;
        animation-timing-function: ease-in;
      }
      25% {
        transform: translate(-3.5vw, 3.5vw) scale(1.5);
        z-index: 2;
        animation-timing-function: ease-out;
      }
      49.9% {
        z-index: 2;
      }
      50% {
        transform: translate(-5vw, 6.5vw) scale(1);
        z-index: -1;
        animation-timing-function: ease-in;
      }
      75% {
        transform: translate(1vw, 2.5vw) scale(0.25);
        z-index: -1;
        animation-timing-function: ease-out;
      }
      99.9% {
        z-index: -1;
      }
    }

    After all our changes, the animation still looks pretty close to what it was before, only way less code:

    CodePen Embed Fallback

    One of the things I don’t like about this animation is that the moon kind of turns at its zenith when it crosses the planet. It would be much better if it traveled in a straight line from the upper right to the lower left. However, we also need it to get a little larger, as if the moon is coming closer to us in its orbit. Because both translation and scaling were done in the transform property, I can’t translate and scale the moon independently.

    If we skip either one in the transform property, it resets the one we skipped, so I’m forced to guess where the mid-point should be so that I can set the scale I need. One way I’ve solved this in the past is to add a wrapping element, then apply scale to one element and translate to the other. However, now that we have individual scale and translate properties, a better way is to separate them from the transform property and use them as separate properties. Separating out the translation and scaling shouldn’t change anything, unless the original order they were declared on the transform property was different than the order of the singular properties.

    #moon1 {
      animation: moon-one 12s infinite;
    }
    
    @keyframes moon-one {
      0%, 100% {
        translate: 0 0;
        scale: 1;
        z-index: 2;
        animation-timing-function: ease-in;
      }
      25% {
        translate: -3.5vw 3.5vw;
        z-index: 2;
        animation-timing-function: ease-out;
      }
      49.9% {
        z-index: 2;
      }
      50% {
        translate: -5vw 6.5vw;
        scale: 1;
        z-index: -1;
        animation-timing-function: ease-in;
      }
      75% {
        translate: 1vw 2.5vw;
        scale: 0.25;
        animation-timing-function: ease-out;
      }
      99.9% {
        z-index: -1;
      }
    }

    Now that we can separate the scale and translate properties and use them independently, we can drop the translate property in the 25% and 75% keyframes because we don’t want them placed precisely in that keyframe. We want the browser’s interpolation to take care of that for us so that it translates smoothly while scaling.

    #moon1 {
      animation: moon-one 12s infinite;
    }
    
    @keyframes moon-one {
      0%, 100% {
        translate: 0 0;
        scale: 1;
        z-index: 2;
        animation-timing-function: ease-in;
      }
      25% {
        scale: 1.5;
        animation-timing-function: ease-out;
      }
      49.9% {
        z-index: 2;
      }
      50% {
        translate: -5vw 6.5vw;
        scale: 1;
        z-index: -1;
        animation-timing-function: ease-in;
      }
      75% {
        scale: 0.25;
        animation-timing-function: ease-out;
      }
      99.9% {
        z-index: -1;
      }
    }
    CodePen Embed Fallback

    Lastly, those different timing functions don’t make a lot of sense anymore because we’ve got the browser working for us, and if we use an ease-in-out timing function on everything, then it should do exactly what we want.

    #moon1 {
      animation: moon-one 12s infinite ease-in-out;
    }
    
    @keyframes moon-one {
      0%, 100% {
        translate: 0 0;
        scale: 1;
        z-index: 2;
      }
      25% {
        scale: 1.5;
      }
      49.9% {
        z-index: 2;
      }
      50% {
        translate: -5vw 6.5vw;
        scale: 1;
        z-index: -1;
      }
      75% {
        scale: 0.25;
      }
      99.9% {
        z-index: -1;
      }
    }
    CodePen Embed Fallback

    And there you go: 141 lines down to 28, and I think the animation looks even better than before. It will certainly be easier to maintain, that’s for sure.

    But what do you think? Was there an optimization step I missed? Let me know in the comments.


    Orbital Mechanics (or How I Optimized a CSS Keyframes Animation) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

    Source: Read More 

    Facebook Twitter Reddit Email Copy Link
    Previous ArticleKadas Albireo is a mapping application based on QGIS
    Next Article Turn Siloed Metrics into Business-Driven Insights with Tx-Insights

    Related Posts

    News & Updates

    I used Lenovo’s latest dual-screen OLED laptop for a month and it wouldn’t be my first choice — here’s why

    June 22, 2025
    News & Updates

    Here’s how I fixed a dead Steam Deck screen — with Valve proving they still have the best customer service in gaming

    June 22, 2025
    Leave A Reply Cancel Reply

    For security, use of Google's reCAPTCHA service is required which is subject to the Google Privacy Policy and Terms of Use.

    Continue Reading

    Cyber Monday – 12 tips to help you shop safely online

    Development

    CVE-2025-3200: Wiesemann & Theis Com-Server Devices Exposed by Deprecated TLS Protocols

    Security

    Code a Dropbox Clone with NextJS

    Development

    Findomain — All Information of Domain

    Learning Resources

    Highlights

    CVE-2025-30326 – Adobe Photoshop Uninitialized Pointer Access Vulnerability

    May 13, 2025

    CVE ID : CVE-2025-30326

    Published : May 13, 2025, 6:15 p.m. | 49 minutes ago

    Description : Photoshop Desktop versions 26.5, 25.12.2 and earlier are affected by an Access of Uninitialized Pointer vulnerability that could result in arbitrary code execution in the context of the current user. Exploitation of this issue requires user interaction in that a victim must open a malicious file.

    Severity: 7.8 | HIGH

    Visit the link for more details, such as CVSS details, affected products, timeline, and more…

    First signs of dedicated Xbox interface for Windows 11 PCs spotted in latest preview

    April 1, 2025

    CVE-2025-5209 – Ivory Search WordPress XSS Vulnerability

    June 17, 2025

    Does Elden Ring Nightreign have crossplay or cross-platform play?

    May 30, 2025
    © DevStackTips 2025. All rights reserved.
    • Contact
    • Privacy Policy

    Type above and press Enter to search. Press Esc to cancel.