Files
ISA-Frontend/.claude/skills/css-animations/references/keyframes-guide.md
Lorenz Hilpert 7612394ba1 ♻️ refactor(claude): reorganize and rename skills for clarity
Consolidate and rename skills to shorter, more intuitive names:
- css-keyframes-animations → css-animations
- api-sync-manager → api-sync
- architecture-documentation → arch-docs
- jest-vitest-patterns → test-migration
- reactive-state-patterns → state-patterns
- library-scaffolder → library-creator

Merge related skills:
- angular-template + html-template → template-standards
- angular-effects-alternatives + ngrx-resource-api → state-patterns

Remove obsolete skills:
- architecture-enforcer (merged into architecture-validator)
- circular-dependency-resolver (merged into architecture-validator)
- standalone-component-migrator (merged into migration-specialist agent)
- swagger-sync-manager (replaced by api-sync)
- api-change-analyzer (merged into api-sync)
- type-safety-engineer (content distributed to relevant skills)
- test-migration-specialist (replaced by migration-specialist agent)

Add migration-specialist agent for standalone and test migrations.
Update all cross-references in CLAUDE.md and agents.
2025-12-11 11:30:05 +01:00

16 KiB

CSS @keyframes Deep Dive

A comprehensive guide for Angular developers transitioning from @angular/animations to native CSS animations.


Table of Contents

  1. Understanding @keyframes
  2. Basic Syntax
  3. Animation Properties
  4. Timing Functions (Easing)
  5. Fill Modes
  6. Advanced Techniques
  7. Angular 20+ Integration
  8. Common Patterns & Recipes
  9. Performance Tips
  10. Debugging Animations

Understanding @keyframes

The @keyframes at-rule controls the intermediate steps in a CSS animation sequence by defining styles for keyframes (waypoints) along the animation. Unlike transitions (which only animate between two states), keyframes let you define multiple intermediate steps.

How It Differs from @angular/animations

@angular/animations Native CSS @keyframes
~60KB JavaScript bundle Zero JS overhead
CPU-based rendering GPU hardware acceleration
Angular-specific syntax Standard CSS (transferable skills)
trigger(), state(), animate() @keyframes + CSS classes

Basic Syntax

The @keyframes Rule

@keyframes animation-name {
  from {
    /* Starting styles (same as 0%) */
  }
  to {
    /* Ending styles (same as 100%) */
  }
}

Percentage-Based Keyframes

For more control, use percentages to define multiple waypoints:

@keyframes bounce {
  0% {
    transform: translateY(0);
  }
  25% {
    transform: translateY(-30px);
  }
  50% {
    transform: translateY(0);
  }
  75% {
    transform: translateY(-15px);
  }
  100% {
    transform: translateY(0);
  }
}

Combining Multiple Percentages

You can apply the same styles to multiple keyframes:

@keyframes pulse {
  0%, 100% {
    opacity: 1;
    transform: scale(1);
  }
  50% {
    opacity: 0.7;
    transform: scale(1.05);
  }
}

Applying the Animation

.element {
  animation: bounce 1s ease-in-out infinite;
}

Animation Properties

Individual Properties

Property Description Example
animation-name Name of the @keyframes animation-name: bounce;
animation-duration How long one cycle takes animation-duration: 2s;
animation-timing-function Speed curve (easing) animation-timing-function: ease-in;
animation-delay Wait before starting animation-delay: 500ms;
animation-iteration-count How many times to run animation-iteration-count: 3; or infinite
animation-direction Forward, reverse, or alternate animation-direction: alternate;
animation-fill-mode Styles before/after animation animation-fill-mode: forwards;
animation-play-state Pause or play animation-play-state: paused;

Shorthand Syntax

/* animation: name duration timing-function delay iteration-count direction fill-mode play-state */
.element {
  animation: slideIn 0.5s ease-out 0.2s 1 normal forwards running;
}

Minimum required: name and duration

.element {
  animation: fadeIn 1s;
}

Multiple Animations

Apply multiple animations to a single element:

.element {
  animation:
    fadeIn 0.5s ease-out,
    slideUp 0.5s ease-out,
    pulse 2s ease-in-out 0.5s infinite;
}

Timing Functions (Easing)

The timing function controls how the animation progresses over time—where it speeds up and slows down.

Keyword Values

Keyword Cubic-Bezier Equivalent Description
linear cubic-bezier(0, 0, 1, 1) Constant speed
ease cubic-bezier(0.25, 0.1, 0.25, 1) Default: slow start, fast middle, slow end
ease-in cubic-bezier(0.42, 0, 1, 1) Slow start, fast end
ease-out cubic-bezier(0, 0, 0.58, 1) Fast start, slow end
ease-in-out cubic-bezier(0.42, 0, 0.58, 1) Slow start and end

Custom Cubic-Bezier

Create custom easing curves with cubic-bezier(x1, y1, x2, y2):

/* Bouncy overshoot effect */
.element {
  animation-timing-function: cubic-bezier(0.68, -0.6, 0.32, 1.6);
}

/* Smooth deceleration */
.element {
  animation-timing-function: cubic-bezier(0.25, 1, 0.5, 1);
}

Tool: Use cubic-bezier.com to visualize and create custom curves.

/* Ease Out Quart - Great for enter animations */
cubic-bezier(0.25, 1, 0.5, 1)

/* Ease In Out Cubic - Smooth state changes */
cubic-bezier(0.65, 0, 0.35, 1)

/* Ease Out Back - Slight overshoot */
cubic-bezier(0.34, 1.56, 0.64, 1)

/* Ease In Out Back - Overshoot both ends */
cubic-bezier(0.68, -0.6, 0.32, 1.6)

Steps Function

For frame-by-frame animations (like sprite sheets):

/* 6 discrete steps */
.sprite {
  animation: walk 1s steps(6) infinite;
}

/* Step positions */
steps(4, jump-start)  /* Jump at start of each interval */
steps(4, jump-end)    /* Jump at end of each interval (default) */
steps(4, jump-both)   /* Jump at both ends */
steps(4, jump-none)   /* No jump at ends */

Timing Function Per Keyframe

Apply different easing to different segments:

@keyframes complexMove {
  0% {
    transform: translateX(0);
    animation-timing-function: ease-out;
  }
  50% {
    transform: translateX(100px);
    animation-timing-function: ease-in;
  }
  100% {
    transform: translateX(200px);
  }
}

Important: The timing function applies to each step individually, not the entire animation.


Fill Modes

Fill modes control what styles apply before and after the animation runs.

Values

Value Before Animation After Animation
none Original styles Original styles
forwards Original styles Last keyframe styles
backwards First keyframe styles Original styles
both First keyframe styles Last keyframe styles

Common Problem: Element Snaps Back

/* BAD: Element disappears then reappears after animation */
@keyframes fadeOut {
  from { opacity: 1; }
  to { opacity: 0; }
}

.element {
  animation: fadeOut 1s; /* Element snaps back to opacity: 1 */
}

/* GOOD: Element stays invisible */
.element {
  animation: fadeOut 1s forwards;
}

Backwards Fill Mode (for delays)

@keyframes slideIn {
  from {
    transform: translateX(-100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

/* Without backwards: element visible at original position during delay */
/* With backwards: element starts at first keyframe position during delay */
.element {
  animation: slideIn 0.5s ease-out 1s backwards;
}

Advanced Techniques

Animation Direction

Control playback direction:

animation-direction: normal;           /* 0% → 100% */
animation-direction: reverse;          /* 100% → 0% */
animation-direction: alternate;        /* 0% → 100% → 0% */
animation-direction: alternate-reverse; /* 100% → 0% → 100% */

Use Case: Breathing/pulsing effects

@keyframes breathe {
  from { transform: scale(1); }
  to { transform: scale(1.1); }
}

.element {
  animation: breathe 2s ease-in-out infinite alternate;
}

Staggered Animations

Create cascading effects with animation-delay:

.item { animation: fadeSlideIn 0.5s ease-out backwards; }
.item:nth-child(1) { animation-delay: 0ms; }
.item:nth-child(2) { animation-delay: 100ms; }
.item:nth-child(3) { animation-delay: 200ms; }
.item:nth-child(4) { animation-delay: 300ms; }

/* Or use CSS custom properties */
.item {
  animation: fadeSlideIn 0.5s ease-out backwards;
  animation-delay: calc(var(--i, 0) * 100ms);
}

In your template:

<div class="item" style="--i: 0">First</div>
<div class="item" style="--i: 1">Second</div>
<div class="item" style="--i: 2">Third</div>

@starting-style (Modern CSS)

Define styles for when an element first enters the DOM:

.modal {
  opacity: 1;
  transform: scale(1);
  transition: opacity 0.3s, transform 0.3s;

  @starting-style {
    opacity: 0;
    transform: scale(0.9);
  }
}

Animating Auto Height

Use CSS Grid for height: auto animations:

.accordion-content {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.3s ease-out;
}

.accordion-content.open {
  grid-template-rows: 1fr;
}

.accordion-content > div {
  overflow: hidden;
}

Pause/Play with CSS

.element {
  animation: spin 2s linear infinite;
  animation-play-state: running;
}

.element:hover {
  animation-play-state: paused;
}

/* Or with a class */
.element.paused {
  animation-play-state: paused;
}

Angular 20+ Integration

Using animate.enter and animate.leave

Angular 20.2+ provides animate.enter and animate.leave to apply CSS classes when elements enter/leave the DOM.

@Component({
  selector: 'app-example',
  template: `
    @if (isVisible()) {
      <div animate.enter="fade-in" animate.leave="fade-out">
        Content here
      </div>
    }
    <button (click)="toggle()">Toggle</button>
  `,
  styles: [`
    .fade-in {
      animation: fadeIn 0.3s ease-out;
    }

    .fade-out {
      animation: fadeOut 0.3s ease-in;
    }

    @keyframes fadeIn {
      from { opacity: 0; }
      to { opacity: 1; }
    }

    @keyframes fadeOut {
      from { opacity: 1; }
      to { opacity: 0; }
    }
  `]
})
export class ExampleComponent {
  isVisible = signal(false);
  toggle() { this.isVisible.update(v => !v); }
}

Dynamic Animation Classes

@Component({
  template: `
    @if (show()) {
      <div [animate.enter]="enterAnimation()" [animate.leave]="leaveAnimation()">
        Dynamic animations!
      </div>
    }
  `
})
export class DynamicAnimComponent {
  show = signal(false);
  enterAnimation = signal('slide-in-right');
  leaveAnimation = signal('slide-out-left');
}

Reusable Animation CSS File

Create a shared animations.css:

/* animations.css */

/* Fade animations */
.fade-in { animation: fadeIn 0.3s ease-out; }
.fade-out { animation: fadeOut 0.3s ease-in; }

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes fadeOut {
  from { opacity: 1; }
  to { opacity: 0; }
}

/* Slide animations */
.slide-in-up { animation: slideInUp 0.3s ease-out; }
.slide-out-down { animation: slideOutDown 0.3s ease-in; }

@keyframes slideInUp {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes slideOutDown {
  from {
    opacity: 1;
    transform: translateY(0);
  }
  to {
    opacity: 0;
    transform: translateY(20px);
  }
}

/* Scale animations */
.scale-in { animation: scaleIn 0.2s ease-out; }
.scale-out { animation: scaleOut 0.2s ease-in; }

@keyframes scaleIn {
  from {
    opacity: 0;
    transform: scale(0.9);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes scaleOut {
  from {
    opacity: 1;
    transform: scale(1);
  }
  to {
    opacity: 0;
    transform: scale(0.9);
  }
}

Import in styles.css or angular.json:

@import 'animations.css';

Common Patterns & Recipes

Loading Spinner

@keyframes spin {
  to { transform: rotate(360deg); }
}

.spinner {
  width: 40px;
  height: 40px;
  border: 3px solid #f3f3f3;
  border-top: 3px solid #3498db;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

Skeleton Loading

@keyframes shimmer {
  0% { background-position: -200% 0; }
  100% { background-position: 200% 0; }
}

.skeleton {
  background: linear-gradient(
    90deg,
    #f0f0f0 25%,
    #e0e0e0 50%,
    #f0f0f0 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}

Attention Pulse

@keyframes attention-pulse {
  0%, 100% {
    box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.5);
  }
  50% {
    box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
  }
}

.notification-badge {
  animation: attention-pulse 2s ease-in-out infinite;
}

Shake (Error Feedback)

@keyframes shake {
  0%, 100% { transform: translateX(0); }
  10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
  20%, 40%, 60%, 80% { transform: translateX(5px); }
}

.error-input {
  animation: shake 0.5s ease-in-out;
}

Slide Down Menu

@keyframes slideDown {
  from {
    opacity: 0;
    transform: translateY(-10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.dropdown-menu {
  animation: slideDown 0.2s ease-out forwards;
}

Toast Notification

@keyframes toastIn {
  from {
    opacity: 0;
    transform: translateY(100%) scale(0.9);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes toastOut {
  from {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
  to {
    opacity: 0;
    transform: translateY(100%) scale(0.9);
  }
}

.toast {
  animation: toastIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.toast.leaving {
  animation: toastOut 0.2s ease-in forwards;
}

Performance Tips

Use Transform and Opacity

These properties are GPU-accelerated and don't trigger layout:

/* GOOD - GPU accelerated */
@keyframes good {
  from { transform: translateX(0); opacity: 0; }
  to { transform: translateX(100px); opacity: 1; }
}

/* AVOID - Triggers layout recalculation */
@keyframes avoid {
  from { left: 0; width: 100px; }
  to { left: 100px; width: 200px; }
}

Use will-change Sparingly

.element {
  will-change: transform, opacity;
}

/* Remove after animation */
.element.animation-complete {
  will-change: auto;
}

Respect Reduced Motion

@keyframes fadeSlide {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.element {
  animation: fadeSlide 0.3s ease-out;
}

@media (prefers-reduced-motion: reduce) {
  .element {
    animation: none;
    /* Or use a simpler fade */
    animation: fadeIn 0.1s ease-out;
  }
}

Avoid Animating Layout Properties

Properties that trigger layout (reflow):

  • width, height
  • top, left, right, bottom
  • margin, padding
  • font-size
  • border-width

Use transform: scale() instead of width/height when possible.


Debugging Animations

Browser DevTools

  1. Chrome DevTools → More Tools → Animations

    • Pause, slow down, or step through animations
    • Inspect timing curves
  2. Firefox → Inspector → Animations tab

    • Visual timeline of all animations

Force Slow Motion

/* Temporarily add to debug */
* {
  animation-duration: 3s !important;
}

Animation Events in JavaScript

element.addEventListener('animationstart', (e) => {
  console.log('Started:', e.animationName);
});

element.addEventListener('animationend', (e) => {
  console.log('Ended:', e.animationName);
  // Clean up class, remove element, etc.
});

element.addEventListener('animationiteration', (e) => {
  console.log('Iteration:', e.animationName);
});

Common Issues

Problem Solution
Animation not running Check animation-duration is > 0
Element snaps back Add animation-fill-mode: forwards
Animation starts wrong Use animation-fill-mode: backwards with delay
Choppy animation Use transform instead of layout properties
Animation restarts on state change Ensure Angular doesn't recreate the element

Quick Reference Card

/* Basic setup */
@keyframes name {
  from { /* start */ }
  to { /* end */ }
}

.element {
  animation: name 0.3s ease-out forwards;
}

/* Angular 20+ */
<div animate.enter="fade-in" animate.leave="fade-out">

/* Shorthand order */
animation: name duration timing delay count direction fill-mode state;

/* Common timing functions */
ease-out: cubic-bezier(0, 0, 0.58, 1)      /* Enter animations */
ease-in: cubic-bezier(0.42, 0, 1, 1)       /* Exit animations */
ease-in-out: cubic-bezier(0.42, 0, 0.58, 1) /* State changes */

/* Fill modes */
forwards   Keep end state
backwards  Apply start state during delay
both       Both of the above

Resources