A dawn mountain scene for a small valley lodge, drawn as four inline SVG ridges, a low sun and a band of drifting mist. As the section scrolls, each layer slides at its own rate, so the far peaks seem to hang back while the dark foreground carries the buttons up and away. The headline sits between the sky and the ridges, which lets the nearer hills rise over it. On phones the ridges stretch to the screen width and the copy stays centred above them.
import { useEffect, useRef } from "react";
// Each layer moves down by scroll * depth, so a higher depth reads as further away.
const LAYERS = [
{ depth: 0.42, fill: "#e8c39c", d: "M0 250 L110 205 L230 228 L370 150 L500 212 L640 168 L780 118 L930 190 L1060 146 L1210 214 L1330 172 L1440 198 V400 H0Z" },
{ depth: 0.3, fill: "#c98f6e", d: "M0 292 L150 238 L300 276 L450 206 L620 268 L760 226 L910 286 L1080 216 L1250 270 L1440 236 V400 H0Z" },
{ depth: 0.18, fill: "#8d5a49", d: "M0 330 C180 286 330 270 520 312 S850 356 1040 304 S1310 270 1440 314 V400 H0Z" },
];
const FRONT = "M0 250 C220 196 470 222 720 240 S1180 204 1440 232 V400 H0Z";
const NOTES = [
{ title: "Twelve rooms", text: "Every room faces the eastern ridge, so first light reaches the bed before the alarm does." },
{ title: "Marked trails", text: "Three loops leave from the porch, from a gentle valley walk to a full day on the high saddle." },
{ title: "A slow kitchen", text: "Breakfast runs until eleven and dinner is one long table, cooked from what the valley grows." },
];
export default function LayeredMountainLandscapeHeroWithDepthParallax() {
const root = useRef<HTMLDivElement>(null);
const layers = useRef<(HTMLDivElement | null)[]>([]);
useEffect(() => {
const box = root.current;
if (!box) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let frame = 0;
let on = true;
const draw = () => {
frame = 0;
const s = box.scrollTop; // on page scroll use: Math.max(0, -hero.getBoundingClientRect().top)
box.classList.toggle("w6lnd-moved", s > 8);
if (reduce) return;
layers.current.forEach((l) => {
if (l) l.style.transform = `translate3d(0,${(s * Number(l.dataset.depth)).toFixed(1)}px,0)`;
});
};
const req = () => {
if (on && !frame) frame = requestAnimationFrame(draw);
};
const io = new IntersectionObserver(([e]) => {
on = e.isIntersecting;
box.toggleAttribute("data-paused", !on);
if (on) req();
});
io.observe(box);
box.addEventListener("scroll", req, { passive: true });
return () => {
cancelAnimationFrame(frame);
io.disconnect();
box.removeEventListener("scroll", req);
};
}, []);
let i = 0;
const layerRef = (n: HTMLDivElement | null) => {
if (n) layers.current[Number(n.dataset.i)] = n;
};
return (
<div
ref={root}
tabIndex={0}
role="region"
aria-label="Haldor Ridge Lodge, scrollable preview"
className="w6lnd relative h-[640px] overflow-y-auto overscroll-contain bg-[#21181a] text-[#2a1b19] outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[#8d5a49]"
>
<style>{`
.w6lnd-layer { will-change: transform; }
.w6lnd-mist { animation: w6lnd-drift 26s ease-in-out infinite alternate; }
.w6lnd[data-paused] .w6lnd-mist { animation-play-state: paused; }
@keyframes w6lnd-drift { from { transform: translate3d(-4%,0,0); } to { transform: translate3d(4%,0,0); } }
.w6lnd-hint { transition: opacity .5s ease; }
.w6lnd-moved .w6lnd-hint { opacity: 0; }
@media (prefers-reduced-motion: reduce) { .w6lnd-mist { animation: none; } .w6lnd-layer { will-change: auto; } }
`}</style>
<header className="relative h-[640px] overflow-hidden bg-gradient-to-b from-[#f6e3c8] via-[#f2d3ae] to-[#eab98f]">
<div ref={layerRef} data-i={i++} data-depth="0.55" className="w6lnd-layer absolute left-[64%] top-[44%] -ml-16 h-32 w-32 rounded-full bg-[#fbeedb] shadow-[0_0_80px_30px_rgba(251,238,219,0.55)]" aria-hidden="true" />
<div className="w6lnd-hint pointer-events-none absolute right-4 top-4 z-30 rounded-full bg-[#2a1b19]/80 px-3 py-1.5 text-xs font-medium text-[#f6e3c8]">Scroll inside</div>
<div ref={layerRef} data-i={i++} data-depth="0.35" className="w6lnd-layer absolute inset-x-0 top-16 z-10 px-6 text-center">
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-[#6e3f30]">Haldor Ridge Lodge</p>
<h1 className="mx-auto mt-4 max-w-3xl text-[clamp(36px,6.6vw,78px)] font-semibold leading-[1.02] tracking-[-0.02em]">Wake up above the cloud line</h1>
<p className="mx-auto mt-4 max-w-md text-base leading-relaxed text-[#4a2c24]">A small lodge at the head of the valley, with the ridge outside every window.</p>
</div>
{LAYERS.map((l) => (
<div key={l.depth} ref={layerRef} data-i={i++} data-depth={l.depth} aria-hidden="true" className="w6lnd-layer absolute inset-x-0 bottom-0 z-20 h-[62%]">
<svg viewBox="0 0 1440 400" preserveAspectRatio="none" className="h-full w-full"><path d={l.d} fill={l.fill} /></svg>
</div>
))}
<div aria-hidden="true" className="w6lnd-mist absolute inset-x-[-10%] bottom-[16%] z-20 h-24 bg-gradient-to-b from-transparent via-[#f6e3c8]/40 to-transparent" />
<div aria-hidden="true" className="absolute inset-x-0 bottom-0 z-20 h-[30%]">
<svg viewBox="0 0 1440 400" preserveAspectRatio="none" className="h-full w-full"><path d={FRONT} fill="#21181a" /></svg>
</div>
<div className="absolute inset-x-0 bottom-8 z-30 flex flex-wrap justify-center gap-3 px-6">
<a href="#" className="inline-flex h-12 items-center rounded-full bg-[#f6e3c8] px-6 text-sm font-semibold text-[#21181a] transition hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#f6e3c8] focus-visible:ring-offset-2 focus-visible:ring-offset-[#21181a]">Check winter dates</a>
<a href="#" className="inline-flex h-12 items-center rounded-full border border-[#f6e3c8]/50 px-6 text-sm font-semibold text-[#f6e3c8] transition hover:border-[#f6e3c8] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#f6e3c8]">See the trails</a>
</div>
</header>
<section className="px-6 pb-20 pt-10 text-[#f1dcc4] sm:px-10">
<div className="mx-auto max-w-5xl">
<h2 className="max-w-xl text-2xl font-semibold leading-snug sm:text-3xl">Quiet rooms, early light and a porch that opens onto three trails.</h2>
<ul className="mt-10 grid gap-8 sm:grid-cols-3">
{NOTES.map((n) => (
<li key={n.title} className="border-t border-[#f1dcc4]/25 pt-5">
<h3 className="text-base font-semibold">{n.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-[#d9bfa6]">{n.text}</p>
</li>
))}
</ul>
</div>
</section>
</div>
);
}
Install with one command: npx shadcn@latest add https://we.inc/r/layered-mountain-landscape-hero-with-depth-parallax.json
Every layer is a normal absolutely positioned element inside a hero with overflow hidden. On each scroll frame the script multiplies the scroll distance by the layer's depth and moves it down by that amount with translate3d. Far layers therefore climb more slowly than the content, which is what the eye reads as distance. Fixed backgrounds are skipped because iOS ignores them and desktop browsers repaint them on every scroll.
It is a layer too, placed between the sky and the ridges with a depth of 0.5. The two nearer ridges move faster than the text, so they rise over it. If you would rather keep the heading on top, give its wrapper a higher z-index than the ridges and a depth of 0.
Yes, as long as each photo is a cut-out with a transparent sky, saved as PNG or WebP with explicit dimensions. Drawn SVG ridges are lighter and scale to any width without blurring, which is why this version uses them.
The scroll listener is passive and only asks for one animation frame at a time, and each frame writes a single transform per layer, which the browser handles on the compositor. When the hero is off screen an IntersectionObserver stops both the frame requests and the mist animation.
Keep the nearest moving layer close to the page speed and the furthest one well short of standing still. Big differences make headlines hard to read while they move and can bother people who are sensitive to motion, which is also why reduced motion turns the effect off.
Related: Parallax Starfield with Slow Drift, Kinetic Type Line Stretching With Scroll, Scroll Linked Words Brighten as You Read.
More scroll effects or all free UI components. Start for free to have a full site built around it.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.