CSS Transitions, Keyframes, and Units: Motion That Stays Maintainable
Choose transitions vs @keyframes, pick units that scale, and build reusable motion snippets — with performance and accessibility rules that keep UI snappy.
Build practical gradient and shadow design tokens, pick easing curves that feel right, and avoid paint-heavy effects that tank scroll performance.
By codefunc
Gradients set atmosphere. Shadows create depth. Easing decides whether motion feels premium or cheap. Together they define a product's visual language — and they are easy to overdo. A dozen layered box-shadows on a scrolling list will cost you frames. This guide focuses on tokenizable values you can reuse, plus performance and motion rules that keep interfaces crisp.
Bottom line: Define a short scale of gradients, elevations, and easings as CSS variables. Prefer fewer, softer shadows; use transform and opacity for animation; tune curves with a Cubic Bezier Generator. Build previews in a CSS Gradient Generator and Box Shadow Generator before pasting into the design system.
Stop inventing a new gradient per component. Ship a small set:
:root {
--gradient-brand: linear-gradient(135deg, #0f766e 0%, #155e75 100%);
--gradient-subtle: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%);
--gradient-overlay: linear-gradient(
to top,
rgb(15 23 42 / 0.72) 0%,
rgb(15 23 42 / 0) 55%
);
}
| Token role | Typical use |
|---|---|
| Brand | CTAs, hero washes, empty states |
| Subtle | Page backgrounds, section dividers |
| Overlay | Text on photography |
| Mesh / multi-stop | Marketing only — keep out of dense UI |
Prototype stops and angles in a CSS Gradient Generator, then freeze the output into variables.
| Type | When |
|---|---|
linear-gradient |
Most UI — headers, buttons, fades |
radial-gradient |
Spotlights, soft glows behind cards |
conic-gradient |
Pie charts, angular sweeps — rare in product UI |
opacity of layered elements..hero {
background-image: var(--gradient-brand);
}
/* Prefer fading a layer over animating color stops */
.hero-glow {
opacity: 0.35;
transition: opacity 200ms var(--ease-out);
}
.hero:hover .hero-glow {
opacity: 0.55;
}
Material-style elevation works when the scale is short and consistent:
:root {
--shadow-sm: 0 1px 2px rgb(15 23 42 / 0.06);
--shadow-md:
0 1px 2px rgb(15 23 42 / 0.06),
0 4px 12px rgb(15 23 42 / 0.08);
--shadow-lg:
0 4px 8px rgb(15 23 42 / 0.06),
0 16px 32px rgb(15 23 42 / 0.12);
}
Tune offsets and blur in a Box Shadow Generator with a live preview, then commit the token — not twenty one-off values.
| Do | Don't |
|---|---|
| 2–4 elevation levels | Unique shadow per card variant |
| Soft, low-opacity darks | Harsh black 0 0 20px #000 |
Pair shadow with slight border (1px solid rgb(0 0 0 / 0.06)) |
Rely on shadow alone for edge definition in light mode |
| Darker UI: use lighter / tinted glows sparingly | Copy light-mode shadows onto near-black surfaces |
box-shadow is not free:
box-shadow → often triggers paint every frametranslateY) + a pre-defined shadow token swap, or animate opacity on a pseudo-element that carries the shadow.card {
box-shadow: var(--shadow-sm);
transition:
transform 200ms var(--ease-out),
box-shadow 200ms var(--ease-out);
}
.card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
For long virtualized lists, keep resting state at --shadow-sm or use borders only; reserve --shadow-lg for modals and popovers.
filter: drop-shadow() follows alpha shapes (good for irregular SVGs) but can be costlier than box-shadow on boxes — prefer box-shadow for rectangles.
Linear motion feels mechanical. Real interfaces use asymmetric curves:
| Name | CSS | Feel |
|---|---|---|
| Ease-out | cubic-bezier(0.16, 1, 0.3, 1) |
Snappy entrances |
| Ease-in-out | cubic-bezier(0.45, 0, 0.55, 1) |
Balanced moves |
| Emphasized | cubic-bezier(0.2, 0, 0, 1) |
Modern "standard" deceleration |
Design and export curves with a Cubic Bezier Generator:
:root {
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out: cubic-bezier(0.45, 0, 0.55, 1);
--ease-emphasized: cubic-bezier(0.2, 0, 0, 1);
}
| Token | Duration | Use |
|---|---|---|
--duration-fast |
120–150ms | Hover, toggles |
--duration-base |
200–250ms | Panels, fades |
--duration-slow |
300–400ms | Large layout shifts |
Pair durations with the CSS Animation Builder when you need keyframes (fade-in, slide-up) instead of transitions.
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.enter {
animation: fade-up var(--duration-base) var(--ease-out) both;
}
Respect reduced motion:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Keep essential opacity changes if needed for state, but drop large translational motion.
Shadows and gradients need dual consideration:
:root {
--shadow-md:
0 1px 2px rgb(15 23 42 / 0.06),
0 4px 12px rgb(15 23 42 / 0.08);
--gradient-subtle: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%);
}
[data-theme="dark"] {
--shadow-md:
0 1px 2px rgb(0 0 0 / 0.45),
0 8px 24px rgb(0 0 0 / 0.35);
--gradient-subtle: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
}
On dark UIs, deep black shadows disappear into the background — increase opacity slightly or add a faint light border. Brand gradients often need a darker stop set, not the same teal-on-teal that worked on white.
A cohesive surface recipe:
.panel {
background: var(--gradient-subtle);
border: 1px solid rgb(15 23 42 / 0.08);
box-shadow: var(--shadow-md);
border-radius: 12px;
transition:
transform var(--duration-fast) var(--ease-out),
box-shadow var(--duration-fast) var(--ease-out);
}
Checklist for a design-system PR:
prefers-reduced-motion coveredCodify gradients, shadows, and easings as a tiny token set — not one-off CSS per component. Soft elevation scales and ease-out curves do most of the visual work. Avoid animating heavy shadows and gradient stops; composite with transform and opacity. Build values in the gradient, shadow, bezier, and animation tools, then freeze them into CSS variables your whole UI reuses.
Choose transitions vs @keyframes, pick units that scale, and build reusable motion snippets — with performance and accessibility rules that keep UI snappy.
A practical guide to Vanilla Extract — type-safe styles at build time, theming with CSS variables, recipes and sprinkles, and when to pick it over Tailwind or CSS Modules.
A practical guide to modern CSS layout — flexbox for one-dimensional alignment, grid for two-dimensional structure, and patterns for combining them in production components.
Find a developer tool