Part 2: CSS Transitions Deep Dive
Creating Smooth, Professional Interactions
Transition Anatomy
transition: property duration timing-function delay;
transition: background-color 0.3s ease-in-out 0.1s;
Multi-Property Transitions
/* Shopping cart item example */
.cart-item {
background: white;
transform: scale(1);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border: 2px solid transparent;
/* Different timings for different properties */
transition:
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s ease-out,
border-color 0.15s linear,
background-color 0.25s ease-in;
}
.cart-item:hover {
transform: scale(1.02);
box-shadow: 0 8px 16px rgba(0,0,0,0.15);
border-color: var(--primary-color);
background: #f8f9fa;
}
- Stagger timings for depth - faster transforms feel more responsive than color changes
- Different easings per property - sharp for borders, smooth for shadows creates hierarchy
- Scale before shadow - mimics real-world physics of lifting off page
- Subtle is professional - 1.02 scale vs 1.2 scale makes huge difference
- Border reveals on hover - transparent to color avoids layout shift
- Orchestrated symphony - multiple properties create rich interaction
Custom Timing Functions
/* Bezier curves for precise control */
.smooth-bounce {
transition: transform 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
/* Creates overshoot effect */
}
.swift-out {
transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1);
/* Material Design "Swift Out" */
}
.elastic {
transition: transform 0.8s cubic-bezier(0.68, -0.6, 0.32, 1.6);
/* Elastic bounce effect */
}
/* Pre-defined vs custom */
.standard { transition: all 0.3s ease; } /* cubic-bezier(0.25, 0.1, 0.25, 1) */
.custom { transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } /* Google Material */
- Cubic-bezier creates personality - standard easings feel generic, custom ones feel designed
- Four control points define curve - x1, y1, x2, y2 between 0 and 1 (mostly)
- Y values beyond 0-1 create overshoot - negative or >1 makes elastic effects
- Material Design standardized timings - Swift out, smooth in, specific curves for specific uses
- Browser DevTools has curve editor - visual tool for creating custom beziers
- Match timing to brand personality - playful brands use bounce, serious use linear
Transition Gotchas & Solutions
/* Problem: Height auto doesn't transition */
.accordion-wrong {
height: 0;
transition: height 0.3s;
}
.accordion-wrong.open {
height: auto; /* Won't animate! */
}
/* Solution: Use max-height trick */
.accordion-right {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.accordion-right.open {
max-height: 500px; /* Larger than content */
}
- Auto values don't transition - browser can't interpolate between 0 and auto
- Max-height workaround - set larger than needed, overflow hidden clips excess
- Display none/block kills transitions - use opacity:0 with pointer-events:none instead
- Transform percentage gotcha - percentages relative to element, not parent
- Z-index doesn't transition - it's an integer, steps between values
- Gradient transitions need tricks - animate background-position or use pseudo-elements
State-Based Transition Patterns
/* Loading → Success → Error states */
.submit-button {
position: relative;
background: var(--primary);
transition: all 0.3s;
}
.submit-button.loading {
background: #6c757d;
pointer-events: none;
transform: scale(0.98);
}
.submit-button.success {
background: #28a745;
transform: scale(1.05) translateY(-2px);
}
.submit-button.error {
background: #dc3545;
animation: shake 0.5s;
}
- States need different feedback - loading subdued, success lifted, error attention-grabbing
- Pointer-events control interaction - disable during loading to prevent double-submits
- Transform for micro-animations - slight scale/translate makes states feel alive
- Combine transitions with animations - smooth state changes, then attention animation
- Color alone isn't enough - add motion for accessibility and clarity
- Reset transitions matter too - returning to default should feel intentional
Accessible Transitions
/* Respect user preferences */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Or more targeted approach */
@media (prefers-reduced-motion: reduce) {
.animated-element {
transition: opacity 0.2s; /* Keep fades, remove motion */
transform: none !important;
}
}
- Motion can trigger vestibular disorders - respect prefers-reduced-motion media query
- Don't remove all animation - instant changes are jarring, keep subtle fades
- 0.01ms not 0ms - some browsers optimize away 0ms, breaking JavaScript hooks
- Focus on opacity over motion - fades are generally safe, movement problematic
- Test with motion preferences - OS settings let you enable reduced motion
- Progressive enhancement approach - core functionality works without any animation
Part 3: Transform Property Mastery
2D and 3D Transformations
Transform Space
2D: X-axis (horizontal), Y-axis (vertical)
3D: + Z-axis (depth) + perspective
Transform Chaining & Order
/* Order matters! */
.card-flip {
/* This moves then rotates */
transform: translateX(100px) rotate(45deg);
/* Different from rotate then move! */
}
/* Student roster card example */
.student-card {
transform-origin: center bottom;
transition: transform 0.3s;
}
.student-card:hover {
/* Build complexity with chains */
transform:
perspective(1000px)
rotateX(-10deg)
translateY(-10px)
scale(1.05);
}
- Transforms apply right to left - last transform happens first mathematically
- Translate before rotate - moves along original axes, not rotated ones
- Scale affects all subsequent transforms - scaling first multiplies all pixel values
- Perspective must come first - sets up 3D space for other transforms
- Transform-origin is the pivot - crucial for realistic rotations
- Mental model: Matrix multiplication - each transform multiplies the transformation matrix
3D Transforms & Perspective
/* 3D product card */
.product-showcase {
perspective: 1000px; /* Parent sets viewport */
}
.product-card {
transform-style: preserve-3d;
transition: transform 0.6s;
transform-origin: center;
}
.product-card:hover {
transform: rotateY(180deg); /* Flip horizontally */
}
.product-card-front,
.product-card-back {
position: absolute;
backface-visibility: hidden;
}
.product-card-back {
transform: rotateY(180deg); /* Pre-rotated */
}
- Perspective creates depth - smaller values = more dramatic 3D effect
- Transform-style: preserve-3d - children maintain 3D position, not flattened
- Backface-visibility hidden - hides element when rotated away from viewer
- RotateX = horizontal axis flip - like a door opening up/down
- RotateY = vertical axis flip - like a door opening left/right
- RotateZ = flat spin - like a record player, no 3D effect needed
Advanced Transform Patterns
/* Parallax scrolling effect */
.parallax-layer {
transform: translateZ(-1px) scale(2);
/* Moves back in Z, scales up to compensate */
}
/* Circular menu items */
.menu-item:nth-child(1) { transform: rotate(0deg) translateX(100px) rotate(0deg); }
.menu-item:nth-child(2) { transform: rotate(60deg) translateX(100px) rotate(-60deg); }
.menu-item:nth-child(3) { transform: rotate(120deg) translateX(100px) rotate(-120deg); }
/* Isometric grid */
.isometric {
transform: rotateX(60deg) rotateZ(45deg);
}
- Parallax uses Z-axis depth - different layers move at different speeds
- Circular layouts with math - rotate, translate, counter-rotate keeps items upright
- Isometric = 2.5D effect - specific rotation angles create pseudo-3D
- Transform math is powerful - combine with CSS calc() for dynamic layouts
- GPU acceleration automatic - transforms trigger hardware acceleration
- Mobile consideration: 3D transforms can drain battery, use sparingly
Transform Performance Optimization
/* Force GPU layer */
.will-animate {
will-change: transform; /* Hints browser */
transform: translateZ(0); /* Hack to force layer */
}
/* Bad: Animating position */
@keyframes slide-bad {
from { left: -100%; }
to { left: 0; }
}
/* Good: Animating transform */
@keyframes slide-good {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
- will-change creates layer early - browser prepares for animation, costs memory
- translateZ(0) hack - forces GPU layer without will-change (older technique)
- Transform vs position performance - transform is 10x+ faster, no layout recalc
- Composite-only properties best - transform and opacity bypass layout/paint
- Remove will-change after animation - layers consume memory, clean up when done
- Profile before optimizing - DevTools shows actual performance, not assumptions
Transform Real-World Examples
/* Navigation drawer pattern */
.nav-drawer {
transform: translateX(-100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.nav-drawer.open {
transform: translateX(0);
}
/* Image zoom on hover */
.gallery-image {
transform-origin: center;
transition: transform 0.3s;
overflow: hidden;
}
.gallery-image img {
transform: scale(1);
transition: transform 0.3s;
}
.gallery-image:hover img {
transform: scale(1.2);
}
- Off-canvas navigation standard - translateX for side menus, translateY for bottom sheets
- Image zoom within container - overflow hidden contains scaled image
- Transform for modals - scale from 0.9 to 1 feels like emergence
- Mobile gestures with transform - track finger movement, apply as transform
- Infinite scroll performance - transform for position, not top/left
- Pattern: Always transform, never position - consistency ensures performance
Part 4: Keyframe Animations
Creating Complex, Multi-Step Animations
Keyframe Timeline
@keyframes timeline {
0% { /* Start */ }
25% { /* Quarter */ }
50% { /* Midpoint */ }
75% { /* Three quarters */ }
100% { /* End */ }
}
Loading Animations
/* Spinning loader */
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
/* Pulsing dots loader */
@keyframes pulse {
0%, 80%, 100% { transform: scale(1); opacity: 1; }
40% { transform: scale(1.3); opacity: 0.7; }
}
.dot { animation: pulse 1.4s infinite ease-in-out; }
.dot:nth-child(2) { animation-delay: 0.2s; }
.dot:nth-child(3) { animation-delay: 0.4s; }
- Infinite animations for waiting states - conveys ongoing process to user
- Linear timing for constant motion - spinning should be mechanical, not organic
- Animation-delay creates sequence - staggered dots feel more dynamic
- Scale and opacity together - multiple properties make animation richer
- Keep loaders lightweight - users see these when performance matters most
- Industry standard: 1-2 second loops - faster feels frantic, slower feels broken
Notification Animations
/* Slide in notification */
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Attention-grabbing shake */
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-10px); }
20%, 40%, 60%, 80% { transform: translateX(10px); }
}
.notification {
animation: slideIn 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.notification.error {
animation: slideIn 0.3s, shake 0.5s 0.3s; /* Chain animations */
}
- Entry animations grab attention - motion in peripheral vision triggers focus
- Overshoot easing feels energetic - slight bounce makes notifications feel urgent
- Shake for errors is universal - mimics head shaking "no" gesture
- Chain animations with delay - enter first, then attention animation
- Exit animations provide closure - fadeOut or slideOut confirms dismissal
- Duration psychology: 0.3s entry feels instant but smooth
Complex Multi-Step Animations
/* Student registration flow animation */
@keyframes formProgress {
0% {
transform: translateX(-100%);
opacity: 0;
}
20% {
transform: translateX(0);
opacity: 1;
}
40% {
transform: scale(1.05);
}
60% {
transform: scale(1) rotate(1deg);
}
80% {
transform: rotate(-1deg);
}
100% {
transform: rotate(0);
opacity: 1;
}
}
.form-step {
animation: formProgress 1.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
- Percentages create story beats - enter, emphasize, settle, complete
- Micro-movements add personality - slight rotation feels playful and human
- Forwards fill mode essential - maintains end state after animation
- 60% sweet spot for main action - not rushed, not dragging
- Easing across entire animation - creates cohesive feel vs per-keyframe easing
- Test at different speeds - users may have different motion preferences
Performance-Optimized Keyframes
/* Bad: Animating expensive properties */
@keyframes bad-resize {
0% { width: 100px; height: 100px; }
50% { width: 200px; height: 200px; }
100% { width: 100px; height: 100px; }
}
/* Good: Using transform for same effect */
@keyframes good-resize {
0%, 100% { transform: scale(1); }
50% { transform: scale(2); }
}
/* Optimization: Reduce keyframe points */
@keyframes optimized {
/* Only define changes */
50% { transform: scale(1.5) rotate(180deg); }
/* Browser interpolates 0% and 100% */
}
- Transform-only keyframes perform best - avoid width, height, margin changes
- Fewer keyframes = smoother animation - browser interpolation is optimized
- Omit unchanged properties - if only transform changes, don't repeat other props
- Animation-timing-function per keyframe - can optimize each segment
- Test on slowest target device - animations that work on desktop may lag on mobile
- Consider reduced motion - provide simpler alternatives for accessibility
Animation Orchestration
/* Staggered list animation */
.list-item {
opacity: 0;
transform: translateY(20px);
animation: fadeInUp 0.5s forwards;
}
.list-item:nth-child(1) { animation-delay: 0.1s; }
.list-item:nth-child(2) { animation-delay: 0.2s; }
.list-item:nth-child(3) { animation-delay: 0.3s; }
/* Use CSS custom properties for dynamic delays */
.list-item { animation-delay: calc(var(--index) * 0.1s); }
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
- Staggered animations create flow - elements appear to cascade naturally
- Animation-delay orchestrates timing - precise control over sequence
- CSS variables enable dynamic delays - JavaScript can set --index for each item
- 0.1s stagger feels connected - longer delays feel disconnected
- Forwards fill maintains end state - crucial for enter animations
- Pattern scales to any list size - works for 3 items or 300
Part 5: Design Systems & CSS Variables
Building Maintainable, Themeable Interfaces
Colors
--primary
--secondary
Spacing
--space-xs
--space-md
Animation
--duration
--easing
CSS Variables for Animation Systems
/* Animation design tokens */
:root {
/* Timing tokens */
--duration-instant: 0.1s;
--duration-fast: 0.2s;
--duration-normal: 0.3s;
--duration-slow: 0.5s;
--duration-slower: 0.8s;
/* Easing tokens */
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-out: cubic-bezier(0.0, 0, 0.2, 1);
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
/* Scale tokens */
--scale-xs: 0.95;
--scale-sm: 0.98;
--scale-md: 1.05;
--scale-lg: 1.1;
}
- Tokens create consistency - all animations use same timing vocabulary
- Semantic naming over values - "fast" not "200ms" makes intent clear
- Progressive scale - consistent increments feel designed, not random
- Custom easings as variables - change animation feel globally with one edit
- Design system alignment - animation tokens match spacing/color token patterns
- Documentation in code - variable names self-document usage
Dynamic Theme Switching
/* Base theme */
:root {
--bg-primary: #ffffff;
--text-primary: #333333;
--transition-theme: all 0.3s var(--ease-in-out);
}
/* Dark theme */
[data-theme="dark"] {
--bg-primary: #1a1a1a;
--text-primary: #ffffff;
}
/* Components use variables */
.card {
background: var(--bg-primary);
color: var(--text-primary);
transition: var(--transition-theme);
}
/* JavaScript toggle */
// document.body.dataset.theme = 'dark';
- CSS variables enable runtime theming - no recompilation needed unlike Sass
- Data attributes for theme state - cleaner than class toggling
- Transition during theme change - smooth color shifts feel polished
- Single source of truth - all colors derive from theme variables
- Respects system preferences - can default to OS dark mode setting
- Performance: CSS variables are fast - browser optimizes cascade
Component Animation Patterns
/* Reusable animation mixins via CSS */
.animate-in {
animation: var(--animation-name, fadeIn)
var(--duration-normal)
var(--ease-out)
forwards;
}
/* Component customization */
.modal { --animation-name: scaleIn; }
.drawer { --animation-name: slideIn; }
.toast { --animation-name: fadeInUp; }
/* Responsive animations */
@media (prefers-reduced-motion: no-preference) {
.animate-in { /* Full animation */ }
}
@media (prefers-reduced-motion: reduce) {
.animate-in { animation: fadeIn 0.2s; }
}
- Variables make animations reusable - same class, different effects per component
- Fallback values provide defaults - var(--name, fallback) syntax prevents breaks
- Component-specific overrides - modals scale, drawers slide, toasts fade up
- Media queries respect preferences - reduced motion gets simpler animations
- Single animation utility class - DRY principle for animation logic
- Composable patterns - combine multiple animation classes for complex effects
Advanced Variable Techniques
/* Calculated relationships */
:root {
--base-duration: 0.2s;
--duration-2x: calc(var(--base-duration) * 2);
--duration-3x: calc(var(--base-duration) * 3);
--base-size: 8px;
--space-1: var(--base-size);
--space-2: calc(var(--base-size) * 2);
--space-3: calc(var(--base-size) * 3);
}
/* Contextual overrides */
.fast-zone {
--base-duration: 0.1s; /* All children animate faster */
}
/* JavaScript integration */
.dynamic-element {
transform: translateX(calc(var(--mouse-x) * 1px))
translateY(calc(var(--mouse-y) * 1px));
}
- Calc() with variables enables systems - mathematical relationships between values
- Base values create rhythm - all timing/spacing derives from base unit
- Scoped overrides for context - form areas faster, reading areas slower
- JavaScript can set CSS variables - element.style.setProperty('--mouse-x', x)
- Variables cascade like properties - inheritance and specificity apply
- Performance: Calc is computed once - not recalculated on every frame
Production-Ready Animation System
/* Complete animation system */
:root {
/* State: Normal */
--state-duration: var(--duration-normal);
--state-easing: var(--ease-in-out);
/* State: Loading */
--loading-duration: var(--duration-slow);
--loading-easing: linear;
/* State: Error */
--error-duration: var(--duration-fast);
--error-easing: var(--ease-bounce);
}
/* Usage */
.button {
transition: all var(--state-duration) var(--state-easing);
}
.button.loading {
--state-duration: var(--loading-duration);
--state-easing: var(--loading-easing);
}
- State-based animation systems - different timing for different states
- Semantic state variables - loading, error, success have different feels
- Single source for changes - update root, entire system updates
- Designer-developer contract - variables become shared language
- Testable and measurable - animation tokens can be validated
- Production pattern: Used by GitHub, Stripe, Slack design systems