CSS Tutorial
CSS Animations
@keyframes plus animation-name run a sequence. Prefer transform and opacity for smoothness.
A sequence, not a one-shot
A transition needs a change — hover, focus, a class added in JavaScript. An animation does not. You name a sequence with @keyframes, attach it with animation-name, and the browser plays that sequence on its own: once, a few times, or forever.
Harbor studio might pulse a “desks free” badge, or fade a banner in when the page loads. Keep that kind of motion small. Animation that never stops fights reading. Prefer a short intro, then stillness.
@keyframes is the storyboard
Inside @keyframes you set stops. from and to are 0% and 100%. You can add extra percentages if the motion has a middle pose. The animation name is yours — a word with no spaces.
Example
<style>
body {
font-family: system-ui, sans-serif;
color: #0c4a6e;
background: #f0f9ff;
margin: 1.25rem;
}
@keyframes rise-in {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
max-width: 18rem;
padding: 1rem 1.2rem;
background: #fff;
border: 1px solid #7dd3fc;
border-radius: 0.75rem;
animation-name: rise-in;
animation-duration: 600ms;
animation-timing-function: ease-out;
animation-fill-mode: both;
}
h1 { margin: 0 0 0.4rem; font-size: 1.15rem; color: #0d9488; }
p { margin: 0; color: #475569; }
</style>
<article class="card">
<h1>Harbor studio</h1>
<p>The card fades and lifts into place. Reload the preview to play it again.</p>
</article>animation-fill-mode: both applies the 0% styles before the animation starts and keeps the 100% styles after it ends. Without it, the card can flash at full opacity, then jump back to the keyframe start.
Open this at /css/try and change translateY(12px) to 8px. The motion should stay on transform and opacity — those two paint cheaply. Animatingtop or height looks similar and costs layout every frame.
The animation properties
Name and duration are the minimum. Iteration, direction, delay, and fill-mode finish the control. There is a shorthand, but the longhands are easier to read while you learn.
| Property | Job |
|---|---|
animation-name | Which @keyframes block to play |
animation-duration | Length of one cycle |
animation-timing-function | Easing inside each cycle |
animation-iteration-count | 1, a number, or infinite |
animation-direction | normal, reverse, alternate |
animation-delay | Wait before the first cycle |
animation-fill-mode | Keep start or end keyframes outside the run |
Example
<style>
body {
font-family: system-ui, sans-serif;
color: #0c4a6e;
background: #f0f9ff;
margin: 1.25rem;
}
@keyframes pulse-ring {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.08); opacity: 0.7; }
}
.badge {
display: inline-block;
padding: 0.35rem 0.75rem;
border-radius: 999px;
background: #ccfbf1;
color: #0f766e;
font-weight: 700;
font-size: 0.85rem;
animation: pulse-ring 1.6s ease-in-out infinite;
}
</style>
<h1>Harbor studio</h1>
<p>Three desks free this hour.</p>
<span class="badge">Open 8–16</span>The shorthand animation: pulse-ring 1.6s ease-in-out infinite packs name, duration, easing, and iteration. Percentage stops at 0%, 50%, and 100% make a breathe-in, breathe-out loop. Pause it in your head before you ship: does a badge that never rests help, or distract?
Prefer transform and opacity
The compositor can often animate transform and opacity without recalculating text wrapping or neighbors. That is why a fade and a slide feel smoother than growing widthor shifting margin-left. Start every animation with those two properties. Reach for layout properties only when the motion cannot be faked with a transform.
Example
<style>
body {
font-family: system-ui, sans-serif;
color: #0c4a6e;
background: #f0f9ff;
margin: 1.25rem;
}
@keyframes slide-label {
from { transform: translateX(-0.75rem); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.hours {
display: inline-block;
padding: 0.5rem 0.85rem;
background: #0ea5e9;
color: #fff;
border-radius: 0.4rem;
animation: slide-label 500ms ease-out both;
}
</style>
<p class="hours">Print shop · Harbor Lane 12</p>Infinite animation plus a large blur or a changing box-shadow can make a laptop fan spin. Keep loops tiny. And honor prefers-reduced-motion: reduce — set animation: none so the final keyframe is just the resting style.
Transition or animation?
If the motion is a reaction — hover, focus, a class toggle — use a transition. If the motion is a timeline that should play by itself, use @keyframes. You can combine them: a card animates in on load, then transitions on hover. Two tools, two jobs.
Worked examples
The short listings above show one property. These sheets paint documents you would actually ship: a lab dashboard, a clinic form, a marks table, a nav bar.
CSS does not invent meaning. HTML already said what the pieces are. Cascade, specificity, and the box model decide how they look. The numbers are classroom values — current, pH, pulse, 3-4-5 — so the style has something real to sit on.
Preview them in the CSS editor at /css/try. Change one property and watch the page paint. The tags stay the same.
Engineering
A live pulse on the logger
@keyframes names a sequence. animation-name plus duration plus iteration runs it. alternate with infinite is a pulse. Prefer opacity and transform.
Respect prefers-reduced-motion in real work: turn the pulse off when the user asked for less motion. This demo is the grammar only.
Example
<style>
@keyframes pulse {
from { opacity: 0.45; }
to { opacity: 1; }
}
.live { animation: pulse 1.2s ease-in-out infinite alternate; color: #0d9488; }
</style>
<p class="live">● logging</p>Astronomy
A slow fade on a title
A one-shot fade uses iteration-count: 1 and forwards so the last keyframe sticks. Infinite on a heading is a headache.
Example
<style>
@keyframes in {
from { opacity: 0; }
to { opacity: 1; }
}
h1 { animation: in 0.8s ease forwards; color: #5b21b6; }
</style>
<h1>Mars</h1>