CSS Animations
Natours uses CSS animations to create smooth, performant entrance effects and micro-interactions. All animations follow best practices for performance by utilizing GPU-accelerated properties and avoiding expensive layout recalculations.
Animation Philosophy
The project demonstrates professional animation techniques:
GPU Acceleration Only animate transform and opacity for 60fps performance
Purposeful Motion Animations guide attention and provide visual feedback
Timing Matters Carefully chosen durations and delays create rhythm
Easing Functions Natural motion with appropriate timing functions
Keyframe Animations
All keyframe definitions are centralized in sass/base/_animations.scss for easy maintenance and reuse.
Move In Left
Creates a smooth entrance from the left with a subtle bounce effect.
sass/base/_animations.scss
@keyframes moveInLeft {
0% {
opacity : 0 ;
transform : translateX ( -100 px );
}
80% {
transform : translateX ( 10 px );
}
100% {
opacity : 1 ;
transform : translateX ( 0 );
}
}
How It Works
Start invisible and offset
At 0%, element is opacity: 0 and positioned 100px to the left
Overshoot at 80%
Element moves past its final position by 10px, creating a bounce effect
Settle into place
At 100%, element is fully visible at its natural position
The overshoot at 80% creates a more natural, playful animation than a linear movement would.
Move In Right
Mirror of moveInLeft, creating entrance from the right.
sass/base/_animations.scss
@keyframes moveInRight {
0% {
opacity : 0 ;
transform : translateX ( 100 px );
}
80% {
transform : translateX ( -10 px );
}
100% {
opacity : 1 ;
transform : translateX ( 0 );
}
}
Key Differences
Starts 100px to the right (translateX(100px) instead of -100px)
Overshoots to the left (translateX(-10px) instead of 10px)
Same timing and opacity behavior
Move In Bottom
Simple upward entrance without overshoot.
sass/base/_animations.scss
@keyframes moveInBottom {
0% {
opacity : 0 ;
transform : translateY ( 32 px );
}
100% {
opacity : 1 ;
transform : translateY ( 0 );
}
}
Why No Overshoot?
This animation is simpler (only two keyframes) because:
Used for call-to-action buttons where subtlety is preferred
Shorter movement distance (32px vs 100px)
Vertical motion feels more natural without bounce
Smaller movements generally don’t need overshoot effects - they can feel jarring.
Applying Animations
Keyframes are applied to elements using the animation property. Let’s see real examples from the project.
Heading Animation
The main page heading uses both left and right animations for a dramatic entrance.
sass/base/_typography.scss
.heading-primary {
color : $color-white ;
text-transform : uppercase ;
backface-visibility : hidden ;
margin-bottom : 6 rem ;
& --main {
display : block ;
font-size : 6 rem ;
font-weight : 400 ;
letter-spacing : 3.5 rem ;
animation : moveInLeft 1 s ease ;
}
& --sub {
display : block ;
font-size : 2 rem ;
font-weight : 400 ;
letter-spacing : 1.75 rem ;
animation : moveInRight 1 s ease ;
}
}
Animation Properties Explained
Duration
Timing Function
backface-visibility
1s The animation takes 1 second to complete animation: moveInLeft 1s ease;
// ^
// duration
Timing guidelines:
0.2-0.5s: Quick micro-interactions
0.5-1s: Standard entrance animations
1-2s: Dramatic, attention-grabbing effects
2s+: Usually too slow, user frustration
ease Built-in easing function for natural motion animation: moveInLeft 1s ease;
// ^
// timing function
Common timing functions:
ease: Slow start, fast middle, slow end (default)
ease-in: Slow start, fast end
ease-out: Fast start, slow end
ease-in-out: Very slow start and end
linear: Constant speed (robotic)
hidden Prevents flickering during animation .heading-primary {
backface-visibility : hidden ;
}
This CSS property:
Hides the back face of 3D transformed elements
Prevents subtle rendering bugs during animations
Improves performance by telling browser element won’t flip
Should be on parent of animated elements
Visual Effect
The heading creates a balanced, symmetrical entrance:
START:
←―― "OUTDOORS" (invisible, 100px left)
"is where life happens" ――→ (invisible, 100px right)
ANIMATION:
―→ "OUTDOORS" ←―
―→ "is where life happens" ←―
END:
"OUTDOORS" (centered, visible)
"is where life happens" (centered, visible)
The call-to-action button uses a delayed animation to appear after the heading.
sass/components/_button.scss
.btn {
& --animated {
animation : moveInBottom .5 s ease .75 s ;
animation-fill-mode : backwards ;
}
}
Full Animation Syntax
animation: moveInBottom .5s ease .75s ;
// │ │ │ │
// │ │ │ └─ delay (0.75s)
// │ │ └────── timing function
// │ └────────── duration (0.5s)
// └────────────────────── animation name
Animation Delay
Page loads
Button is immediately visible at starting position (100% opacity, no transform)
0.75 second delay
Nothing happens for 0.75s, giving the heading time to animate first
Animation starts
Button animates upward from 32px below its final position
0.5s later
Animation complete - button is in final position
Total time until button reaches final position: 0.75s delay + 0.5s duration = 1.25s
Animation Fill Mode
animation-fill-mode: backwards;
This property solves a timing problem:
Without backwards:
0s ────────── 0.75s ─────── 1.25s
│ │ │
Button Delay Animation
visible ends, complete
at final animation
position starts
(jumps to
start state)
With backwards:
0s ──────────────────────── 0.75s ─────── 1.25s
│ │ │
Button Delay Animation
immediately ends, complete
takes 0% animation
keyframe starts
state (invisible,
32px down)
Always use animation-fill-mode: backwards with delayed animations to prevent elements from jumping.
GPU-Accelerated Properties
Notice that all animations only use transform and opacity. This is critical for performance .
transform GPU Accelerated translate, scale, rotate, skewRuns on GPU compositor thread
opacity GPU Accelerated Fading elements in/out Runs on GPU compositor thread
Properties to Avoid
CPU-Intensive Properties These cause layout recalculation and repainting:
width, height (triggers layout)
top, left, right, bottom (triggers layout)
margin, padding (triggers layout)
background-position (triggers repaint)
color, background-color (triggers repaint)
// ❌ BAD - Causes layout recalculation
@keyframes slideInBad {
from { left : -100 px ; }
to { left : 0 ; }
}
// ✅ GOOD - GPU accelerated
@keyframes slideInGood {
from { transform : translateX ( -100 px ); }
to { transform : translateX ( 0 ); }
}
The translateX() version is 100x faster on most devices because it doesn’t trigger layout recalculation.
The transform property is the foundation of performant animations.
Translate Functions
// X-axis (horizontal)
transform: translateX(100px); // Move right 100px
transform: translateX(-100px); // Move left 100px
// Y-axis (vertical)
transform: translateY(32px); // Move down 32px
transform: translateY(-32px); // Move up 32px
// Both axes
transform: translate(-50%, -50%); // Move left 50%, up 50%
Browser paints element once
Initial render creates a texture of the element
Transform moves the texture
GPU shifts the texture without re-rendering the element
No layout recalculation
Other elements aren’t affected - no cascade of changes
Runs on compositor thread
Main JavaScript thread stays free and responsive
// Multiple transforms in one declaration
transform: translateX(100px) rotate(45deg) scale(1 .1 );
// Applied from RIGHT to LEFT:
// 1. Scale to 110%
// 2. Rotate 45 degrees
// 3. Move right 100px
Transforms are applied from right to left, which affects the result when combining rotation with translation.
Timing and Easing
The right timing and easing make animations feel natural instead of robotic.
Duration Guidelines
Duration Use Case Example 0.1-0.2s Instant feedback Button active state 0.2-0.3s Micro-interactions Hover effects 0.3-0.5s Quick transitions Dropdown menus 0.5-1s Standard animations Entrance effects 1-2s Dramatic effects Hero section reveal 2s+ Special occasions Avoid generally
Easing Explained
ease (default)
ease-in
ease-out
ease-in-out
linear
Slow → Fast → Slow animation: moveInLeft 1s ease;
Best for: Most animations, natural motion Cubic bezier equivalent: cubic-bezier(0.25, 0.1, 0.25, 1) Slow → Fast animation: fadeOut 0 .3s ease-in;
Best for: Elements exiting the screen Accelerates as it goes, like gravity Fast → Slow animation: dropIn 0 .5s ease-out;
Best for: Elements entering the screen Decelerates, like friction slowing an object Slow → Fast → Slow (exaggerated) animation: slide 1s ease-in-out;
Best for: Looping animations, transitions More pronounced slow-down than ease Constant speed animation: rotate 2s linear infinite;
Best for: Loading spinners, continuous rotations Feels robotic for most animations - use sparingly
Custom Cubic Bezier
For complete control, define custom easing curves:
// Bouncy entrance
animation: bounce 0 .8s cubic-bezier(0 .68 , -0 .55 , 0 .265 , 1 .55 );
// Smooth, professional
animation: smooth 0 .6s cubic-bezier(0 .4 , 0 .0 , 0 .2 , 1);
Animation Choreography
Natours uses delays to create a sequence of animations that tell a story.
// 1. Main heading starts immediately
.heading-primary--main {
animation : moveInLeft 1 s ease ;
// Starts: 0s
// Ends: 1s
}
// 2. Sub heading starts simultaneously
.heading-primary--sub {
animation : moveInRight 1 s ease ;
// Starts: 0s
// Ends: 1s
}
// 3. Button waits for headings, then animates
.btn--animated {
animation : moveInBottom .5 s ease .75 s ;
animation-fill-mode : backwards ;
// Starts: 0.75s
// Ends: 1.25s
}
Timeline Visualization
0s 0.5s 0.75s 1s 1.25s
│ │ │ │ │
├─ Headings start │ │ │
│ (left & right) │ │ │
│ │ │ │
│ ├─ Button │ │
│ │ starts │ │
│ │ │ │
│ │ Headings │
│ │ complete │
│ │ │
│ │ Button complete
│ │ │
└─────────────────────┴────────────────────┴─────→
The button starts animating while the headings are still moving, creating overlapping motion that feels more dynamic than sequential animations.
Best Practices
Use transform, not position Always use translateX/Y instead of left/top for movement
Keep it subtle Animations should enhance, not distract. Less is more.
Respect user preferences Honor prefers-reduced-motion for accessibility
Test on real devices What’s smooth on desktop might stutter on mobile
Use delays purposefully Choreograph animations to tell a story
Add backface-visibility Prevent flickering with backface-visibility: hidden
Accessibility Considerations
Some users find animations disorienting or distracting.
Respecting User Preferences
// Disable animations for users who prefer reduced motion
@media ( prefers-reduced-motion : reduce) {
* ,
* ::before ,
* ::after {
animation-duration : 0.01 ms !important ;
animation-iteration-count : 1 !important ;
transition-duration : 0.01 ms !important ;
}
}
This media query respects the “Reduce motion” accessibility setting in operating systems.
Guidelines
Avoid flashing: No rapid on/off animations that could trigger seizures
Provide controls: For looping animations, offer pause buttons
Don’t rely on animation alone: Motion shouldn’t be the only way to convey information
Keep it short: Long animations frustrate users who see them repeatedly
Common Pitfalls
Animating width/height This causes layout thrashing and poor performance: // ❌ DON'T
@keyframes expandBad {
from { width : 0 ; }
to { width : 300 px ; }
}
Use scaleX instead: // ✅ DO
@keyframes expandGood {
from { transform : scaleX ( 0 ); }
to { transform : scaleX ( 1 ); }
}
Forgetting vendor prefixes While modern browsers support unprefixed animations, older ones need: -webkit-backface-visibility: hidden;
backface-visibility: hidden;
Use a tool like Autoprefixer to handle this automatically.
Too many simultaneous animations Animating dozens of elements at once can cause jank. Stagger animations with delays: .card:nth-child ( 1 ) { animation-delay : 0 s ; }
.card:nth-child ( 2 ) { animation-delay : 0.1 s ; }
.card:nth-child ( 3 ) { animation-delay : 0.2 s ; }
Advanced Techniques
Multiple Animations
Apply multiple keyframe animations to one element:
.element {
animation :
fadeIn 0.5 s ease ,
slideUp 0.5 s ease ,
bounce 0.8 s ease 0.5 s ;
}
Infinite Animations
@keyframes pulse {
0%, 100% { transform : scale ( 1 ); }
50% { transform : scale ( 1.05 ); }
}
.loading-indicator {
animation : pulse 2 s ease infinite ;
}
Animation Direction
.element {
animation : slideIn 1 s ease ;
animation-direction : reverse ; // Play animation backwards
}
Button Component See how animations are applied to interactive elements
Typography Learn about the heading styles that use these animations