For years, any real animation on a web page meant JavaScript, a jQuery plugin, or a library like GSAP. Modern CSS can now do most of that on its own: state-change transitions, looping keyframe animations, and even effects that used to require JavaScript, like fading an element in as it enters the page, or as the user scrolls.
The goal today is not to memorize every property. It's to know what's possible, so you know what to ask an AI tool for, and so you can read and evaluate the CSS it hands back to you.
The simplest form of animation: tell the browser that when a property changes, like on hover, focus, or a class being added, it should animate smoothly between the old and new value instead of jumping instantly.
button {
background-color: #4466cc;
transition: background-color 0.3s ease, transform 0.3s ease;
}
button:hover {
background-color: #cc3333;
transform: scale(1.1);
}
Transitions only animate between two states. For anything that loops, or moves through several steps on its own (no hover needed), use @keyframes to define the steps, then the animation property to play them.
.circle {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.4); opacity: 0.6; }
}
transform is what actually moves, spins, resizes, or slants an element. It's usually combined with transition or animation, transform on its own just repositions the element instantly.
Hover each box
.demo-translate:hover { transform: translateY(-15px); } /* move */
.demo-rotate:hover { transform: rotate(20deg); } /* rotate */
.demo-scale:hover { transform: scale(1.3); } /* resize */
.demo-skew:hover { transform: skewX(-15deg); } /* slant */
Until recently, you could not animate an element appearing from display: none, the browser just snapped it into existence with no transition. Two newer CSS features fix that: @starting-style and transition-behavior: allow-discrete.
display: none. No JavaScript animation code, just CSS.
.card {
display: none;
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.35s ease, transform 0.35s ease,
display 0.35s allow-discrete;
}
.card.is-open {
display: block;
opacity: 1;
transform: translateY(0);
}
/* tells the browser what "before the animation" looks like,
since a display:none element has no prior state to animate from */
@starting-style {
.card.is-open {
opacity: 0;
transform: translateY(-10px);
}
}
Baseline newly available since August 2024. Works in current Chrome/Edge, Firefox, and Safari.
CSS @starting-style Rule @ w3schools
Newer still: animations that play based on scroll position instead of time, no JavaScript scroll listener required. animation-timeline: view() plays an animation as an element scrolls into and out of its scroll container.
Scroll inside the box below
.target {
animation: fadeIn linear;
animation-timeline: view(); /* tied to scroll position, not time */
animation-range: entry 0% cover 40%;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(40px); }
to { opacity: 1; transform: translateY(0); }
}
Support note: this one is not fully cross-browser yet. It works in Chrome, Edge, and Safari, but not yet in Firefox, so it should not be the only way you build something essential. If you don't see the demo above animate as you scroll, that's why.
Animation isn't free for every visitor. Some people get real physical discomfort, dizziness or nausea, from motion on screen, and most operating systems let them turn on a system-wide "reduce motion" setting. CSS can detect that setting and respect it.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}
Any page with real animation, especially anything that loops or moves automatically like the pulsing circle earlier in this slide, should include a rule like this.
CSS Accessibility (prefers-reduced-motion) @ w3schools