Parallax Layers

Hills, sun and title move at different speeds as you scroll, so a flat scene gains depth.

  • JS
  • Backgrounds
  • Scroll
  • Added

Into the hills

Scroll ↓

AI prompt

Create a layered parallax scene in React and CSS. Stack four absolutely positioned layers in a sticky scene: sky with a sun, far hills, near hills, and the headline. On scroll, compute progress 0 to 1 through the section and write it into a CSS custom property --p on the scene (no React state). Each layer moves with its own factor: transform: translateY(calc(var(--p) * <n>px)), where farther layers move less (sun 60px, far hills 30px, near hills -20px) and the headline drifts up and fades (translateY(calc(var(--p) * -80px)), opacity calc(1 - var(--p) * 1.4)). Draw the hills with clip-path polygons or radial gradients. Use will-change: transform on the layers. Under prefers-reduced-motion, keep every layer still. The headline reads "Into the hills".

Source

Demo.tsx
'use client'

import s from './demo.module.css'

export default function ParallaxLayers({ labels }: { labels: Record<string, string> }) {
  const onScroll = (e: React.UIEvent<HTMLDivElement>) => {
    const el = e.currentTarget
    el.style.setProperty('--p', String(el.scrollTop / (el.scrollHeight - el.clientHeight || 1)))
  }

  return (
    <div className={s.frame} tabIndex={0} onScroll={onScroll}>
      <div className={s.section}>
        <div className={s.scene}>
          <span className={s.sun} />
          <span className={s.far} />
          <p className={s.title}>{labels.title}</p>
          <span className={s.near} />
          <span className={s.label}>{labels.hint} ↓</span>
        </div>
      </div>
    </div>
  )
}
demo.module.css
.frame {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}

.frame:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -4px; }

.hint {
  display: block;
  font: 500 var(--text-xs) / 1 var(--font-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;
  color: var(--stage-muted);
}

.frame { --p: 0; }

.section { height: 300%; }

.scene {
  position: sticky;
  top: 0;
  height: 240px;
  overflow: hidden;
  background: linear-gradient(oklch(34% 0.06 280), oklch(62% 0.11 45));
}

.sun, .far, .near, .title { position: absolute; will-change: transform; }

.sun {
  left: 60%;
  top: 30%;
  width: 54px;
  height: 54px;
  border-radius: 50%;
  background: oklch(90% 0.09 80);
  transform: translateY(calc(var(--p) * 60px));
}

.far {
  inset: 45% -10% -10%;
  background: oklch(40% 0.07 30);
  clip-path: polygon(0 40%, 18% 10%, 34% 35%, 52% 0, 70% 30%, 86% 8%, 100% 30%, 100% 100%, 0 100%);
  transform: translateY(calc(var(--p) * 30px));
}

.near {
  inset: 68% -10% -20%;
  background: var(--stage-bg);
  clip-path: polygon(0 30%, 22% 0, 40% 25%, 62% 5%, 80% 30%, 100% 10%, 100% 100%, 0 100%);
  transform: translateY(calc(var(--p) * -20px));
}

.title {
  left: 0;
  right: 0;
  top: 18%;
  margin: 0;
  text-align: center;
  font: 700 var(--text-2xl) / 1 var(--font-display);
  color: var(--stage-ink);
  transform: translateY(calc(var(--p) * -80px));
  opacity: calc(1 - var(--p) * 1.4);
}

.label {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 12px;
  font: 500 var(--text-xs) / 1 var(--font-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;
  color: var(--stage-muted);
}

@media (prefers-reduced-motion: reduce) {
  .sun, .far, .near, .title { transform: none; opacity: 1; }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.