AppyDev Blog
Alternatives to CSS Animations: Transitions, Web Animations API, and GSAP
Compare alternatives to CSS keyframes for UI motion. Try the same card entrance in CSS and JavaScript, then choose based on the controls you need.
If you're looking for an alternative to CSS keyframes, start with the behavior you need. Use a CSS transition for a simple change between two states. Use the Web Animations API if JavaScript needs to play, pause, reverse, or cancel the effect. Reach for a GSAP timeline when several elements need to move in a coordinated sequence. CSS keyframes are still a good fit for a self-contained effect with a few stages and predictable timing.
There are two different searches hiding in this question. You might want to replace the way your code schedules motion, or you might simply want a different collection of effects. Animate.css alternatives covers the second case with tools like Animista and AOS. The examples below help with the first case by showing how the same card entrance works with CSS and with a browser API.
Use a transition for a change between two states
A transition tells the browser how to get from one value to another after something changes. A button can change color when it receives focus. A panel can slide a little when a class is added. The component already knows which state it is in, so the browser can interpolate between the styles for those states. You can usually describe the whole effect in a short rule and keep the state change in whatever code already owns the component.
For example, imagine a card that moves up four pixels when its link gets keyboard focus. The base rule describes the resting position and says how long a transform should take. The focus rule sets the new position. When focus moves away, the same transition brings the card back. Give keyboard users a visible focus indicator as well. The movement can reinforce the state, while the outline keeps the state easy to recognize when motion is reduced.
Transitions get awkward when the sequence needs more than a start and an end. Suppose you want an icon to overshoot, settle, and briefly change opacity halfway through. Those are separate stages you can express directly with keyframes. You could wire up extra classes and callbacks to simulate the stages, but at that point keyframes may be the clearer description. See MDN’s guide to CSS transitions for how transitions start and which properties can animate.
Try the same card entrance with CSS and JavaScript
This example gives a card a 220 ms entrance from eight pixels below its resting position. Both versions animate opacity and transform, and both leave the card visible and in its resting position when the effect ends. Try the versions separately so the CSS and JavaScript effects don't run on the same card at the same time.
For the CSS version, use this markup and apply the class when the card is inserted. There is no replay button in this version because the example illustrates a one-time entrance.
<article class="card-enter">Your export is ready.</article>
@keyframes card-enter {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: no-preference) {
.card-enter {
animation: card-enter 220ms ease-out;
}
}
The card's base style stays visible. Only the animation temporarily applies the starting opacity. That matters when the visitor has requested reduced motion, when the animation is removed, or when you inspect the component after it finishes. The text and the state it represents are available without a successful animation.
For the JavaScript version, use this markup. Run the script after the elements exist, for example with a deferred script or a script placed after the markup. The button makes it easy to trigger the same entrance again.
<article id="export-card">Your export is ready.</article>
<button id="replay-card" type="button">Replay entrance</button>
const card = document.querySelector('#export-card')
const replay = document.querySelector('#replay-card')
const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)')
let entrance
function playEntrance() {
entrance?.cancel()
if (reducedMotion.matches) return
entrance = card.animate(
[
{ opacity: 0, transform: 'translateY(8px)' },
{ opacity: 1, transform: 'translateY(0)' },
],
{ duration: 220, easing: 'ease-out' }
)
}
replay.addEventListener('click', playEntrance)
reducedMotion.addEventListener('change', () => {
if (reducedMotion.matches) entrance?.cancel()
})
playEntrance()
The extra code gives you an Animation object. You can keep it in the component that owns the card, pause it when a user opens another control, or cancel it when the card is removed. The example cancels the previous run before starting another. That lets a second click restart the effect without leaving multiple animations competing over opacity and position. When reduced motion becomes enabled, canceling the effect restores the visible card.
If your app only needs the initial entrance, the CSS version is easier to maintain. The JavaScript version earns its place when playback itself is part of the interaction. Think of a scrubber that updates the animation position, a notification that restarts when a new event comes in, or a preview that can play in reverse. Those controls become easier to understand when the code has an explicit animation object. See the Web Animations API documentation and reduced motion reference for the underlying browser behavior.
Use a timeline when several effects need to move together
A timeline is useful when timing becomes a relationship between effects. Consider a menu where the backdrop fades, the panel slides, and the links appear in a short stagger. The links should start before the panel has fully settled. If the panel opens more slowly on a later design pass, you want to adjust the overlap in one place. Independent delays can express this, but they force you to keep track of the dependencies whenever a duration changes.
A GSAP timeline lets you group those pieces and position them relative to each other. The timeline also gives you a common control for playback and reversal. That can make an interaction easier to revise, especially if opening and closing use the same sequence. It adds a library and another API for the team to maintain, so test whether one component actually needs that coordination before using a timeline for every small effect.
Write down which system owns each moving property. If the panel's CSS transition and your JavaScript timeline both change its transform, the results can be hard to reason about when the user interrupts the sequence. Give the motion to one system or put different transforms on separate elements. Test opening, closing, and reopening quickly. The panel should reach the correct state even if the previous animation was halfway through.
Pick a tool based on the part you want to replace
If you want a reusable CSS effect, Animista lets you preview and adjust motion before copying the styles into your project. You still decide what triggers the effect and how it behaves when motion is reduced. Start with one animation, keep the rules your component needs, and check that the effect plays in context. A transition that looks fine on an isolated sample may be distracting when several cards enter together.
If the trigger is specifically an element scrolling into view, compare AOS with the other options on AppyDev’s web animation tools page. Try it on a page with real content, including images or items loaded after the first render. Check that the content remains readable with motion disabled. If all you need is a little feedback when someone presses a button, start with a transition. That effect belongs to the state of the button rather than to the scroll position.
Use the same small test for each candidate. Start with the component you need to ship and ask whether it has a visible, usable resting state. Then try the interaction with the keyboard, interrupt it halfway through, and enable reduced motion. If the layout jumps or the component becomes unusable when the effect is interrupted, fix that before adding a more elaborate sequence. The best choice is the one that gives you the playback controls you need and leaves the component easy to use when the animation is skipped.
Check performance on the page where the effect will ship
Run the example on its own first so you know what it should look like. Then test the same motion on the real page, ideally on a phone or with CPU throttling in your browser's performance tools. Check whether tapping, scrolling, and typing still feel responsive while the effect runs. If the browser spends too much time recalculating layout or painting, changing the animation library alone may leave the same work in place. Look at what properties change and how many elements move together. MDN’s animation performance guide explains why the way an effect is implemented matters.
The card example uses transform to move the visual box without asking surrounding content to move with it. That works for a decorative entrance. An expanding panel has a different requirement because the content below it may need to move to make room. Test that component with short and long content before choosing an approach. A simple instant expansion with a brief opacity transition inside the panel can be enough when a height animation would make the page feel slow or unstable.
Finally, check how many effects are running on the first screen. An entrance that works for a single card can delay access to a whole list if every card starts hidden and waits for a turn. Try animating one meaningful change, such as the appearance of a newly completed export, and let the rest of the interface appear immediately.