Animation Without the Chaos

Animations often bring a mix of delight and frustration to interface work. Without a clear structure, they quickly become a source of confusion. This is especially true in the wild: open almost any established codebase and you will find multiple @keyframes definitions for what is essentially the same effect. Three fade-ins, two or three slide variations, a handful of zooms, and at least two spins are common.

Consolidating and standardizing these definitions is one of the most rewarding first tasks on a new project. It makes a strong case for treating keyframes as tokens, just like colors or spacing, and centralizing them in a shared, maintainable stylesheet.

Why Keyframes Proliferate

The redundancy is understandable. Developers use the same fundamental animations daily: fades, slides, zooms, spins. These are simple exercises, and it is often faster to write a quick @keyframes rule than to search for an existing one. This is particularly true in component-based architectures, where teams work in parallel. The result is scattered definitions that are rarely identical, creating maintenance nightmares and opening the door to subtle bugs.

The Global Scope Trap

These duplicated keyframes are not just untidy; they are dangerous. Despite the component-based approach, CSS keyframes are always global. Every definition applies to every component. The animation you intend to use may be overwritten by the @keyframes rule with the same name that was loaded last.

As long as the definitions are identical, nothing breaks. The moment you customize one for a specific case, you face a dilemma. Either your animation silently falls back to the wrong definition, or your component loads last and unintentionally changes the behavior of every other component referencing that name.

Both components in the example above use the same animation name. The second definition overwrites the first, forcing both components to use the latter, regardless of which component originally defined which rule. This can work flawlessly in local development but fail mysteriously in production, where style loading order often changes.

A Single Source of Truth

The fix is to treat keyframes like any other design token. Create a dedicated shared stylesheet that holds centralized keyframes. These should be well-documented, reusable, and tailored to your project’s specific needs.

This solves duplicated code and global scope issues in one stroke. There is no more guessing if a fade already exists. There is no more accidental overwriting. These tokens are also dynamic: they are built with CSS custom properties, so they can adapt to specific use cases, like needing a slightly larger pulse in one corner of the app.

Your First Token: Fade-In

The most straightforward token to build is the ubiquitous fade-in. Many projects contain a dozen or more variations of this simple effect, all animating opacity from 0 to 1. Create a kf-tokens.css file and import it into your project, then define a single keyframe with an explanatory comment.

Use a kf- prefix on all keyframe names. It namespaces your file, prevents conflicts, and signals that these definitions come from your token library.

The Flexible Slide

Fade-ins are simple because they have no parameters. Slides are more interesting because they raise a question: slide from where? From 100px right? From 50% left, top, or bottom? Building separate keyframes for each direction is wasteful. Instead, build one rule that takes a custom property for the starting position.

This single @keyframes declaration then serves any slide direction by changing the custom property value. One rule, infinite possibilities. You can even add a second custom property to create slide-out effects as well.

Bidirectional Zooms

Zoom effects are similarly over-duplicated. Whether it is a subtle scale-up for a toast or a larger zoom for a modal, you can consolidate them all into one flexible token. One definition can handle any scale variation, from 80% (good for standard UI elements) to larger or smaller values via custom properties.

These examples also illustrate that keyframe tokens are designed to integrate well with each other. Combining them yields complex and intentional visual behavior, something we will explore further in a later section.

Managing Continuous Motion

Entrance animations run once and stop. Continuous animations like spin and pulse require further versatility, as they need to support various speeds, directions, and behaviors.

A Universal Spin

You will find projects with spins that go clockwise, counterclockwise, and across a range of turn counts. A well-designed token can handle all these cases by letting you define the exact rotation pattern. The same keyframe with custom properties can drive a loading spinner, a rotating icon, or a quick wiggle effect.

The Pulse Paradox

Pulse animations present another challenge: they can pulse scale, opacity, or even color. Rather than authoring separate keyframes for each property, you can create a keyframe that works with any CSS property that you want to animate. A single token can then handle both subtle attention grabs and dramatic highlights.

Next-Level Motion: Bounce and Elastic

Tokens also make advanced easing patterns accessible. Commonly skipped effects like bounce become as easy to use as a fade. For example, a strong bounce will have 500ms as its sweet spot duration, while heavier sections will benefit from slightly longer timing. A simple bounce token uses a custom property to control the jump height.

Implementing an elastic entrance is trickier because it requires more complex calculations inside the keyframes. You use separate custom properties for the horizontal and vertical starting points. Together, they allow an elastic entrance that comes from any point on the screen. Reusing these advanced effects across a project can be accomplished by changing a single custom property, making them far more common and practical.

Combining Keyframes Without Conflicts

Layering basic keyframes tokens is straightforward when each animation targets a different property. You define the first animation, define the second, set the relevant variables, and the browser handles the rest.

/* Fade in + slide in */
.toast {
  animation:
    kf-fade-in 0.4s,
    kf-slide-in 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
  --kf-slide-from: 0 40px;
}

/* Zoom in + fade in */
.modal {
  animation:
    kf-fade-in 0.3s,
    kf-zoom 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
  --kf-zoom-from: 0.7;
  --kf-zoom-to: 1;
}

/* Slide in + pulse */
.notification {
  animation:
    kf-slide-in 0.5s,
    kf-pulse 1.2s ease-in-out infinite alternate;
  --kf-slide-from: -100px 0;
  --kf-pulse-scale-from: 0.95;
  --kf-pulse-scale-to: 1.05;
}

That approach breaks down when two animations try to control the *same* property, say opacity or scale. By default, CSS only applies the last animation in the list for any given property; the earlier one is silently ignored. The following snippet, for instance, will only run kf-pulse, not the intended zoom-and-pulse effect.

.bad-combo {
  animation:
    kf-zoom 0.5s forwards,
    kf-pulse 1.2s infinite alternate;
  --kf-zoom-from: 0.5;
  --kf-zoom-to: 1.2;
  --kf-pulse-scale-from: 0.8;
  --kf-pulse-scale-to: 1.1;
}

Using animation-composition

The direct fix for same-property collisions is the animation-composition property. Without it, kf-pulse replaces kf-zoom, so you lose the initial zoom and never reach the expected scale value of 1.2. Setting animation-composition to add tells the browser to combine both animations rather than let one overwrite the other.

.component-two {
  animation-composition: add;
}

See the Pen [Keyframes Tokens - Demo 8 [forked]](https://codepen.io/smashingmag/pen/YPqrYZw) by Amit Sheen.

See the Pen Keyframes Tokens - Demo 8 [forked] by Amit Sheen.

This technique is also handy when you need to animate an element that already has a static transform. For example, an element positioned with the translate property will visibly jump before a kf-slide-in animation begins without animation-composition. Adding it keeps the element in place and animates it smoothly.

See the Pen [Keyframes Tokens - Demo 9 [forked]](https://codepen.io/smashingmag/pen/myPBpWr) by Amit Sheen.

See the Pen Keyframes Tokens - Demo 9 [forked] by Amit Sheen.

Staggering Animations

Another way to manage multiple animations is to stagger them: start the second only after the first finishes. This works well for sequences like an entrance animation followed by a looping idle animation.

/* fade in + opacity pulse */
.notification {
  animation:
    kf-fade-in 2s ease-out,
    kf-pulse 0.5s 2s ease-in-out infinite alternate;
  --kf-pulse-opacity-to: 0.5;
}

See the Pen [Keyframes Tokens - Demo 10 [forked]](https://codepen.io/smashingmag/pen/bNpoaqo) by Amit Sheen.

See the Pen Keyframes Tokens - Demo 10 [forked] by Amit Sheen.

Transform Order and Its Consequences

Most animations in interface work rely on the transform property, largely for convenience and GPU acceleration. But with transforms, order is critical. Individual transform properties — translate, rotate, scale — are always applied in that fixed sequence. The transform property, in contrast, applies its functions in the order written.

/* Pink square: First translate, then rotate */ 
.example-one {
  transform: translateX(100px) rotate(45deg);
}

/* Green square: First rotate, then translate */
.example-two { 
  transform: rotate(45deg) translateX(100px);
}

See the Pen [Keyframes Tokens - Demo 11 [forked]](https://codepen.io/smashingmag/pen/zxqEpZb) by Amit Sheen.

See the Pen Keyframes Tokens - Demo 11 [forked] by Amit Sheen.

Individual transforms inside keyframes all execute before any functions in the transform property. So setting translate on an element that also uses the kf-spin keyframes moves it before the rotation begins. Putting the equivalent translate() function inside transform causes the rotation to happen first, shifting the element relative to its rotated angle.

/* Common animation for both spinners */ 
.spinner {
  animation: kf-spin 1s linear infinite;
}

/* Pink spinner: translate before rotate (individual transform) */
.spinner-pink {
  translate: 100% 50%;
}

/* Green spinner: rotate then translate (function order) */
.spinner-green {
  transform: translate(100%, 50%);
}

See the Pen [Keyframes Tokens - Demo 12 [forked]](https://codepen.io/smashingmag/pen/NPNaXjw) by Amit Sheen.

See the Pen Keyframes Tokens - Demo 12 [forked] by Amit Sheen.

This is not a bug. It is a quirk to keep in mind when mixing static transforms with keyframes tokens. If needed, you can create alternative keyframes — like a kf-spin-alt — that use the rotate() function instead of the individual rotate property.

Building in Reduced Motion

Keyframes tokens make accessibility easier to enforce because reduced-motion handling can be baked into the tokens themselves. The right response to prefers-reduced-motion varies by animation type.

Muting Certain Animations

Some animations, like pulse effects, should disappear entirely for users who request reduced motion. Wrapping those keyframes in the appropriate media query removes them without any extra code.


@media (prefers-reduced-motion: no-preference) {
  @keyfrmaes kf-pulse {
    from {
      scale: var(--kf-pulse-scale-from, 1);
      opacity: var(--kf-pulse-opacity-from, 1);
    }
    to {
      scale: var(--kf-pulse-scale-to, 1);
      opacity: var(--kf-pulse-opacity-to, 1);
    }
  }
}

Instant Alternatives for Entrance Animations

Entrance animations often cannot simply be dropped because the element depends on the animation’s final values. The solution is a default set of keyframes that jumps straight to the end state, with the full animation added only inside a media query for prefers-reduced-motion: no-preference.

/* pop in instantly for reduced motion */
@keyframes kf-zoom {
  from, to {
    scale: var(--kf-zoom-to, 1);
  }
}

@media (prefers-reduced-motion: no-preference) {
  /* Original zoom keyframes */
  @keyframes kf-zoom {
    from {
      scale: var(--kf-zoom-from, 0.8);
    }
    to {
      scale: var(--kf-zoom-to, 1);
    }
  }
}

Users who prefer reduced motion see the element appear instantly; everyone else gets the animated transition.

Softer Versions

For cases where complete removal feels wrong, a calmer alternative can preserve a sense of appearance without the intensity. A bounce entrance, for example, can be swapped for a gentle fade-in.


@keyframes kf-bounce {
  /* Soft fade-in for reduced motion */
}

@media (prefers-reduced-motion: no-preference) {
  @keyframes kf-bounce {
    /* Original bounce keyframes */
  }
}

Practical Adoption Strategies

Building a token library is one thing; getting teams to use it reliably is another. A few practices help.

  • Adopt gradually. Start with the most common animations like fades and slides. They deliver immediate value without requiring broad rewrites.
  • Namespace consistently. A clear prefix distinguishes token keyframes from one-off local animations and prevents accidental collisions.
  • Document inline. A short comment above each token saves future developers from guessing. They should be able to scan the file, find the effect, and copy the usage pattern.
  • Expose only useful knobs. Sensible custom properties give flexibility without overcomplicating the system. Provide the variables that matter; keep the rest opinionated.
  • Design for reduced motion from the start. Not every animation needs an alternative, but many do. Building adjustments in early means they are never retrofitted later.

Treating keyframes as part of the design token workflow changes how they are perceived. They stop being scattered tricks and become part of the product’s design language — the way motion expresses the interface.