Understanding Parallax Scrolling
Parallax scrolling creates an illusion of depth by moving background images slower than foreground content during scrolling. This technique adds visual interest and creates immersive user experiences. Modern CSS and JavaScript offer multiple approaches to achieve parallax effects.
Types of Parallax Effects
- CSS Fixed Attachment: Simple, performant, limited control
- Transform-based: Smooth, GPU-accelerated, more flexible
- JavaScript-driven: Complete control, custom animations
- Intersection Observer: Performance-optimized, modern approach
CSS Fixed Attachment Method
The simplest parallax effect uses background-attachment: fixed
. This creates a stationary background while content scrolls over it.
/* Basic fixed parallax */ .parallax-section { height: 100vh; background-image: url('mountain-landscape.jpg'); background-attachment: fixed; background-position: center; background-repeat: no-repeat; background-size: cover; } /* Content overlay */ .parallax-content { background: rgba(0,0,0,0.5); color: white; padding: 3rem; text-align: center; }
Fixed Attachment Demo
Scroll within this container to see the fixed background effect.
Multi-Layer Parallax with Transform
Create more sophisticated parallax effects by animating multiple layers at different speeds using CSS transforms and JavaScript.
Layered Mountains
Multiple layers moving at different speeds
/* HTML Structure */ <div class="parallax-container"> <div class="layer" data-speed="0.2"></div> <div class="layer" data-speed="0.5"></div> <div class="layer" data-speed="0.8"></div> <div class="content">Foreground Content</div> </div> /* CSS */ .parallax-container { height: 100vh; overflow: hidden; position: relative; } .layer { position: absolute; top: 0; left: 0; width: 100%; height: 120%; will-change: transform; } /* JavaScript */ window.addEventListener('scroll', () => { const scrolled = window.pageYOffset; const layers = document.querySelectorAll('.layer'); layers.forEach(layer => { const speed = layer.dataset.speed; const yPos = -(scrolled * speed); layer.style.transform = `translateY(${yPos}px)`; }); });
JavaScript Parallax Techniques
1. Transform-Based Parallax
Uses CSS transforms for smooth, GPU-accelerated animations.
function updateParallax() { const scrolled = window.pageYOffset; elements.forEach((el, index) => { const speed = (index + 1) * 0.5; const yPos = scrolled * speed; el.style.transform = `translateY(${yPos}px)`; }); } window.addEventListener('scroll', updateParallax);
2. Intersection Observer
Performance-optimized parallax that only animates visible elements.
const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { // Start parallax animation animateParallax(entry.target); } else { // Stop animation for performance stopParallax(entry.target); } }); }); // Observe parallax elements document.querySelectorAll('.parallax').forEach(el => { observer.observe(el); });
3. RequestAnimationFrame
Smooth animations using the browser's optimal rendering cycle.
let ticking = false; function updateParallax() { const scrolled = window.pageYOffset; // Update parallax elements parallaxElements.forEach(el => { const speed = el.dataset.speed || 0.5; const yPos = scrolled * speed; el.style.transform = `translateY(${yPos}px)`; }); ticking = false; } function requestTick() { if (!ticking) { requestAnimationFrame(updateParallax); ticking = true; } } window.addEventListener('scroll', requestTick);
Advanced Parallax Effects
1. 3D Perspective Parallax
/* 3D parallax container */ .parallax-3d { height: 100vh; overflow-x: hidden; overflow-y: auto; perspective: 1px; } .parallax-3d .layer { position: absolute; top: 0; left: 0; right: 0; bottom: 0; } /* Different depths */ .layer-back { transform: translateZ(-1px) scale(2); } .layer-middle { transform: translateZ(-0.5px) scale(1.5); } .layer-front { transform: translateZ(0); }
2. Mouse-Following Parallax
Mouse Parallax
Move your mouse to see the effect
// Mouse parallax effect document.addEventListener('mousemove', (e) => { const { clientX, clientY } = e; const centerX = window.innerWidth / 2; const centerY = window.innerHeight / 2; const deltaX = (clientX - centerX) / centerX; const deltaY = (clientY - centerY) / centerY; elements.forEach((el, index) => { const depth = (index + 1) * 10; const rotateX = deltaY * depth; const rotateY = deltaX * depth; el.style.transform = `rotateX(${rotateX}deg) rotateY(${rotateY}deg)`; }); });
3. Scroll-Based Animations
// Scroll-triggered parallax with easing function easeOutQuart(t) { return 1 - (--t) * t * t * t; } function animateOnScroll() { const scrolled = window.pageYOffset; const windowHeight = window.innerHeight; parallaxElements.forEach(el => { const elementTop = el.offsetTop; const elementHeight = el.offsetHeight; // Check if element is in viewport if (scrolled + windowHeight > elementTop && scrolled < elementTop + elementHeight) { const progress = (scrolled + windowHeight - elementTop) / (windowHeight + elementHeight); const easedProgress = easeOutQuart(progress); const translateY = easedProgress * 100; el.style.transform = `translateY(${translateY}px)`; } }); }
Performance Optimization
Critical Performance Tips
1. Use GPU Acceleration
/* Force GPU acceleration */ .parallax-element { will-change: transform; transform: translateZ(0); /* Hack for older browsers */ backface-visibility: hidden; } /* Use transforms instead of changing position */ /* Good */ element.style.transform = `translateY(${yPos}px)`; /* Bad - causes reflow */ element.style.top = yPos + 'px';
2. Throttle Scroll Events
// Throttle function function throttle(func, limit) { let inThrottle; return function() { const args = arguments; const context = this; if (!inThrottle) { func.apply(context, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } } } // Throttled scroll handler const throttledParallax = throttle(updateParallax, 16); // ~60fps window.addEventListener('scroll', throttledParallax);
3. Passive Event Listeners
// Passive listeners for better performance window.addEventListener('scroll', updateParallax, { passive: true }); // Use Intersection Observer for viewport detection const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('animate'); } }); }, { threshold: 0.1, rootMargin: '50px' });
4. Reduce Paint and Layout
/* Minimize expensive properties */ .parallax-element { /* Avoid these - cause repaint/reflow */ /* width, height, padding, margin, border */ /* Use these instead */ transform: translateY(0); opacity: 1; filter: blur(0); } /* Optimize background images */ .parallax-bg { background-size: cover; background-attachment: fixed; /* Use image-rendering for performance */ image-rendering: optimizeSpeed; }
Mobile Optimization
Mobile Parallax Challenges
- iOS Safari:
background-attachment: fixed
is disabled - Performance: Mobile devices have limited processing power
- Battery: Complex animations drain battery quickly
- Touch scrolling: Different scroll behavior than desktop
/* Mobile-first parallax approach */ .parallax-section { /* Default: simple background for mobile */ background-attachment: scroll; background-position: center; background-size: cover; } /* Enhanced parallax for capable devices */ @media (min-width: 768px) and (prefers-reduced-motion: no-preference) { .parallax-section { background-attachment: fixed; } } /* JavaScript feature detection */ const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i .test(navigator.userAgent); const prefersReducedMotion = window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches; if (!isMobile && !prefersReducedMotion) { // Enable full parallax initParallax(); } else { // Use simple scroll effects initSimpleEffects(); }
Alternative Mobile Techniques
/* Transform-based mobile parallax */ .mobile-parallax { transform: translateY(0); transition: transform 0.1s ease-out; } /* Simplified scroll effects */ function mobileParallax() { const scrolled = window.pageYOffset; const elements = document.querySelectorAll('.mobile-parallax'); elements.forEach(el => { const rect = el.getBoundingClientRect(); if (rect.top < window.innerHeight && rect.bottom > 0) { const speed = 0.1; // Reduced speed for mobile const yPos = scrolled * speed; el.style.transform = `translateY(${yPos}px)`; } }); } // Reduced frequency for mobile if (window.innerWidth < 768) { window.addEventListener('scroll', throttle(mobileParallax, 32), // 30fps for mobile { passive: true } ); }
CSS-Only Parallax Techniques
1. Pure CSS Transform Parallax
/* CSS-only parallax using perspective */ .parallax-container { height: 100vh; overflow-x: hidden; overflow-y: auto; perspective: 1px; } .parallax-layer { position: absolute; top: 0; left: 0; right: 0; bottom: 0; } .parallax-back { transform: translateZ(-1px) scale(2); } .parallax-mid { transform: translateZ(-0.5px) scale(1.5); } .parallax-front { transform: translateZ(0); }
2. CSS Animation Parallax
/* Keyframe-based parallax */ @keyframes parallaxMove { 0% { transform: translateY(0); } 100% { transform: translateY(-50px); } } .css-parallax { animation: parallaxMove 10s ease-in-out infinite alternate; animation-play-state: paused; animation-delay: calc(var(--scroll) * -1s); animation-iteration-count: 1; animation-fill-mode: both; } /* Update CSS custom property with JavaScript */ window.addEventListener('scroll', () => { const scrolled = window.pageYOffset; const maxScroll = document.body.scrollHeight - window.innerHeight; const scrollPercent = scrolled / maxScroll; document.documentElement.style.setProperty('--scroll', scrollPercent); });
Accessibility Considerations
Accessibility Best Practices
- Respect prefers-reduced-motion: Disable parallax for sensitive users
- Provide alternatives: Ensure content is accessible without parallax
- Avoid vestibular triggers: Limit extreme motion effects
- Test with screen readers: Ensure compatibility
/* Respect user preferences */ @media (prefers-reduced-motion: reduce) { .parallax-element { animation: none; transform: none !important; } .parallax-section { background-attachment: scroll; } } /* JavaScript implementation */ const prefersReducedMotion = window.matchMedia( '(prefers-reduced-motion: reduce)' ); function initParallax() { if (prefersReducedMotion.matches) { // Provide static alternative return; } // Initialize parallax effects setupParallaxEffects(); } // Listen for preference changes prefersReducedMotion.addEventListener('change', () => { if (prefersReducedMotion.matches) { disableParallax(); } else { enableParallax(); } });
Popular Parallax Libraries
1. AOS (Animate On Scroll)
<!-- Include AOS --> <link rel="stylesheet" href="https://unpkg.com/aos@next/dist/aos.css" /> <!-- HTML --> <div data-aos="fade-up" data-aos-duration="1000"> Content with parallax effect </div> <script src="https://unpkg.com/aos@next/dist/aos.js"></script> <script> AOS.init({ duration: 1200, once: true, offset: 100 }); </script>
2. Rellax.js
<!-- HTML --> <div class="rellax" data-rellax-speed="-7"> Slow parallax element </div> <div class="rellax" data-rellax-speed="5"> Fast parallax element </div> <script src="https://cdn.jsdelivr.net/gh/dixonandmoe/rellax@master/rellax.min.js"></script> <script> var rellax = new Rellax('.rellax', { speed: -7, center: false, wrapper: null, round: true, vertical: true, horizontal: false }); </script>
3. Lax.js
<script src="https://cdn.jsdelivr.net/npm/lax.js"></script> <div class="lax" data-lax-preset="spin">Spinning element</div> <div class="lax" data-lax-preset="fadeInOut">Fading element</div> <script> window.onload = function() { lax.setup(); // Initialize const updateLax = () => { lax.update(window.scrollY); window.requestAnimationFrame(updateLax); } window.requestAnimationFrame(updateLax); } </script>