CSS animations bring interfaces to life without a single line of JavaScript. A button that subtly changes color on hover, a loading spinner that rotates endlessly, a notification that slides in from the edge of the screen. These interactions make the difference between a website that feels static and one that feels responsive and polished.
The animation capabilities built into CSS have grown substantially over the past few years. What used to require a JavaScript library can now be done with a few lines of CSS. And because CSS animations are handled by the browser's compositor thread, they perform better than JavaScript animations on most devices.
CSS Transitions vs Keyframe Animations
CSS offers two animation mechanisms, and understanding when to use each one saves you from writing unnecessary code.
Transitions animate a property change from one value to another. They require a trigger (usually a pseudo-class like :hover or a class change via JavaScript). Transitions are ideal for simple state changes.
`css
.button {
background-color: #2563eb;
transition: background-color 0.2s ease;
}
.button:hover {
background-color: #1d4ed8;
}
`
Keyframe animations define a sequence of states that play automatically or on trigger. They can loop, alternate direction, and control intermediate steps. Use keyframes for anything more complex than a two-state transition.
`css
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
animation: fadeIn 0.3s ease-out;
}
`
Rule of thumb: if you are going from state A to state B, use a transition. If you need state A to B to C, or if the animation needs to loop, use keyframes.

Building a Loading Spinner with Keyframes
Loading spinners are one of the most common CSS animations. Here is a simple one that rotates a circle with a visible gap:
`css
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #e5e7eb;
border-top-color: #2563eb;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
`
The linear timing function makes the rotation speed constant. Using ease would make it accelerate and decelerate, which looks odd for a spinner. The infinite keyword makes it loop forever.
The Loading Spinner Generator creates customized spinners with different styles (dots, bars, circles, pulse effects) and lets you adjust size, color, and speed. Generate the CSS, copy it into your project, and you have a professional-looking loader without writing the animation from scratch.
For more complex loading indicators, you can chain multiple animations. A pulsing dot loader uses staggered animation delays:
`css
@keyframes pulse {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
.dot { animation: pulse 1.4s infinite ease-in-out both; }
.dot:nth-child(1) { animation-delay: -0.32s; }
.dot:nth-child(2) { animation-delay: -0.16s; }
`
The Animation Delay Calculator helps you compute evenly spaced delay values when you have multiple elements that need to animate in sequence.
Loading spinners are one of the most common CSS animations.
Timing Functions: The Secret to Natural-Looking Motion
The timing function (also called easing) controls how an animation progresses between its start and end states. This single property makes the difference between animation that feels mechanical and animation that feels natural.
ease (default): starts slow, accelerates, then slows down. Good for most general animations.
ease-in: starts slow, accelerates to the end. Use for elements leaving the screen (they speed up as they exit).
ease-out: starts fast, decelerates to the end. Use for elements entering the screen (they slow down as they arrive, like a real object coming to rest).
ease-in-out: slow start and slow end. Good for animations that draw attention (pulsing notifications, loading states).
linear: constant speed. Only use for continuous rotations (spinners) and progress indicators. Linear motion on most other animations looks robotic.
cubic-bezier(): custom curves. This is where experienced animators spend their time. A cubic bezier gives you full control over the acceleration curve. cubic-bezier(0.68, -0.55, 0.27, 1.55) creates a bouncy overshoot effect.
Most animations look better with ease-out on entrance and ease-in on exit. This mimics how physical objects behave: they decelerate when arriving at a position and accelerate when leaving.

Hover Effects That Feel Professional
Hover effects are the most visible animation on most websites. Getting them right takes a light touch. The best hover effects are subtle enough that users feel them rather than notice them.
Button lift:
`css
.button {
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
`
Card elevation:
`css
.card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
}
`
Link underline reveal:
`css
.link {
position: relative;
}
.link::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 2px;
background: currentColor;
transition: width 0.2s ease-out;
}
.link:hover::after {
width: 100%;
}
`
Notice that all these effects use short durations (0.15-0.2 seconds). Hover effects should feel instant. Anything longer than 0.3 seconds feels sluggish. Also note that the transition is defined on the base state, not the hover state. This ensures the animation plays both ways: on hover and on un-hover.
Hover effects are the most visible animation on most websites.
Performance: Animating the Right Properties
Not all CSS properties are equal when it comes to animation performance. The browser handles different properties at different stages of the rendering pipeline, and some stages are much cheaper than others.
Free (compositor only): transform and opacity. These properties are handled by the GPU compositor and do not trigger layout recalculation or repaint. Animate these whenever possible.
Expensive (triggers repaint): background-color, color, box-shadow, border. These require the browser to repaint the element but do not affect layout. Acceptable for small elements, problematic for large areas.
Very expensive (triggers layout): width, height, margin, padding, top, left, font-size. These force the browser to recalculate the layout of the element and potentially its siblings and parents. Avoid animating these properties.
Instead of animating width, use transform: scaleX(). Instead of animating top/left, use transform: translate(). Instead of animating height to reveal content, use max-height with a fixed value (still triggers layout but is more predictable).
For production, run your animation CSS through the CSS Minifier to reduce file size. Keyframe declarations with many steps can be verbose, and minification removes the whitespace without affecting behavior.
Respecting User Preferences
Some users experience motion sickness, seizures, or discomfort from animated content. The prefers-reduced-motion media query lets you respect their system preference:
`css
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
`
This effectively disables all animations for users who have enabled the "reduce motion" setting in their operating system. The 0.01ms duration (rather than 0s) ensures that animation end states are still applied.
A more nuanced approach is to remove decorative animations while keeping functional ones. A loading spinner communicates important status information and should remain (perhaps as a static progress bar instead). A parallax scrolling effect is purely decorative and should be disabled.
Accessibility is not optional. If your animation triggers the prefers-reduced-motion query and you have not handled it, you are creating an inaccessible experience for a portion of your users.
Some users experience motion sickness, seizures, or discomfort from animated content.
FAQ
Can CSS animations replace JavaScript animation libraries?
For most UI animations, yes. CSS handles transitions, keyframe sequences, and state-based animations well. JavaScript libraries like GSAP and Framer Motion are still valuable for complex coordinated animations (staggered lists, scroll-linked animations with precise control, physics-based motion), but the gap is closing. Start with CSS and reach for JavaScript only when CSS cannot express what you need.
How do I animate an element when it scrolls into view?
Use the Intersection Observer API in JavaScript to add a class when the element enters the viewport, then trigger the animation via that class. In 2026, CSS scroll-driven animations (via animation-timeline: view()) are supported in all major browsers, which lets you do this without JavaScript entirely.
Why does my animation look janky on mobile?
Most likely you are animating expensive properties (width, height, top, left) that trigger layout recalculation. Switch to transform and opacity. Also check that will-change is applied to the animated element to hint the browser to prepare for the animation.
What is a good default duration for animations?
For hover effects: 0.15 to 0.2 seconds. For element transitions (fade in, slide in): 0.2 to 0.3 seconds. For page transitions: 0.3 to 0.5 seconds. For loading animations: 0.8 to 1.5 seconds per cycle. Shorter is almost always better than longer. Users should feel the animation, not wait for it.
JSON Guide: Format, Validate, and Convert JSON Files
JSON guide for developers: syntax rules, common parse errors, formatting and schema validation, plus how to convert between JSON and CSV files.
Base64, URL Encoding & HTML Entities Explained
Encode and decode Base64, URLs, and HTML entities in your browser. Learn when to use each format, with clear examples and free converter tools.
Regular Expressions for Beginners: A Practical Guide
Learn regular expressions from scratch: basic syntax, character classes, quantifiers, and practical patterns for matching emails, URLs, and phone numbers.
