Practical Animation Patterns
Lessons 01–03 covered the mechanics. This lesson is about patterns — animation recipes that appear constantly in real interfaces. Each one is self-contained: a @keyframes rule and the animation declaration that drives it. None of them depend on the STO project. The goal is to build vocabulary you can reach for immediately when you recognize the problem.
The four patterns are:
- Loading spinner — continuous rotation while content loads
- Pulse badge — repeating scale-and-opacity loop to draw attention
- Skeleton shimmer — sweeping highlight that signals loading content
- Slide-in notification — entrance from off-screen with exit behavior
Pattern 1 — Loading spinner
Section titled “Pattern 1 — Loading spinner”A spinner communicates that something is loading. The visual is a partial circle (an element with a thick border where one side is transparent) that rotates indefinitely.
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); }}
.spinner { width: 32px; height: 32px; border: 3px solid #e5e7eb; border-top-color: #2563eb; border-radius: 50%; animation: spin 700ms linear infinite;}Key decisions:
lineartiming — rotational motion should be constant speed.easewould create a subtle acceleration at the start of each revolution, which reads as erratic.infiniteiteration count — the spinner runs until the element is removed from the DOM.- No
fill-modeneeded — an infinitely looping animation never enters a “before” or “after” state.
The HTML is a single <div class="spinner"></div>. No content inside it — the element itself is the graphic.
Pattern 2 — Pulse badge
Section titled “Pattern 2 — Pulse badge”A pulsing element (a notification count, a status dot, a “live” indicator) draws attention without requiring interaction. The motion should be subtle — a gentle scale and opacity rhythm, not a startling jump.
@keyframes pulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.15); opacity: 0.75; }}
.badge-live { animation: pulse 1.8s ease-in-out infinite;}The 0% and 100% keyframes match exactly, so the loop is seamless — no jump between the end of one cycle and the start of the next. ease-in-out creates the soft breathing feel: the badge swells slowly, peaks, and deflates slowly.
1.8s is intentionally slower than most UI animations. Fast pulses feel urgent and anxious; slow pulses feel calm and informational. Match the speed to the urgency of the content.
Pattern 3 — Skeleton shimmer
Section titled “Pattern 3 — Skeleton shimmer”Skeleton screens replace content while it loads — placeholder shapes that match the approximate layout of the real content. A shimmer sweep across the skeleton communicates that loading is actively in progress, not stalled.
The technique uses a moving gradient background:
@keyframes shimmer { from { background-position: -200% center; } to { background-position: 200% center; }}
.skeleton { background: linear-gradient( 90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75% ); background-size: 200% 100%; animation: shimmer 1.5s linear infinite;}How it works:
- The
linear-gradientcreates a highlight band — darker grey on the sides, lighter grey in the middle. background-size: 200% 100%makes the gradient twice as wide as the element, so only half of it is visible at any moment.- Animating
background-positionslides the gradient horizontally. Starting at-200%and ending at200%ensures the highlight passes fully across the element. lineartiming keeps the sweep at constant speed, which reads as mechanical and loading — appropriate here.
Apply .skeleton to any shape. Common HTML:
<div class="skeleton" style="height: 1rem; width: 60%; border-radius: 4px;"></div><div class="skeleton" style="height: 1rem; width: 40%; border-radius: 4px; margin-top: 8px;"></div>In real projects, skeleton dimensions are inlined or defined in component-specific classes to match the content they replace.
Pattern 4 — Slide-in notification
Section titled “Pattern 4 — Slide-in notification”Notifications, toasts, and banners typically enter from off-screen and either stay until dismissed or exit after a timeout. The entrance animation is straightforward; combining entrance and exit in pure CSS requires a little planning.
@keyframes slideInRight { from { transform: translateX(110%); opacity: 0; } to { transform: translateX(0); opacity: 1; }}
@keyframes slideOutRight { from { transform: translateX(0); opacity: 1; } to { transform: translateX(110%); opacity: 0; }}
.notification { animation: slideInRight 300ms ease-out both;}
.notification.is-dismissing { animation: slideOutRight 250ms ease-in both;}The entrance uses ease-out — it arrives quickly and decelerates into position. The exit uses ease-in — it starts slowly and accelerates off screen. This asymmetry matches physical intuition: things enter your field of view and slow to a stop; things leaving accelerate away.
110% instead of 100% accounts for the element having its own width — translating by exactly 100% of its own width would leave it flush with the edge rather than fully off-screen.
Dismissal is handled by toggling .is-dismissing with JavaScript. The animation-fill-mode: both on the exit keeps the element invisible at translateX(110%) after the animation completes, so it does not snap back to its original position before JavaScript removes it from the DOM.
Exercise
Section titled “Exercise”Create a file called animations-test.html in the same folder as your STO index.html. Give it a minimal HTML structure and link a fresh animations-test.css file. Implement the spinner and pulse patterns:
Spinner:
<div class="spinner"></div>@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); }}
.spinner { width: 40px; height: 40px; border: 4px solid #e5e7eb; border-top-color: #2563eb; border-radius: 50%; animation: spin 700ms linear infinite;}Pulse badge:
<span class="badge-live">● LIVE</span>@keyframes pulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.15); opacity: 0.75; }}
.badge-live { display: inline-block; animation: pulse 1.8s ease-in-out infinite;}Note display: inline-block on the badge — transform has no effect on inline elements. Any element you want to animate with transform must be a block, inline-block, flex, or grid item.
Open animations-test.html in the browser and confirm:
- The spinner rotates smoothly and continuously
- The badge pulses at a calm, steady rhythm
- There is no jump or stutter between pulse cycles
You do not need to implement the shimmer or slide-in for this exercise, but reading through the patterns until you understand each decision is the point.
- The loading spinner uses
transform: rotate()withlineartiming andinfiniteiteration count. A partial border on a circle element creates the visual. - The pulse badge uses matching 0% and 100% keyframes so the loop is seamless. Slow duration (
1.8s) reads as calm; fast reads as urgent. - The skeleton shimmer animates
background-positionon an oversized linear gradient.background-size: 200% 100%is the setup that makes the sweep work. - The slide-in notification uses separate
@keyframesfor entrance and exit, each with the opposite easing (ease-outin,ease-inout).animation-fill-mode: bothprevents the element snapping back after the exit animation ends. transformhas no effect ondisplay: inlineelements — applydisplay: inline-block(or block, flex, grid) before animating.
Lesson 05 returns to the STO project. You will wire up the hero entrance animations using slideUpFade and fadeIn — the two @keyframes rules you wrote in earlier lessons — and build the staggered sequence that makes the hero section load with intentional timing.