Web Pages Medium
Hero Parallax
A multi-layer parallax hero section driven by scroll position, creating depth through independent layer speeds.
Open in Lab
MCP
css js transform scroll-event vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #030712;
color: #f1f5f9;
overflow-x: hidden;
}
/* --- Hero --- */
.hero {
position: relative;
height: 100vh;
overflow: hidden;
display: grid;
place-items: center;
}
/* Parallax layer */
.layer {
position: absolute;
inset: -20%;
will-change: transform;
}
/* Stars layer */
.stars {
width: 100%;
height: 100%;
background-image: radial-gradient(
1px 1px at 20% 30%,
rgba(255, 255, 255, 0.8) 0%,
transparent 100%
), radial-gradient(1px 1px at 60% 15%, rgba(255, 255, 255, 0.6) 0%, transparent 100%),
radial-gradient(1px 1px at 80% 60%, rgba(255, 255, 255, 0.7) 0%, transparent 100%),
radial-gradient(2px 2px at 40% 75%, rgba(255, 255, 255, 0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 70% 40%, rgba(255, 255, 255, 0.8) 0%, transparent 100%),
radial-gradient(1px 1px at 10% 55%, rgba(255, 255, 255, 0.6) 0%, transparent 100%),
radial-gradient(2px 2px at 50% 88%, rgba(255, 255, 255, 0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 90% 25%, rgba(255, 255, 255, 0.7) 0%, transparent 100%);
background-size: 500px 400px;
background-repeat: repeat;
}
/* Gradient orbs */
.orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
}
.orb--1 {
width: 500px;
height: 400px;
top: 10%;
left: 5%;
background: rgba(56, 189, 248, 0.2);
}
.orb--2 {
width: 450px;
height: 380px;
bottom: 15%;
right: 5%;
background: rgba(168, 85, 247, 0.2);
}
/* Content layer — centered */
.layer--content {
position: relative;
inset: auto;
width: 100%;
display: grid;
place-items: center;
z-index: 10;
}
.hero-content {
max-width: 720px;
text-align: center;
padding: 2rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.25rem;
}
.hero-eyebrow {
font-size: 0.8125rem;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #38bdf8;
}
.hero-title {
font-size: clamp(2.5rem, 7vw, 5rem);
font-weight: 800;
line-height: 1.1;
letter-spacing: -0.03em;
background: linear-gradient(160deg, #ffffff 40%, #94a3b8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-subtitle {
font-size: clamp(1rem, 2vw, 1.125rem);
line-height: 1.65;
color: #64748b;
max-width: 520px;
}
.hero-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
justify-content: center;
margin-top: 0.5rem;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.9375rem;
font-weight: 600;
padding: 0.75rem 1.75rem;
border-radius: 0.875rem;
text-decoration: none;
transition: background 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
}
.btn--primary {
background: #0ea5e9;
color: #ffffff;
box-shadow: 0 0 24px rgba(14, 165, 233, 0.4);
}
.btn--primary:hover {
background: #38bdf8;
box-shadow: 0 0 32px rgba(14, 165, 233, 0.6);
transform: translateY(-1px);
}
.btn--ghost {
background: rgba(255, 255, 255, 0.06);
color: #cbd5e1;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.btn--ghost:hover {
background: rgba(255, 255, 255, 0.1);
color: #f1f5f9;
transform: translateY(-1px);
}
/* Below fold */
.below-fold {
min-height: 60vh;
display: grid;
place-items: center;
color: #475569;
font-size: 1.125rem;
}
/* Disable parallax for reduced motion */
@media (prefers-reduced-motion: reduce) {
.layer {
position: relative;
inset: auto;
transform: none !important;
}
}// Hero Parallax — multi-layer scroll-driven parallax
(function () {
"use strict";
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const layers = document.querySelectorAll(".layer[data-speed]");
if (!layers.length) return;
let ticking = false;
function updateLayers() {
const scrollY = window.scrollY;
layers.forEach((layer) => {
const speed = parseFloat(layer.dataset.speed ?? "0");
if (speed === 0) return; // content layer: no parallax
const offset = scrollY * speed;
layer.style.transform = `translateY(${offset}px)`;
});
ticking = false;
}
window.addEventListener(
"scroll",
() => {
if (!ticking) {
requestAnimationFrame(updateLayers);
ticking = true;
}
},
{ passive: true }
);
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hero Parallax</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<section class="hero" aria-label="Hero">
<div class="layer" data-speed="0.2" aria-hidden="true">
<!-- Far background: stars/dots -->
<div class="stars"></div>
</div>
<div class="layer" data-speed="0.5" aria-hidden="true">
<!-- Mid layer: gradient orbs -->
<div class="orb orb--1"></div>
<div class="orb orb--2"></div>
</div>
<div class="layer layer--content" data-speed="0.0">
<!-- Foreground: actual hero text -->
<div class="hero-content">
<p class="hero-eyebrow">Open Source</p>
<h1 class="hero-title">Build beautiful<br/>web experiences</h1>
<p class="hero-subtitle">
Reusable web resources — animations, pages, components, and patterns.
All open source.
</p>
<div class="hero-actions">
<a href="#" class="btn btn--primary">Browse Library</a>
<a href="#" class="btn btn--ghost">View Docs</a>
</div>
</div>
</div>
</section>
<main class="below-fold">
<p>Scroll up to see the parallax effect ↑</p>
</main>
<script src="script.js"></script>
</body>
</html>import { useEffect, useRef } from "react";
interface ParallaxLayerProps {
speed: number;
children: React.ReactNode;
className?: string;
}
function ParallaxLayer({ speed, children, className = "" }: ParallaxLayerProps) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
if (speed === 0) return;
let ticking = false;
const update = () => {
if (ref.current) {
ref.current.style.transform = `translateY(${window.scrollY * speed}px)`;
}
ticking = false;
};
const onScroll = () => {
if (!ticking) {
requestAnimationFrame(update);
ticking = true;
}
};
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, [speed]);
return (
<div ref={ref} className={`absolute inset-[-20%] will-change-transform ${className}`}>
{children}
</div>
);
}
export default function HeroParallax() {
return (
<div className="bg-gray-950 text-slate-100 font-sans">
{/* Hero */}
<section className="relative h-screen overflow-hidden grid place-items-center">
{/* Stars layer */}
<ParallaxLayer speed={0.2}>
<div
className="w-full h-full opacity-60"
style={{
backgroundImage: `
radial-gradient(1px 1px at 20% 30%, rgba(255,255,255,0.8) 0%, transparent 100%),
radial-gradient(1px 1px at 60% 15%, rgba(255,255,255,0.6) 0%, transparent 100%),
radial-gradient(2px 2px at 40% 75%, rgba(255,255,255,0.5) 0%, transparent 100%),
radial-gradient(1px 1px at 90% 25%, rgba(255,255,255,0.7) 0%, transparent 100%)
`,
backgroundSize: "500px 400px",
}}
/>
</ParallaxLayer>
{/* Orbs layer */}
<ParallaxLayer speed={0.5}>
<div
className="absolute w-[500px] h-[400px] top-[10%] left-[5%] rounded-full"
style={{ background: "rgba(56,189,248,0.2)", filter: "blur(80px)" }}
/>
<div
className="absolute w-[450px] h-[380px] bottom-[15%] right-[5%] rounded-full"
style={{ background: "rgba(168,85,247,0.2)", filter: "blur(80px)" }}
/>
</ParallaxLayer>
{/* Content */}
<div className="relative z-10 text-center px-8 flex flex-col items-center gap-5 max-w-3xl">
<p className="text-xs font-semibold tracking-[0.12em] uppercase text-sky-400">
Open Source
</p>
<h1 className="text-5xl md:text-7xl font-extrabold leading-[1.1] tracking-tight bg-gradient-to-b from-white to-slate-400 bg-clip-text text-transparent">
Build beautiful
<br />
web experiences
</h1>
<p className="text-slate-500 text-lg leading-relaxed max-w-md">
Reusable web resources — animations, pages, components, and patterns. All open source.
</p>
<div className="flex gap-4 flex-wrap justify-center mt-2">
<a
href="#"
className="px-7 py-3 rounded-2xl bg-sky-500 text-white font-semibold text-base hover:bg-sky-400 transition-colors shadow-[0_0_24px_rgba(14,165,233,0.4)]"
>
Browse Library
</a>
<a
href="#"
className="px-7 py-3 rounded-2xl bg-white/6 border border-white/10 text-slate-300 font-semibold text-base hover:bg-white/10 transition-colors"
>
View Docs
</a>
</div>
</div>
</section>
<div className="min-h-[60vh] grid place-items-center text-slate-600 text-lg">
Scroll up to see the parallax effect ↑
</div>
</div>
);
}<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const starsRef = ref(null);
const orbsRef = ref(null);
let ticking = false;
function onScroll() {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
const y = window.scrollY;
if (starsRef.value) starsRef.value.style.transform = `translateY(${y * 0.2}px)`;
if (orbsRef.value) orbsRef.value.style.transform = `translateY(${y * 0.5}px)`;
ticking = false;
});
}
onMounted(() => {
if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
window.addEventListener("scroll", onScroll, { passive: true });
}
});
onUnmounted(() => {
window.removeEventListener("scroll", onScroll);
});
</script>
<template>
<div style="background:#030712;color:#f1f5f9;font-family:system-ui,-apple-system,sans-serif">
<!-- Hero -->
<section style="position:relative;height:100vh;overflow:hidden;display:grid;place-items:center">
<!-- Stars layer -->
<div ref="starsRef" style="position:absolute;inset:-20%;will-change:transform">
<div style="width:100%;height:100%;opacity:0.6;background-image:radial-gradient(1px 1px at 20% 30%, rgba(255,255,255,0.8) 0%, transparent 100%),radial-gradient(1px 1px at 60% 15%, rgba(255,255,255,0.6) 0%, transparent 100%),radial-gradient(2px 2px at 40% 75%, rgba(255,255,255,0.5) 0%, transparent 100%),radial-gradient(1px 1px at 90% 25%, rgba(255,255,255,0.7) 0%, transparent 100%);background-size:500px 400px"></div>
</div>
<!-- Orbs layer -->
<div ref="orbsRef" style="position:absolute;inset:-20%;will-change:transform">
<div style="position:absolute;width:500px;height:400px;top:10%;left:5%;border-radius:50%;background:rgba(56,189,248,0.2);filter:blur(80px)"></div>
<div style="position:absolute;width:450px;height:380px;bottom:15%;right:5%;border-radius:50%;background:rgba(168,85,247,0.2);filter:blur(80px)"></div>
</div>
<!-- Content -->
<div style="position:relative;z-index:10;text-align:center;padding:0 2rem;display:flex;flex-direction:column;align-items:center;gap:1.25rem;max-width:48rem">
<p style="font-size:0.75rem;font-weight:600;letter-spacing:0.12em;text-transform:uppercase;color:#38bdf8">Open Source</p>
<h1 style="font-size:clamp(2.5rem,7vw,4.5rem);font-weight:800;line-height:1.1;letter-spacing:-0.02em;margin:0;background:linear-gradient(to bottom,white,#94a3b8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">
Build beautiful<br/>web experiences
</h1>
<p style="color:#64748b;font-size:1.125rem;line-height:1.6;max-width:28rem;margin:0">
Reusable web resources — animations, pages, components, and patterns. All open source.
</p>
<div style="display:flex;gap:1rem;flex-wrap:wrap;justify-content:center;margin-top:0.5rem">
<a href="#" style="padding:0.75rem 1.75rem;border-radius:1rem;background:#0ea5e9;color:white;font-weight:600;font-size:1rem;text-decoration:none;box-shadow:0 0 24px rgba(14,165,233,0.4)">Browse Library</a>
<a href="#" style="padding:0.75rem 1.75rem;border-radius:1rem;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);color:#cbd5e1;font-weight:600;font-size:1rem;text-decoration:none">View Docs</a>
</div>
</div>
</section>
<div style="min-height:60vh;display:grid;place-items:center;color:#475569;font-size:1.125rem">
Scroll up to see the parallax effect ↑
</div>
</div>
</template><script>
import { onMount, onDestroy } from "svelte";
let starsEl;
let orbsEl;
let ticking = false;
function onScroll() {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
const y = window.scrollY;
if (starsEl) starsEl.style.transform = `translateY(${y * 0.2}px)`;
if (orbsEl) orbsEl.style.transform = `translateY(${y * 0.5}px)`;
ticking = false;
});
}
onMount(() => {
if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
window.addEventListener("scroll", onScroll, { passive: true });
}
});
onDestroy(() => {
if (typeof window !== "undefined") {
window.removeEventListener("scroll", onScroll);
}
});
</script>
<div style="background:#030712;color:#f1f5f9;font-family:system-ui,-apple-system,sans-serif">
<!-- Hero -->
<section style="position:relative;height:100vh;overflow:hidden;display:grid;place-items:center">
<!-- Stars layer -->
<div bind:this={starsEl} style="position:absolute;inset:-20%;will-change:transform">
<div style="width:100%;height:100%;opacity:0.6;background-image:radial-gradient(1px 1px at 20% 30%, rgba(255,255,255,0.8) 0%, transparent 100%),radial-gradient(1px 1px at 60% 15%, rgba(255,255,255,0.6) 0%, transparent 100%),radial-gradient(2px 2px at 40% 75%, rgba(255,255,255,0.5) 0%, transparent 100%),radial-gradient(1px 1px at 90% 25%, rgba(255,255,255,0.7) 0%, transparent 100%);background-size:500px 400px"></div>
</div>
<!-- Orbs layer -->
<div bind:this={orbsEl} style="position:absolute;inset:-20%;will-change:transform">
<div style="position:absolute;width:500px;height:400px;top:10%;left:5%;border-radius:50%;background:rgba(56,189,248,0.2);filter:blur(80px)"></div>
<div style="position:absolute;width:450px;height:380px;bottom:15%;right:5%;border-radius:50%;background:rgba(168,85,247,0.2);filter:blur(80px)"></div>
</div>
<!-- Content -->
<div style="position:relative;z-index:10;text-align:center;padding:0 2rem;display:flex;flex-direction:column;align-items:center;gap:1.25rem;max-width:48rem">
<p style="font-size:0.75rem;font-weight:600;letter-spacing:0.12em;text-transform:uppercase;color:#38bdf8">Open Source</p>
<h1 style="font-size:clamp(2.5rem,7vw,4.5rem);font-weight:800;line-height:1.1;letter-spacing:-0.02em;margin:0;background:linear-gradient(to bottom,white,#94a3b8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text">
Build beautiful<br/>web experiences
</h1>
<p style="color:#64748b;font-size:1.125rem;line-height:1.6;max-width:28rem;margin:0">
Reusable web resources — animations, pages, components, and patterns. All open source.
</p>
<div style="display:flex;gap:1rem;flex-wrap:wrap;justify-content:center;margin-top:0.5rem">
<a href="#" style="padding:0.75rem 1.75rem;border-radius:1rem;background:#0ea5e9;color:white;font-weight:600;font-size:1rem;text-decoration:none;box-shadow:0 0 24px rgba(14,165,233,0.4)">Browse Library</a>
<a href="#" style="padding:0.75rem 1.75rem;border-radius:1rem;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);color:#cbd5e1;font-weight:600;font-size:1rem;text-decoration:none">View Docs</a>
</div>
</div>
</section>
<div style="min-height:60vh;display:grid;place-items:center;color:#475569;font-size:1.125rem">
Scroll up to see the parallax effect ↑
</div>
</div>Hero Parallax
A multi-layer parallax hero that creates a sense of depth by moving background layers at different speeds relative to scroll position.
How it works
Each layer has a data-speed attribute controlling how fast it moves relative to the scroll:
0.0— fixed (doesn’t move)0.5— moves at half scroll speed1.0— moves at full scroll speed (standard)
On scroll, requestAnimationFrame reads window.scrollY and applies translateY to each layer.
Performance tips
- Use
will-change: transformto hint GPU compositing - Keep layers to 3–4 max to avoid overdraw
- Use
requestAnimationFrame(notscrollevent directly) for smooth 60fps
Accessibility
Wrap with prefers-reduced-motion check to disable parallax for users who prefer it.