| |

Tailwind CSS 64 🎨 Custom CSS Keyframes and Advanced Multi-Stage Keyframe Timing

Tailwind ships four animations by default: animate-spin, animate-ping, animate-pulse, and animate-bounce. They cover the common loading and attention states, and they are enough for a prototype. A production interface needs more: a notification that slides in and settles, a modal that scales up and fades, a skeleton that shimmers, a checkmark that draws itself. The @keyframes rule is the mechanism, and the Tailwind theme extension is how the keyframes and the animation utilities are registered. This chapter covers the registration, the multi-stage timing, and the patterns that make the animations feel designed.

Key point: A Tailwind animation utility is a CSS animation shorthand. The theme.extend.animation entry sets the shorthand, and the theme.extend.keyframes entry defines the @keyframes rule. The two are separate entries, and both are required for the animation to work.


Why custom keyframes matter

The default problem. The four built-in animations cover the loading spinner and the attention pulse. They do not cover the entrance, the exit, the morph, or the sequence. The custom keyframes are the answer.

The multi-stage problem. A notification that slides in and then fades requires two stages: the slide and the fade. A single from/to pair cannot express both. The percentage keyframes (0%, 50%, 100%) express the intermediate states.

The easing problem. A bounce requires the cubic-bezier easing. A spring requires the linear() easing function with the multiple points. The animation-timing-function and the per-keyframe easing control the feel.

The composition problem. A complex animation is the composition of the smaller ones. The modal entrance is the fade and the scale, and the two run at the same time. The animation shorthand accepts the comma-separated list.

The performance problem. The animations that trigger the layout (the width, the height, the top) run on the main thread. The animations that use the transform and the opacity run on the compositor. The rule is to animate the transform and the opacity where possible.

The reduced-motion problem. The animations can trigger the motion sickness. The motion-reduce: variant and the prefers-reduced-motion media query disable the animation for the users who opt out.

The design-system problem. The animations are part of the design system. The durations, the easings, and the keyframes are the tokens. The theme.extend is the registration, and the components use the utility names.


a. Registering the keyframes and the animations

The Tailwind theme has the keyframes and the animation entries. The keyframes defines the @keyframes rule, and the animation sets the animation shorthand.

The basic registration

// tailwind.config.js
export default {
  theme: {
    extend: {
      keyframes: {
        wiggle: {
          '0%, 100%': { transform: 'rotate(-3deg)' },
          '50%': { transform: 'rotate(3deg)' },
        },
      },
      animation: {
        wiggle: 'wiggle 1s ease-in-out infinite',
      },
    },
  },
};

The keyframes.wiggle defines the rule, and the animation.wiggle sets the shorthand. The utility animate-wiggle is generated .

The usage

<div class="animate-wiggle">Wiggle</div>

The class applies the animation: wiggle 1s ease-in-out infinite.

The multi-stage keyframes

The percentage keyframes express the intermediate states.

keyframes: {
  slideIn: {
    '0%': { transform: 'translateY(-100%)', opacity: '0' },
    '60%': { transform: 'translateY(10%)', opacity: '1' },
    '80%': { transform: 'translateY(-5%)' },
    '100%': { transform: 'translateY(0)' },
  },
},
animation: {
  slideIn: 'slideIn 0.6s ease-out',
},

The 0% is the start, the 60% overshoots, the 80% corrects, and the 100% settles. The multi-stage produces the bounce feel.

The per-keyframe easing

The animation-timing-function can be set inside the keyframe.

keyframes: {
  bounce: {
    '0%': { transform: 'translateY(0)', animationTimingFunction: 'ease-out' },
    '50%': { transform: 'translateY(-25%)', animationTimingFunction: 'ease-in' },
    '100%': { transform: 'translateY(0)' },
  },
},

The easing changes at each stage, and the motion feels more natural.

The keyframe with the CSS variables

The keyframe can reference the CSS variables for the customization.

keyframes: {
  slideIn: {
    '0%': { transform: 'translateX(var(--slide-from, -100%))' },
    '100%': { transform: 'translateX(0)' },
  },
},

The --slide-from is the custom property, and the component sets it.

The registration table

EntryPurpose
theme.extend.keyframesThe @keyframes rule
theme.extend.animationThe animation shorthand
animate-*The generated utility

b. The multi-stage timing

The multi-stage timing is the set of the techniques that make the animation feel designed rather than mechanical.

The anticipation

The anticipation is the small movement in the opposite direction before the main movement.

keyframes: {
  anticipate: {
    '0%': { transform: 'scale(1)' },
    '20%': { transform: 'scale(0.9)' },
    '100%': { transform: 'scale(1.1)' },
  },
},

The 20% is the anticipation, and the 100% is the result.

The overshoot

The overshoot is the movement past the target and back.

keyframes: {
  overshoot: {
    '0%': { transform: 'translateY(100%)' },
    '70%': { transform: 'translateY(-10%)' },
    '100%': { transform: 'translateY(0)' },
  },
},

The 70% passes the target, and the 100% settles.

The follow-through

The follow-through is the continuation of the motion after the main element stops.

keyframes: {
  cardEnter: {
    '0%': { transform: 'translateY(20px) scale(0.95)', opacity: '0' },
    '60%': { transform: 'translateY(-4px) scale(1.02)', opacity: '1' },
    '100%': { transform: 'translateY(0) scale(1)' },
  },
},

The 60% overshoots, and the 100% settles.

The stagger

The stagger is the delay between the sibling elements.

<div class="animate-fadeIn [animation-delay:0ms]">Item 1</div>
<div class="animate-fadeIn [animation-delay:100ms]">Item 2</div>
<div class="animate-fadeIn [animation-delay:200ms]">Item 3</div>

The arbitrary value [animation-delay:100ms] sets the delay. The pattern is used for the list entrance.

The infinite loop

The infinite value repeats the animation.

animation: {
  shimmer: 'shimmer 2s infinite',
},
keyframes: {
  shimmer: {
    '0%': { backgroundPosition: '-200% 0' },
    '100%': { backgroundPosition: '200% 0' },
  },
},

The infinite keyword repeats the shimmer.

The alternate direction

The alternate value reverses the animation on each iteration.

animation: {
  pulse: 'pulse 1s ease-in-out infinite alternate',
},

The alternate produces the back-and-forth motion.

The fill mode

The forwards value keeps the final state after the animation.

animation: {
  fadeIn: 'fadeIn 0.5s ease-out forwards',
},

The forwards keeps the 100% state, and the element does not revert.

The multi-stage table

TechniquePurpose
AnticipationThe opposite movement before
OvershootThe movement past the target
Follow-throughThe continuation after
StaggerThe delay between the siblings
InfiniteThe repeat
AlternateThe reverse on each iteration
ForwardsThe keep the final state

c. The common animation patterns

The patterns below are the ones that appear in the real interfaces.

The fade in

keyframes: {
  fadeIn: {
    '0%': { opacity: '0' },
    '100%': { opacity: '1' },
  },
},
animation: {
  fadeIn: 'fadeIn 0.5s ease-out',
},

The animate-fadeIn produces the fade.

The slide in

keyframes: {
  slideInUp: {
    '0%': { transform: 'translateY(100%)', opacity: '0' },
    '100%': { transform: 'translateY(0)', opacity: '1' },
  },
},
animation: {
  slideInUp: 'slideInUp 0.4s ease-out',
},

The animate-slideInUp produces the entrance from the bottom.

The scale in

keyframes: {
  scaleIn: {
    '0%': { transform: 'scale(0.95)', opacity: '0' },
    '100%': { transform: 'scale(1)', opacity: '1' },
  },
},
animation: {
  scaleIn: 'scaleIn 0.2s ease-out',
},

The animate-scaleIn produces the modal entrance.

The shimmer

keyframes: {
  shimmer: {
    '0%': { backgroundPosition: '-200% 0' },
    '100%': { backgroundPosition: '200% 0' },
  },
},
animation: {
  shimmer: 'shimmer 2s linear infinite',
},

The animate-shimmer produces the skeleton loading.

<div class="animate-shimmer bg-gradient-to-r from-slate-200 via-slate-100 to-slate-200 bg-[length:200%_100%]">
</div>

The gradient and the background size produce the shimmer.

The bounce

keyframes: {
  bounceIn: {
    '0%': { transform: 'translateY(-25%)', opacity: '0' },
    '50%': { transform: 'translateY(0)', opacity: '1' },
    '70%': { transform: 'translateY(-10%)' },
    '100%': { transform: 'translateY(0)' },
  },
},
animation: {
  bounceIn: 'bounceIn 0.6s ease-out',
},

The animate-bounceIn produces the entrance.

The spin with the delay

animation: {
  'spin-slow': 'spin 3s linear infinite',
  'spin-slower': 'spin 5s linear infinite',
},

The custom spin durations.

The draw

keyframes: {
  draw: {
    '0%': { strokeDashoffset: '100' },
    '100%': { strokeDashoffset: '0' },
  },
},
animation: {
  draw: 'draw 1s ease-out forwards',
},

The animate-draw produces the checkmark drawing.

<svg class="h-12 w-12">
  <path class="animate-draw stroke-current" stroke-dasharray="100" d="M 10 50 L 30 70 L 90 10" />
</svg>

The pattern table

PatternKeyframes
Fade inThe opacity
Slide inThe translate + the opacity
Scale inThe scale + the opacity
ShimmerThe background position
Bounce inThe translate + the overshoot
DrawThe stroke offset

Complete Example Session

// ============================================
// PART 1: THE BASIC KEYFRAMES
// ============================================

// tailwind.config.js
export default {
  theme: {
    extend: {
      keyframes: {
        wiggle: {
          '0%, 100%': { transform: 'rotate(-3deg)' },
          '50%': { transform: 'rotate(3deg)' },
        },
      },
      animation: {
        wiggle: 'wiggle 1s ease-in-out infinite',
      },
    },
  },
};

// ============================================
// PART 2: THE MULTI-STAGE
// ============================================

keyframes: {
  slideIn: {
    '0%': { transform: 'translateY(-100%)', opacity: '0' },
    '60%': { transform: 'translateY(10%)', opacity: '1' },
    '80%': { transform: 'translateY(-5%)' },
    '100%': { transform: 'translateY(0)' },
  },
},
animation: {
  slideIn: 'slideIn 0.6s ease-out',
},

// ============================================
// PART 3: THE PER-KEYFRAME EASING
// ============================================

keyframes: {
  bounce: {
    '0%': { transform: 'translateY(0)', animationTimingFunction: 'ease-out' },
    '50%': { transform: 'translateY(-25%)', animationTimingFunction: 'ease-in' },
    '100%': { transform: 'translateY(0)' },
  },
},

// ============================================
// PART 4: THE CSS VARIABLE
// ============================================

keyframes: {
  slideIn: {
    '0%': { transform: 'translateX(var(--slide-from, -100%))' },
    '100%': { transform: 'translateX(0)' },
  },
},

// ============================================
// PART 5: THE STAGGER
// ============================================

// <div class="animate-fadeIn [animation-delay:0ms]">Item 1</div>
// <div class="animate-fadeIn [animation-delay:100ms]">Item 2</div>
// <div class="animate-fadeIn [animation-delay:200ms]">Item 3</div>

// ============================================
// PART 6: THE SHIMMER
// ============================================

keyframes: {
  shimmer: {
    '0%': { backgroundPosition: '-200% 0' },
    '100%': { backgroundPosition: '200% 0' },
  },
},
animation: {
  shimmer: 'shimmer 2s linear infinite',
},

// <div class="animate-shimmer bg-gradient-to-r from-slate-200 via-slate-100 to-slate-200 bg-[length:200%_100%]" />

// ============================================
// PART 7: THE DRAW
// ============================================

keyframes: {
  draw: {
    '0%': { strokeDashoffset: '100' },
    '100%': { strokeDashoffset: '0' },
  },
},
animation: {
  draw: 'draw 1s ease-out forwards',
},

// <svg><path class="animate-draw stroke-current" stroke-dasharray="100" d="..." /></svg>

// ============================================
// PART 8: THE REDUCED MOTION
// ============================================

// <div class="animate-slideIn motion-reduce:animate-none">...</div>

// @media (prefers-reduced-motion: reduce) {
//   .animate-slideIn { animation: none; }
// }

// ============================================
// PART 9: THE COMPOSITION
// ============================================

// <div class="animate-fadeIn animate-scaleIn">...</div>
// The two animations run at the same time.

animation: {
  'fadeIn-scaleIn': 'fadeIn 0.3s ease-out, scaleIn 0.3s ease-out',
},

// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================

// 1. Register the keyframes in theme.extend.keyframes.
// 2. Register the animation in theme.extend.animation.
// 3. Use the percentage keyframes for the multi-stage.
// 4. Use the per-keyframe easing for the natural motion.
// 5. Use the CSS variables for the customization.
// 6. Use the animation-delay for the stagger.
// 7. Use the fill-mode forwards for the final state.
// 8. Use the motion-reduce variant for the accessibility.
// 9. Use the transform and the opacity for the performance.
// 10. Use the comma-separated animation for the composition.

The ten parts showed the basic keyframes, the multi-stage, the per-keyframe easing, the CSS variable, the stagger, the shimmer, the draw, the reduced motion, the composition, and the complete pattern.


Quick Reference

The Theme Entries

EntryPurpose
theme.extend.keyframesThe @keyframes rule
theme.extend.animationThe animation shorthand
theme.extend.transitionDurationThe custom durations
theme.extend.transitionTimingFunctionThe custom easings

The Animation Shorthand

ValuePurpose
name duration timing delay iteration direction fillThe order

The Common Properties

PropertyPurpose
animation-nameThe keyframes name
animation-durationThe duration
animation-timing-functionThe easing
animation-delayThe delay
animation-iteration-countThe repeat
animation-directionThe alternate
animation-fill-modeThe forwards

The Multi-Stage Techniques

TechniqueKeyframe Positions
Anticipation0%, 20%, 100%
Overshoot0%, 70%, 100%
Follow-through0%, 60%, 100%
Bounce0%, 50%, 70%, 100%

The Animation Patterns

PatternKeyframes
Fade inThe opacity
Slide inThe translate + the opacity
Scale inThe scale + the opacity
ShimmerThe background position
Bounce inThe translate + the overshoot
DrawThe stroke offset

Best Practices

✅ Do This:

// Register the keyframes and the animation together.
keyframes: { wiggle: { ... } },
animation: { wiggle: 'wiggle 1s ease-in-out infinite' },  // ✅

// Use the percentage keyframes for the multi-stage.
keyframes: {
  slideIn: {
    '0%': { ... },
    '60%': { ... },
    '100%': { ... },
  },
},  // ✅

// Use the per-keyframe easing.
'0%': { transform: 'translateY(0)', animationTimingFunction: 'ease-out' },  // ✅

// Use the CSS variables for the customization.
'0%': { transform: 'translateX(var(--slide-from, -100%))' },  // ✅

// Use the animation-delay for the stagger.
<div class="animate-fadeIn [animation-delay:100ms]">  // ✅

// Use the motion-reduce variant.
<div class="animate-slideIn motion-reduce:animate-none">  // ✅

// Use the transform and the opacity.
keyframes: { fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' } } },  // ✅

❌ Don’t Do This:

// Don't define the keyframes without the animation.
keyframes: { wiggle: { ... } },  // ❌ No animate-wiggle utility

// Don't use only the from/to for the multi-stage.
keyframes: {
  slideIn: {
    from: { transform: 'translateY(-100%)' },
    to: { transform: 'translateY(0)' },
  },
},  // ❌ No overshoot

// Don't animate the layout properties.
keyframes: {
  grow: {
    '0%': { width: '0' },
    '100%': { width: '100%' },
  },
},  // ❌ The main thread

// Don't forget the motion-reduce.
<div class="animate-spin">  // ❌ The motion sickness

// Don't use the same name for the keyframes and the utility.
animation: { wiggle: 'wiggle 1s' },  // ⚠️ The name collision is confusing

// Don't use a very long duration.
animation: { slow: 'spin 10s linear infinite' },  // ⚠️ The user waits

Common Pitfalls

PitfallWhy It HappensFix
Animation does not runThe animation entry is missingAdd the animation
Class not generatedThe keyframes name mismatchMatch the names
No overshootOnly from/to usedUse the percentage
Motion sicknessNo reduced-motionAdd the motion-reduce: variant
JankyThe layout properties animatedUse the transform and the opacity
RevertsThe forwards is missingAdd the fill mode
Same delayNo animation-delayAdd the stagger

Real-World Examples

1. The Wiggle

animation: { wiggle: 'wiggle 1s ease-in-out infinite' }

2. The Slide In

animation: { slideIn: 'slideIn 0.6s ease-out' }

3. The Per-Keyframe Easing

'0%': { transform: 'translateY(0)', animationTimingFunction: 'ease-out' }

4. The CSS Variable

'0%': { transform: 'translateX(var(--slide-from, -100%))' }

5. The Stagger

<div class="animate-fadeIn [animation-delay:100ms]">

6. The Shimmer

animation: { shimmer: 'shimmer 2s linear infinite' }

7. The Draw

animation: { draw: 'draw 1s ease-out forwards' }

8. The Reduced Motion

<div class="animate-slideIn motion-reduce:animate-none">

9. The Composition

animation: { 'fadeIn-scaleIn': 'fadeIn 0.3s, scaleIn 0.3s' }

10. The Complete Pattern

keyframes: { slideIn: { ... } },
animation: { slideIn: 'slideIn 0.6s ease-out' },

Visual

The Registration

┌────────────────────────────────────────────────────────────┐
│  THE REGISTRATION                                          │
│                                                            │
│  tailwind.config.js                                        │
│  ┌──────────────────────────────────────────────────┐     │
│  │  theme: {                                         │     │
│  │    extend: {                                      │     │
│  │      keyframes: {                                 │     │
│  │        wiggle: {                                  │     │
│  │          '0%, 100%': { transform: 'rotate(-3deg)' },│    │
│  │          '50%': { transform: 'rotate(3deg)' },    │     │
│  │        },                                         │     │
│  │      },                                           │     │
│  │      animation: {                                 │     │
│  │        wiggle: 'wiggle 1s ease-in-out infinite',  │     │
│  │      },                                           │     │
│  │    },                                             │     │
│  │  }                                                │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The keyframes define the rule. The animation sets the     │
│  shorthand. The animate-wiggle utility is generated.       │
└────────────────────────────────────────────────────────────┘

The Multi-Stage Timing

┌────────────────────────────────────────────────────────────┐
│  THE MULTI-STAGE TIMING                                    │
│                                                            │
│  0% ─────▶ 60% ─────▶ 80% ─────▶ 100%                     │
│  start     overshoot  correct    settle                    │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  translateY(-100%)  ← 0%                          │     │
│  │  translateY(10%)    ← 60% (overshoot)             │     │
│  │  translateY(-5%)    ← 80% (correct)               │     │
│  │  translateY(0)      ← 100% (settle)               │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The overshoot and the correction produce the bounce.      │
└────────────────────────────────────────────────────────────┘

The Stagger

┌────────────────────────────────────────────────────────────┐
│  THE STAGGER                                               │
│                                                            │
│  Item 1  ─────▶  (delay 0ms)                              │
│  Item 2    ─────▶  (delay 100ms)                          │
│  Item 3      ─────▶  (delay 200ms)                        │
│  Item 4        ─────▶  (delay 300ms)                      │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  <div class="animate-fadeIn [animation-delay:0ms]">│    │
│  │  <div class="animate-fadeIn [animation-delay:100ms]">│  │
│  │  <div class="animate-fadeIn [animation-delay:200ms]">│  │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The cascade of the delays produces the wave.              │
└────────────────────────────────────────────────────────────┘

The Reduced Motion

┌────────────────────────────────────────────────────────────┐
│  THE REDUCED MOTION                                        │
│                                                            │
│  <div class="animate-slideIn motion-reduce:animate-none">  │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  Without the preference:                          │     │
│  │    animation: slideIn 0.6s ease-out               │     │
│  │                                                   │     │
│  │  With prefers-reduced-motion: reduce:             │     │
│  │    animation: none                                │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The accessibility requirement.                            │
└────────────────────────────────────────────────────────────┘

Summary

ItemValue
theme.extend.keyframesThe @keyframes rule
theme.extend.animationThe animation shorthand
animate-*The generated utility
Multi-stageThe percentage keyframes
Per-keyframe easingThe animationTimingFunction inside the keyframe
StaggerThe animation-delay
forwardsThe keep the final state
infiniteThe repeat
alternateThe reverse
motion-reduce:The accessibility variant

Key takeaways:

  • The keyframes and the animation are separate entries. The theme.extend.keyframes defines the rule, and the theme.extend.animation sets the shorthand. The animate-* utility is generated from the animation entry.
  • The percentage keyframes express the multi-stage. The 0%, the 60%, the 80%, and the 100% produce the overshoot, the correction, and the settle. The from/to pair cannot express the intermediate states.
  • The per-keyframe easing makes the motion natural. The animationTimingFunction inside the keyframe changes the easing at each stage. The anticipation, the overshoot, and the follow-through are the techniques.
  • The CSS variables make the keyframes customizable. The var(--slide-from, -100%) is the default with the override. The component sets the custom property.
  • The animation-delay produces the stagger. The arbitrary value [animation-delay:100ms] sets the delay. The cascade of the delays produces the wave.
  • The forwards fill mode keeps the final state. The element does not revert after the animation. The infinite repeats, and the alternate reverses.
  • The motion-reduce: variant is the accessibility. The motion-reduce:animate-none disables the animation for the users who prefer reduced motion. The 3D and the bouncing animations are the most common offenders.
  • The transform and the opacity are the performant properties. The animations that use them run on the compositor. The layout properties run on the main thread, and the jank is the result.

Remember: The custom keyframes and the multi-stage timing are the mechanism for the animations that the default utilities do not cover. The theme.extend.keyframes defines the rule, and the theme.extend.animation sets the shorthand. The percentage keyframes produce the overshoot and the bounce, the per-keyframe easing makes the motion natural, and the animation-delay produces the stagger. The motion-reduce: variant is the accessibility, and the transform and the opacity are the performant properties. The result is an interface where the entrances and the exits feel designed, the loading states shimmer, and the checkmarks draw themselves.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!