CSS3 & Responsive Design

CSS Keyframe Animations

35 min Lesson 41 of 60

What Are CSS Keyframe Animations?

CSS keyframe animations allow you to create complex, multi-step animations entirely in CSS without any JavaScript. While CSS transitions let you animate between two states (a start and an end), keyframe animations let you define as many intermediate steps as you want. You can make an element spin, bounce, pulse, fade through multiple colors, or follow a complex motion path -- all by describing keyframes that the browser interpolates between smoothly.

Keyframe animations consist of two parts: the @keyframes rule that defines the animation sequence, and the animation properties that apply that sequence to an element. This separation of definition and application means you can reuse the same animation on multiple elements with different durations, delays, and timing functions. Once you understand keyframe animations, you can build loading spinners, attention-grabbing UI effects, page entrance animations, and much more -- all with pure CSS.

The @keyframes Rule

The @keyframes at-rule defines the stages of an animation. You give the animation a name and then describe what styles should apply at various points during the animation. The browser fills in the gaps between your keyframes using interpolation, creating a smooth animation.

Naming Keyframes

Every @keyframes rule needs a name. This name is a custom identifier that you will reference later when applying the animation to an element. The name follows the same rules as CSS custom identifiers: it can contain letters, numbers, hyphens, and underscores, but cannot start with a number. Choose descriptive names that convey what the animation does.

Naming Your Keyframe Animations

/* Good: descriptive names */
@keyframes fadeIn { /* ... */ }
@keyframes slideUpFromBottom { /* ... */ }
@keyframes pulseGlow { /* ... */ }
@keyframes spinClockwise { /* ... */ }
@keyframes bounceIn { /* ... */ }

/* Avoid: vague or confusing names */
@keyframes anim1 { /* ... */ }
@keyframes myAnimation { /* ... */ }

/* Valid but not recommended: names that match CSS keywords */
/* Avoid names like "none", "inherit", "initial", "unset" */
Note: Keyframe names are case-sensitive. fadeIn and fadein are treated as two different animations. Also avoid naming your keyframes with CSS-wide values like none, initial, inherit, or unset, as these will be interpreted as keywords rather than animation names.

from/to Syntax

The simplest way to define keyframes is using the from and to keywords. from represents the start of the animation (0%) and to represents the end (100%). This is ideal for simple two-state animations.

Using from/to Keywords

/* Simple fade in */
@keyframes fadeIn {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

/* Slide in from the left */
@keyframes slideInLeft {
    from {
        transform: translateX(-100%);
        opacity: 0;
    }
    to {
        transform: translateX(0);
        opacity: 1;
    }
}

/* Rotate element */
@keyframes rotate360 {
    from {
        transform: rotate(0deg);
    }
    to {
        transform: rotate(360deg);
    }
}

Percentage-Based Keyframes

For animations with more than two states, you use percentages to define keyframes at any point during the animation timeline. Percentages give you fine-grained control over what happens at each stage. You can define as many keyframe steps as you need, from 0% to 100%.

Multi-Step Animations with Percentages

/* Bounce effect with multiple stages */
@keyframes bounce {
    0% {
        transform: translateY(0);
    }
    25% {
        transform: translateY(-30px);
    }
    50% {
        transform: translateY(0);
    }
    75% {
        transform: translateY(-15px);
    }
    100% {
        transform: translateY(0);
    }
}

/* Color cycle through multiple colors */
@keyframes colorCycle {
    0% { background-color: #e74c3c; }
    25% { background-color: #f39c12; }
    50% { background-color: #2ecc71; }
    75% { background-color: #3498db; }
    100% { background-color: #e74c3c; }
}

/* Complex entrance animation */
@keyframes dramaticEntrance {
    0% {
        opacity: 0;
        transform: scale(0.3) rotate(-15deg);
    }
    50% {
        opacity: 0.8;
        transform: scale(1.05) rotate(3deg);
    }
    70% {
        transform: scale(0.95) rotate(-1deg);
    }
    100% {
        opacity: 1;
        transform: scale(1) rotate(0deg);
    }
}

You can also combine multiple percentage selectors that share the same styles, using a comma-separated list:

Combining Keyframe Selectors

/* Blink animation: visible at start, middle, and end */
@keyframes blink {
    0%, 50%, 100% {
        opacity: 1;
    }
    25%, 75% {
        opacity: 0;
    }
}

/* Hold a state for a portion of the animation */
@keyframes fadeInAndHold {
    0% {
        opacity: 0;
    }
    30% {
        opacity: 1;
    }
    /* Hold at full opacity from 30% to 100% */
    100% {
        opacity: 1;
    }
}

Animation Properties in Detail

Once you have defined a @keyframes rule, you apply it to an element using animation properties. There are eight individual properties that control every aspect of how the animation behaves.

animation-name

The animation-name property specifies which @keyframes rule to apply. It must match the name you used in your @keyframes declaration exactly, including case. Set it to none to remove an animation.

Applying an Animation by Name

.element {
    animation-name: fadeIn;
}

/* Remove an animation */
.element.paused {
    animation-name: none;
}

animation-duration

The animation-duration property sets how long one cycle of the animation takes to complete. It accepts time values in seconds (s) or milliseconds (ms). The default value is 0s, which means no animation is visible -- you must always set a duration for any animation to play.

Setting Animation Duration

.fast-animation {
    animation-name: fadeIn;
    animation-duration: 0.3s;
}

.normal-animation {
    animation-name: fadeIn;
    animation-duration: 1s;
}

.slow-animation {
    animation-name: fadeIn;
    animation-duration: 3s;
}

/* Milliseconds work too */
.precise-animation {
    animation-name: fadeIn;
    animation-duration: 750ms;
}

animation-timing-function

The animation-timing-function controls the acceleration curve of the animation. It determines how the animation progresses over time -- whether it starts slow and speeds up, maintains a constant speed, or follows a custom curve. This is one of the most important properties for making animations feel natural.

Timing Functions for Animations

/* Built-in keyword values */
.linear { animation-timing-function: linear; }
/* Constant speed from start to finish */

.ease { animation-timing-function: ease; }
/* Slow start, fast middle, slow end (default) */

.ease-in { animation-timing-function: ease-in; }
/* Slow start, accelerates toward end */

.ease-out { animation-timing-function: ease-out; }
/* Fast start, decelerates toward end */

.ease-in-out { animation-timing-function: ease-in-out; }
/* Slow start and slow end */

/* Custom cubic-bezier curves */
.custom-bounce {
    animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
}

/* Steps for frame-by-frame animation */
.sprite-animation {
    animation-timing-function: steps(8);
    /* Jumps through 8 discrete steps */
}

.typewriter {
    animation-timing-function: steps(20, end);
    /* 20 steps, jump at the end of each step */
}
Tip: You can set different timing functions for individual keyframe segments by placing animation-timing-function inside a keyframe block. The timing function declared at a keyframe controls the acceleration from that keyframe to the next one, giving you fine-grained control over each stage of the animation.

Per-Keyframe Timing Functions

@keyframes customBounce {
    0% {
        transform: translateY(0);
        animation-timing-function: ease-out;
        /* Ease out from 0% to 40% */
    }
    40% {
        transform: translateY(-150px);
        animation-timing-function: ease-in;
        /* Ease in from 40% to 60% */
    }
    60% {
        transform: translateY(0);
        animation-timing-function: ease-out;
        /* Ease out from 60% to 80% */
    }
    80% {
        transform: translateY(-40px);
        animation-timing-function: ease-in;
    }
    100% {
        transform: translateY(0);
    }
}

animation-delay

The animation-delay property specifies how long to wait before the animation starts. It accepts time values in seconds or milliseconds. A positive value delays the start, while a negative value makes the animation start immediately but partway through its cycle, as if it had already been running for that amount of time.

Animation Delay: Positive and Negative

/* Wait 0.5 seconds before starting */
.delayed {
    animation-name: fadeIn;
    animation-duration: 1s;
    animation-delay: 0.5s;
}

/* Negative delay: start immediately, but 500ms into the animation */
.already-running {
    animation-name: rotate360;
    animation-duration: 2s;
    animation-delay: -0.5s;
    /* Animation starts at the 25% mark immediately */
}

/* Stagger multiple elements with increasing delays */
.item:nth-child(1) { animation-delay: 0s; }
.item:nth-child(2) { animation-delay: 0.1s; }
.item:nth-child(3) { animation-delay: 0.2s; }
.item:nth-child(4) { animation-delay: 0.3s; }
.item:nth-child(5) { animation-delay: 0.4s; }
Tip: Negative delays are extremely useful for creating staggered animations where you want elements to appear already in motion. For example, if you have multiple spinner elements, you can give each a different negative delay so they start at different positions in the rotation cycle, creating a more natural distributed effect.

animation-iteration-count

The animation-iteration-count property determines how many times the animation plays. It accepts any positive number (including decimals) or the keyword infinite for continuous looping.

Controlling Iteration Count

/* Play once (default) */
.once { animation-iteration-count: 1; }

/* Play three times */
.thrice { animation-iteration-count: 3; }

/* Play half a cycle (stops at 50%) */
.half { animation-iteration-count: 0.5; }

/* Loop forever */
.forever { animation-iteration-count: infinite; }

/* Practical: loading spinner runs infinitely */
.spinner {
    animation-name: rotate360;
    animation-duration: 1s;
    animation-timing-function: linear;
    animation-iteration-count: infinite;
}

animation-direction

The animation-direction property controls whether the animation plays forward, backward, or alternates between the two on successive cycles. This is especially powerful combined with multiple iterations.

Animation Direction Values

/* normal: plays 0% to 100% on every cycle (default) */
.forward { animation-direction: normal; }

/* reverse: plays 100% to 0% on every cycle */
.backward { animation-direction: reverse; }

/* alternate: odd cycles go forward, even cycles go backward */
.ping-pong {
    animation-direction: alternate;
    animation-iteration-count: infinite;
    /* 1st: 0%→100%, 2nd: 100%→0%, 3rd: 0%→100%, ... */
}

/* alternate-reverse: odd cycles backward, even cycles forward */
.reverse-ping-pong {
    animation-direction: alternate-reverse;
    animation-iteration-count: infinite;
    /* 1st: 100%→0%, 2nd: 0%→100%, 3rd: 100%→0%, ... */
}

/* Practical: pulsing element */
@keyframes pulse {
    from { transform: scale(1); }
    to { transform: scale(1.1); }
}

.pulse {
    animation-name: pulse;
    animation-duration: 0.8s;
    animation-direction: alternate;
    animation-iteration-count: infinite;
    animation-timing-function: ease-in-out;
}

animation-fill-mode

The animation-fill-mode property determines what styles apply to the element before the animation starts (during a delay) and after it ends. By default, an animated element reverts to its original styles once the animation finishes. The fill mode lets you override this behavior.

Understanding animation-fill-mode

/* none (default): element returns to its original styles before/after */
.no-fill {
    animation-fill-mode: none;
    opacity: 1; /* original */
    animation-name: fadeIn; /* animates from opacity:0 to opacity:1 */
    /* Before delay: opacity is 1 (original) */
    /* After animation: opacity is 1 (original) */
}

/* forwards: element retains the styles from the LAST keyframe */
.keep-end-state {
    animation-fill-mode: forwards;
    opacity: 1;
    animation-name: fadeOut; /* animates from opacity:1 to opacity:0 */
    /* After animation: opacity stays at 0 (last keyframe) */
}

/* backwards: element applies styles from the FIRST keyframe during delay */
.apply-start-early {
    animation-fill-mode: backwards;
    opacity: 1;
    animation-name: fadeIn; /* starts at opacity:0 */
    animation-delay: 2s;
    /* During the 2s delay: opacity is 0 (first keyframe) */
    /* After animation: opacity is 1 (original) */
}

/* both: combines forwards and backwards behavior */
.fill-both {
    animation-fill-mode: both;
    opacity: 1;
    animation-name: fadeIn;
    animation-delay: 2s;
    /* During delay: opacity is 0 (first keyframe - backwards) */
    /* After animation: opacity is 1 (last keyframe - forwards) */
}
Warning: animation-fill-mode: forwards keeps the final keyframe styles applied indefinitely. This can cause confusion during debugging because the element's computed styles will show the keyframe values rather than the original CSS. If you find an element is not responding to style changes as expected, check whether a fill mode is overriding your styles.

animation-play-state

The animation-play-state property lets you pause and resume an animation. It accepts two values: running (default) and paused. This is often toggled via JavaScript or CSS pseudo-classes like :hover.

Pausing and Resuming Animations

/* Pause animation on hover */
.animated-element {
    animation: rotate360 2s linear infinite;
}

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

/* Pause when a parent has a specific class */
.container.frozen .animated-element {
    animation-play-state: paused;
}

/* Useful for reduced-motion preferences */
@media (prefers-reduced-motion: reduce) {
    * {
        animation-play-state: paused !important;
    }
}

The animation Shorthand

The animation shorthand property lets you combine all animation properties into a single declaration. The syntax is flexible -- values are identified by type rather than position, with one exception: the first time value is always animation-duration and the second is animation-delay.

Animation Shorthand Syntax

/* Syntax:
   animation: name duration timing-function delay iteration-count
              direction fill-mode play-state; */

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

/* Equivalent shorthand */
.element {
    animation: fadeIn 1s ease-out 0.5s 1 normal forwards running;
}

/* You can omit values that use defaults */
.element {
    animation: fadeIn 1s ease-out 0.5s forwards;
}

/* Minimum needed: name and duration */
.element {
    animation: fadeIn 1s;
}

/* With infinite looping */
.spinner {
    animation: rotate360 1s linear infinite;
}

/* With alternate direction */
.pulse {
    animation: pulse 0.8s ease-in-out infinite alternate;
}

Multiple Animations

You can apply multiple animations to a single element by separating them with commas. Each animation runs independently and can have its own duration, delay, timing function, and other settings. This is incredibly powerful for composing complex effects from simpler building blocks.

Applying Multiple Animations

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

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

@keyframes colorShift {
    from { color: #333; }
    to { color: #0066cc; }
}

/* Apply all three simultaneously */
.element {
    animation:
        slideIn 0.6s ease-out forwards,
        fadeIn 0.4s ease-out forwards,
        colorShift 1s ease-in-out 0.6s forwards;
}

/* Each animation is independent:
   - slideIn: 0.6s with ease-out
   - fadeIn: 0.4s with ease-out
   - colorShift: starts after 0.6s delay, lasts 1s */

Chaining Animations with Delays

You can create sequential animation chains by using delays that match the duration of previous animations. This lets you build step-by-step animation sequences without JavaScript.

Chaining Animations Sequentially

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

@keyframes expand {
    from { width: 0; }
    to { width: 100%; }
}

@keyframes revealContent {
    from { opacity: 0; max-height: 0; }
    to { opacity: 1; max-height: 500px; }
}

/* Chain: slideDown finishes, then expand starts, then revealContent */
.card-header {
    animation: slideDown 0.5s ease-out both;
}

.card-divider {
    animation: expand 0.3s ease-out 0.5s both;
    /* Starts after slideDown finishes (0.5s delay) */
}

.card-body {
    animation: revealContent 0.4s ease-out 0.8s both;
    /* Starts after expand finishes (0.5s + 0.3s = 0.8s delay) */
}

Animation Events in JavaScript

CSS animations fire three JavaScript events that let you respond to animation milestones. These events are essential for coordinating animations with application logic, triggering follow-up actions, or cleaning up after animations complete.

Listening for Animation Events

const element = document.querySelector('.animated');

/* Fires when the animation starts (after any delay) */
element.addEventListener('animationstart', (e) => {
    console.log('Animation started:', e.animationName);
    console.log('Elapsed time:', e.elapsedTime);
});

/* Fires at the start of each new iteration (not the first) */
element.addEventListener('animationiteration', (e) => {
    console.log('New iteration of:', e.animationName);
});

/* Fires when the animation completes */
element.addEventListener('animationend', (e) => {
    console.log('Animation ended:', e.animationName);
    /* Common pattern: remove the animation class */
    element.classList.remove('animate-in');
    /* Or trigger a follow-up action */
    showNextElement();
});

/* Fires if the animation is cancelled (e.g., element removed) */
element.addEventListener('animationcancel', (e) => {
    console.log('Animation cancelled:', e.animationName);
});
Note: The animationiteration event does not fire on the first cycle -- only on subsequent iterations. If your animation has animation-iteration-count: 1, this event will never fire. Also, if an animation is removed or the element is hidden before completion, the animationcancel event fires instead of animationend.

Practical Animation Examples

Let us build several real-world animations that you will use frequently in web development. Each example demonstrates different techniques and combinations of animation properties.

Loading Spinner

CSS-Only Loading Spinner

/* HTML: <div class="spinner"></div> */

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

.spinner {
    width: 40px;
    height: 40px;
    border: 4px solid #e0e0e0;
    border-top-color: #3498db;
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}

/* Variant: dual-ring spinner */
@keyframes dualSpin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

.dual-spinner {
    width: 48px;
    height: 48px;
    border: 5px solid transparent;
    border-top-color: #3498db;
    border-bottom-color: #3498db;
    border-radius: 50%;
    animation: dualSpin 1s linear infinite;
}

/* Three-dot loading indicator */
@keyframes dotPulse {
    0%, 80%, 100% {
        transform: scale(0);
        opacity: 0.5;
    }
    40% {
        transform: scale(1);
        opacity: 1;
    }
}

.dot-loader {
    display: flex;
    gap: 8px;
}

.dot-loader span {
    width: 12px;
    height: 12px;
    background: #3498db;
    border-radius: 50%;
    animation: dotPulse 1.4s ease-in-out infinite;
}

.dot-loader span:nth-child(2) {
    animation-delay: 0.16s;
}

.dot-loader span:nth-child(3) {
    animation-delay: 0.32s;
}

Pulse Effect

Pulsing Notification Badge

@keyframes pulse {
    0% {
        transform: scale(1);
        box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.7);
    }
    50% {
        transform: scale(1.05);
        box-shadow: 0 0 0 10px rgba(231, 76, 60, 0);
    }
    100% {
        transform: scale(1);
        box-shadow: 0 0 0 0 rgba(231, 76, 60, 0);
    }
}

.notification-badge {
    width: 24px;
    height: 24px;
    background: #e74c3c;
    border-radius: 50%;
    color: white;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 12px;
    animation: pulse 2s ease-in-out infinite;
}

/* Subtle pulse for call-to-action buttons */
@keyframes subtlePulse {
    0%, 100% {
        box-shadow: 0 0 0 0 rgba(52, 152, 219, 0.4);
    }
    50% {
        box-shadow: 0 0 0 15px rgba(52, 152, 219, 0);
    }
}

.cta-button {
    padding: 12px 32px;
    background: #3498db;
    color: white;
    border: none;
    border-radius: 6px;
    animation: subtlePulse 2.5s ease-in-out infinite;
}

.cta-button:hover {
    animation-play-state: paused;
}

Typewriter Effect

CSS Typewriter Animation

@keyframes typing {
    from { width: 0; }
    to { width: 100%; }
}

@keyframes blinkCaret {
    0%, 100% { border-right-color: transparent; }
    50% { border-right-color: #333; }
}

.typewriter {
    display: inline-block;
    overflow: hidden;
    white-space: nowrap;
    border-right: 3px solid #333;
    font-family: monospace;
    font-size: 1.25rem;
    /* Steps count should match character count of the text */
    animation:
        typing 3s steps(30) 1s forwards,
        blinkCaret 0.75s step-end infinite;
    width: 0;
}

/* The text content determines the step count:
   "Welcome to my portfolio site." = 30 characters = steps(30) */

Bounce Entrance

Bounce-In Animation

@keyframes bounceIn {
    0% {
        opacity: 0;
        transform: scale(0.3);
    }
    20% {
        transform: scale(1.1);
    }
    40% {
        transform: scale(0.9);
    }
    60% {
        opacity: 1;
        transform: scale(1.03);
    }
    80% {
        transform: scale(0.97);
    }
    100% {
        opacity: 1;
        transform: scale(1);
    }
}

.bounce-in {
    animation: bounceIn 0.8s cubic-bezier(0.215, 0.610, 0.355, 1) both;
}

/* Staggered bounce-in for a list of items */
.list-item {
    opacity: 0;
    animation: bounceIn 0.6s ease-out both;
}

.list-item:nth-child(1) { animation-delay: 0s; }
.list-item:nth-child(2) { animation-delay: 0.1s; }
.list-item:nth-child(3) { animation-delay: 0.2s; }
.list-item:nth-child(4) { animation-delay: 0.3s; }
.list-item:nth-child(5) { animation-delay: 0.4s; }

Fade-In Variations

Multiple Fade-In Animations

/* Fade in from bottom */
@keyframes fadeInUp {
    from {
        opacity: 0;
        transform: translateY(30px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* Fade in from top */
@keyframes fadeInDown {
    from {
        opacity: 0;
        transform: translateY(-30px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* Fade in with scale */
@keyframes fadeInScale {
    from {
        opacity: 0;
        transform: scale(0.8);
    }
    to {
        opacity: 1;
        transform: scale(1);
    }
}

/* Fade in with blur */
@keyframes fadeInBlur {
    from {
        opacity: 0;
        filter: blur(10px);
    }
    to {
        opacity: 1;
        filter: blur(0);
    }
}

/* Apply these as utility classes */
.fade-in-up { animation: fadeInUp 0.6s ease-out both; }
.fade-in-down { animation: fadeInDown 0.6s ease-out both; }
.fade-in-scale { animation: fadeInScale 0.5s ease-out both; }
.fade-in-blur { animation: fadeInBlur 0.7s ease-out both; }

Skeleton Loading Placeholder

Shimmer Effect for Loading Skeletons

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

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

.skeleton-title {
    width: 60%;
    height: 24px;
    margin-bottom: 12px;
}

.skeleton-text {
    width: 100%;
    height: 16px;
    margin-bottom: 8px;
}

.skeleton-text:last-child {
    width: 80%;
}

Accessibility and Reduced Motion

Animations can cause discomfort, nausea, or seizures for people with vestibular disorders, motion sensitivity, or photosensitive epilepsy. Modern CSS provides the prefers-reduced-motion media query to detect when a user has requested reduced motion in their operating system settings. You should always respect this preference.

Respecting prefers-reduced-motion

/* Approach 1: Remove all animations for reduced-motion users */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* Approach 2: Provide alternative subtle animations */
.hero-title {
    animation: bounceIn 0.8s ease-out both;
}

@media (prefers-reduced-motion: reduce) {
    .hero-title {
        animation: fadeIn 0.3s ease-out both;
        /* Replace bouncy entrance with simple fade */
    }
}

/* Approach 3: Progressive enhancement -- add animations only
   when no motion preference is set */
.card {
    opacity: 1; /* Default: no animation */
}

@media (prefers-reduced-motion: no-preference) {
    .card {
        animation: fadeInUp 0.5s ease-out both;
    }
}
Warning: Never rely solely on animations to convey important information. Animations should enhance the user experience, not be the only way to understand the interface. A loading spinner should be accompanied by text like "Loading..." for screen readers, and animated notifications should also update ARIA live regions.

Exercise 1: Animated Notification System

Build a notification component with CSS keyframe animations. Create a notification card that slides in from the right side of the screen using a slideInRight animation lasting 0.4 seconds with an ease-out timing function. Use animation-fill-mode: both so the element stays in place after animating. Add a progress bar at the bottom of the notification that shrinks from 100% width to 0% over 5 seconds using a linear timing function (this visually counts down until the notification auto-dismisses). After the 5-second countdown, add a slideOutRight animation with a 5-second delay to make the notification slide back out. Create three different notification types (success, warning, error) each with a distinct pulse color on the left border. If multiple notifications appear, stagger them vertically with increasing animation-delay values. Finally, add a prefers-reduced-motion query that replaces slide animations with simple fade animations.

Exercise 2: Animated Landing Page Hero

Create a landing page hero section with a coordinated animation sequence. Start by fading in the background image or gradient over 1.5 seconds. After the background settles, animate the main headline using a fadeInDown animation with a 0.5-second delay. Then reveal the subtitle using fadeInUp with a 1-second delay. Next, animate a call-to-action button with a bounceIn effect with a 1.5-second delay. Once all elements have appeared, add a subtle continuous float animation to a decorative element (like an arrow or icon) that moves it up and down 10 pixels using alternate direction and infinite iterations. Add a second decorative element with the same float animation but a negative delay of -1 second so the two elements are out of phase. Use animation-fill-mode: both on all entrance animations so elements are hidden during their delay period. Ensure the entire sequence is disabled or simplified when prefers-reduced-motion: reduce is active.