Skip to main content

Parallax Mountain Hero

Scroll-driven layered parallax hero. Sky, title, and foreground mountain translate at depth-weighted speeds via GSAP ScrollTrigger — a cinematic Z-axis sensation as the user traverses one viewport of scroll.

HeroReactGSAPScrollTrigger
CSSTailwind

Manual

Create a file and paste the following code into it.

src/components/ui/parallax-mountain-hero.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
"use client";

import { useLayoutEffect, useRef } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

const LAYER_SKY = "/images/components/parallax-mountain-hero/layer-1-sky.webp";
const LAYER_MOUNTAIN =
  "/images/components/parallax-mountain-hero/layer-2-mountain.webp";
const LAYER_HIKER =
  "/images/components/parallax-mountain-hero/layer-4-hiker.webp";

// Index = depth - 1. Bigger value = stays more static = visually farther.
const DEPTH_TO_YPERCENT = [70, 55, 40, 10] as const;

type ParallaxMountainHeroProps = {
  skySrc?: string;
  mountainSrc?: string;
  hikerSrc?: string;
  title?: string;
};

/**
 * Scroll-driven 4-layer parallax hero.
 *
 * @param {string} skySrc - URL for the deepest background image [Optional]
 * @param {string} mountainSrc - URL for the mid mountain ridge [Optional]
 * @param {string} hikerSrc - URL for the foreground figure [Optional]
 * @param {string} title - Hero title between mid and front layers [Optional, default: "Parallax"]
 *
 * @example
 * <ParallaxMountainHero />
 */
export function ParallaxMountainHero({
  skySrc = LAYER_SKY,
  mountainSrc = LAYER_MOUNTAIN,
  hikerSrc = LAYER_HIKER,
  title = "Parallax",
}: ParallaxMountainHeroProps) {
  const rootRef = useRef<HTMLDivElement>(null);
  const layersRef = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    const root = rootRef.current;
    const layersEl = layersRef.current;
    if (!root || !layersEl) return;

    let alive = true;

    const ctx = gsap.context(() => {
      const layers = layersEl.querySelectorAll<HTMLElement>(
        "[data-parallax-layer]",
      );

      const tl = gsap.timeline({
        scrollTrigger: {
          trigger: layersEl,
          start: "top top",
          end: "bottom top",
          scrub: true,
        },
      });

      layers.forEach((layer) => {
        const depth = Number(layer.dataset.parallaxLayer ?? "1");
        const yPercent = DEPTH_TO_YPERCENT[depth - 1] ?? 0;
        tl.fromTo(layer, { yPercent: 0 }, { yPercent, ease: "none" }, 0);
      });

      const imgs = Array.from(
        layersEl.querySelectorAll<HTMLImageElement>("img"),
      );
      Promise.all(
        imgs.map((img) => img.decode().catch(() => undefined)),
      ).then(() => {
        if (alive) ScrollTrigger.refresh();
      });
    }, root);

    return () => {
      alive = false;
      ctx.revert();
    };
  }, []);

  return (
    <div ref={rootRef} className="parallax-mountain">
      <link rel="preconnect" href="https://fonts.googleapis.com" />
      <link
        rel="preconnect"
        href="https://fonts.gstatic.com"
        crossOrigin="anonymous"
      />
      <link
        rel="stylesheet"
        href="https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap"
      />

      <section className="parallax-mountain__header">
        <div className="parallax-mountain__visuals">
          <div ref={layersRef} className="parallax-mountain__layers">
            <img
              src={skySrc}
              alt=""
              data-parallax-layer="1"
              className="parallax-mountain__layer-img"
              fetchPriority="high"
              loading="eager"
              decoding="async"
            />
            <img
              src={mountainSrc}
              alt=""
              data-parallax-layer="2"
              className="parallax-mountain__layer-img"
              fetchPriority="high"
              loading="eager"
              decoding="async"
            />
            <div
              data-parallax-layer="3"
              className="parallax-mountain__layer-title"
            >
              <h2 className="parallax-mountain__title">{title}</h2>
            </div>
            <img
              src={hikerSrc}
              alt=""
              data-parallax-layer="4"
              className="parallax-mountain__layer-img"
              loading="eager"
              decoding="async"
            />
          </div>
          <div className="parallax-mountain__fade" aria-hidden />
        </div>
      </section>

      <section className="parallax-mountain__content">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="80"
          height="80"
          viewBox="0 0 160 160"
          fill="none"
          aria-hidden
          className="parallax-mountain__mark"
        >
          <path
            d="M94.8284 53.8578C92.3086 56.3776 88 54.593 88 51.0294V0H72V59.9999C72 66.6273 66.6274 71.9999 60 71.9999H0V87.9999H51.0294C54.5931 87.9999 56.3777 92.3085 53.8579 94.8283L18.3431 130.343L29.6569 141.657L65.1717 106.142C67.684 103.63 71.9745 105.396 72 108.939V160L88.0001 160L88 99.9999C88 93.3725 93.3726 87.9999 100 87.9999H160V71.9999H108.939C105.407 71.9745 103.64 67.7091 106.12 65.1938L106.142 65.1716L141.657 29.6568L130.343 18.3432L94.8284 53.8578Z"
            fill="currentColor"
          />
        </svg>
      </section>

      <style>{`
        .parallax-mountain {
          width: 100%;
          position: relative;
          overflow: hidden;
          background: #000;
          color: #fff;
          -webkit-font-smoothing: antialiased;
          -moz-osx-font-smoothing: grayscale;
        }

        .parallax-mountain__header {
          z-index: 2;
          padding: 10em 1em;
          justify-content: center;
          align-items: center;
          min-height: 100dvh;
          display: flex;
          position: relative;
        }

        .parallax-mountain__content {
          padding: 10em 1em;
          justify-content: center;
          align-items: center;
          min-height: 100dvh;
          display: flex;
          position: relative;
        }

        .parallax-mountain__visuals {
          width: 100%;
          height: 120%;
          position: absolute;
          top: 0;
          left: 0;
        }

        .parallax-mountain__layers {
          width: 100%;
          height: 100%;
          position: absolute;
          top: 0;
          left: 0;
          overflow: hidden;
        }

        .parallax-mountain__layer-img {
          pointer-events: none;
          object-fit: cover;
          object-position: center;
          width: 100%;
          height: 117.5%;
          position: absolute;
          top: -17.5%;
          left: 0;
          user-select: none;
          -webkit-user-drag: none;
          will-change: transform;
        }

        .parallax-mountain__layer-title {
          justify-content: center;
          align-items: center;
          width: 100%;
          height: 100dvh;
          display: flex;
          position: absolute;
          top: 0;
          left: 0;
          will-change: transform;
        }

        .parallax-mountain__title {
          pointer-events: auto;
          text-align: center;
          margin: 0 .075em .1em 0;
          font-family: 'Archivo Black', 'Helvetica Neue', Helvetica, Arial, sans-serif;
          font-size: clamp(3.5rem, 14vw, 10em);
          font-weight: 700;
          line-height: 1;
          color: #fff;
          position: relative;
        }

        .parallax-mountain__fade {
          z-index: 30;
          background-image: linear-gradient(rgba(0, 0, 0, 0), #000);
          width: 100%;
          height: 20%;
          position: absolute;
          bottom: 0;
          left: 0;
          pointer-events: none;
        }

        .parallax-mountain__mark {
          color: #fff;
        }
      `}</style>
    </div>
  );
}

Update the import paths to match your project setup.

Similar components

3D Showcase Hero

Liquid Reveal

Grid Distortion

Pulse Stripes Layout

Resource details

PublishedMay 12, 2026
CategoryHero
ReactGSAPScrollTrigger