22 CSS Parallax Effects18 / 22

Pure CSSMIT licensed

Horizontal Parallax Scroll Panels

Vertical scrolling drives a horizontal track: four panels slide across a pinned viewport while the art inside each panel counter-moves, so the sequence has depth as well as direction. Useful for process explainers and product stories where the steps want to read left to right without hijacking the wheel.

Published Updated

Live Demo
Try it

The code

<div class="plx-18">
  <header class="plx-18__head">
    <p class="plx-18__brand">Halide Film Lab</p>
    <h1 class="plx-18__h1">Four steps from roll to print</h1>
    <p class="plx-18__lede">Scroll down — the track moves sideways.</p>
  </header>
  <div class="plx-18__runway">
    <div class="plx-18__viewport">
      <div class="plx-18__track">
        <section class="plx-18__panel">
          <div class="plx-18__art" aria-hidden="true"><span class="plx-18__disc"></span><span class="plx-18__strip"></span></div>
          <div class="plx-18__copy">
            <p class="plx-18__num">01</p>
            <h2 class="plx-18__h2">Intake</h2>
            <p class="plx-18__p">Rolls are logged by hand, one card per roll, before anything is opened. The card follows the film through every bath and ends up in the envelope with your negatives.</p>
          </div>
        </section>
        <section class="plx-18__panel">
          <div class="plx-18__art" aria-hidden="true"><span class="plx-18__disc plx-18__disc--b"></span><span class="plx-18__strip"></span></div>
          <div class="plx-18__copy">
            <p class="plx-18__num">02</p>
            <h2 class="plx-18__h2">Develop</h2>
            <p class="plx-18__p">C-41 at 38 degrees, replenished daily and tested against a control strip every morning. Push and pull to plus or minus two stops on request.</p>
          </div>
        </section>
        <section class="plx-18__panel">
          <div class="plx-18__art" aria-hidden="true"><span class="plx-18__disc plx-18__disc--c"></span><span class="plx-18__strip"></span></div>
          <div class="plx-18__copy">
            <p class="plx-18__num">03</p>
            <h2 class="plx-18__h2">Scan</h2>
            <p class="plx-18__p">Flat scans at 3600 dpi with no automatic correction, plus a corrected set if you want one. You get both, always, at no extra cost.</p>
          </div>
        </section>
        <section class="plx-18__panel">
          <div class="plx-18__art" aria-hidden="true"><span class="plx-18__disc plx-18__disc--d"></span><span class="plx-18__strip"></span></div>
          <div class="plx-18__copy">
            <p class="plx-18__num">04</p>
            <h2 class="plx-18__h2">Print</h2>
            <p class="plx-18__p">Optical prints up to 30 by 40 centimetres, wet-mounted. Turnaround is four working days, or same-week for standing accounts.</p>
          </div>
        </section>
      </div>
    </div>
  </div>
  <footer class="plx-18__foot">
    <p class="plx-18__p">Drop-off Tuesday to Saturday. Mail-in envelopes are free — ask for one with your next order.</p>
  </footer>
</div>
.plx-18 {
  width: 100%;
  min-height: 100vh;
  display: block;
  position: relative;
  box-sizing: border-box;
  --font-display: "Avenir Next Condensed", "Oswald", "Arial Narrow", system-ui, sans-serif;
  --font-body: system-ui, -apple-system, "Segoe UI", sans-serif;
  --char: #211f1c;
  --char-2: #2b2824;
  --ink: #f6f1e6;
  --muted: #a89e8c;
  --accent: #ffb703;
  background: var(--char);
  color: var(--ink);
  font-family: var(--font-body);
  overflow-x: clip;
}

.plx-18 * {
  box-sizing: border-box;
}

.plx-18__head {
  padding: clamp(32px, 6vw, 76px) clamp(18px, 4vw, 54px) clamp(22px, 4vw, 44px);
  max-inline-size: 46rem;
}

.plx-18__brand {
  margin: 0 0 14px;
  font: 700 10px/1 var(--font-body);
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--accent);
}

.plx-18__h1 {
  margin: 0 0 10px;
  font-family: var(--font-display);
  font-size: clamp(28px, 6.2vw, 56px);
  font-weight: 600;
  line-height: 1.02;
  letter-spacing: 0.01em;
  text-transform: uppercase;
}

.plx-18__lede {
  margin: 0;
  font-size: 14px;
  line-height: 1.55;
  color: var(--muted);
}

.plx-18__runway {
  block-size: 360svh;
}

.plx-18__viewport {
  position: sticky;
  top: 0;
  block-size: 100svh;
  overflow: clip;
}

.plx-18__track {
  display: flex;
  block-size: 100%;
  inline-size: 400%;
  will-change: transform;
}

.plx-18__panel {
  inline-size: 25%;
  block-size: 100%;
  min-inline-size: 0;
  padding: clamp(16px, 3vw, 40px);
  display: grid;
  align-content: center;
  gap: clamp(14px, 2.4vw, 26px);
  border-inline-end: 1px solid #3a352e;
  background: var(--char-2);
  overflow: clip;
}

.plx-18__panel:nth-child(even) {
  background: #262320;
}

.plx-18__art {
  position: relative;
  block-size: clamp(120px, 26vh, 240px);
  will-change: transform;
}

.plx-18__disc {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 4%;
  inline-size: clamp(90px, 18vh, 170px);
  aspect-ratio: 1;
  border-radius: 50%;
  border: clamp(8px, 2vh, 16px) solid var(--accent);
  box-shadow: 8px 8px 0 0 #12100e;
}

.plx-18__disc--b {
  border-color: #f27e3f;
}

.plx-18__disc--c {
  border-color: #6fd3c7;
}

.plx-18__disc--d {
  border-color: #f6f1e6;
}

.plx-18__strip {
  position: absolute;
  inset-block-end: 6%;
  inset-inline: 12% 4%;
  block-size: clamp(28px, 6vh, 52px);
  background: repeating-linear-gradient(90deg, #12100e 0 10px, transparent 10px 26px) top / 100% 6px repeat-x, repeating-linear-gradient(90deg, #12100e 0 10px, transparent 10px 26px) bottom / 100% 6px repeat-x, #4a443b;
  box-shadow: 8px 8px 0 0 #12100e;
}

.plx-18__copy {
  min-inline-size: 0;
  max-inline-size: 34rem;
}

.plx-18__num {
  margin: 0 0 8px;
  font-family: var(--font-display);
  font-size: clamp(30px, 6vw, 58px);
  font-weight: 600;
  line-height: 1;
  color: var(--accent);
}

.plx-18__h2 {
  margin: 0 0 10px;
  font-family: var(--font-display);
  font-size: clamp(22px, 4.6vw, 38px);
  font-weight: 600;
  letter-spacing: 0.01em;
  text-transform: uppercase;
}

.plx-18__p {
  margin: 0;
  font-size: clamp(14px, 3vw, 16px);
  line-height: 1.66;
  color: #cdc4b3;
  text-wrap: pretty;
}

.plx-18__foot {
  padding: clamp(34px, 6vw, 80px) clamp(18px, 4vw, 54px) 120px;
  border-top: 1px solid #3a352e;
}

.plx-18__foot .plx-18__p {
  max-inline-size: 34rem;
  color: var(--muted);
}

@supports (animation-timeline: scroll()) {
  .plx-18__track {
    animation: plx-18-slide linear both;
    animation-timeline: scroll(root);
    animation-range: 6% 92%;
  }

  .plx-18__art {
    animation: plx-18-counter linear both;
    animation-timeline: scroll(root);
    animation-range: 6% 92%;
  }
}

@keyframes plx-18-slide {
  from {
    transform: translateX(0);
  }

  to {
    transform: translateX(-75%);
  }
}

@keyframes plx-18-counter {
  from {
    transform: translateX(40px);
  }

  to {
    transform: translateX(-40px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .plx-18__runway {
    block-size: auto;
  }

  .plx-18__viewport {
    position: static;
    block-size: auto;
  }

  .plx-18__track {
    animation: none !important;
    transform: none !important;
    display: grid;
    inline-size: 100%;
    will-change: auto;
  }

  .plx-18__panel {
    inline-size: 100%;
    block-size: auto;
    min-block-size: 70svh;
    border-inline-end: 0;
    border-bottom: 1px solid #3a352e;
  }

  .plx-18__art {
    animation: none !important;
    transform: none !important;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Parallax Effect from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Horizontal Parallax Scroll Panels
Source: https://codefronts.com/motion/css-parallax-designs/css-horizontal-parallax-scroll/

Vertical scrolling drives a horizontal track: four panels slide across a pinned viewport while the art inside each panel counter-moves, so the sequence has depth as well as direction. Useful for process explainers and product stories where the steps want to read left to right without hijacking the wheel.
## HTML
```html
<div class="plx-18">
  <header class="plx-18__head">
    <p class="plx-18__brand">Halide Film Lab</p>
    <h1 class="plx-18__h1">Four steps from roll to print</h1>
    <p class="plx-18__lede">Scroll down — the track moves sideways.</p>
  </header>
  <div class="plx-18__runway">
    <div class="plx-18__viewport">
      <div class="plx-18__track">
        <section class="plx-18__panel">
          <div class="plx-18__art" aria-hidden="true"><span class="plx-18__disc"></span><span class="plx-18__strip"></span></div>
          <div class="plx-18__copy">
            <p class="plx-18__num">01</p>
            <h2 class="plx-18__h2">Intake</h2>
            <p class="plx-18__p">Rolls are logged by hand, one card per roll, before anything is opened. The card follows the film through every bath and ends up in the envelope with your negatives.</p>
          </div>
        </section>
        <section class="plx-18__panel">
          <div class="plx-18__art" aria-hidden="true"><span class="plx-18__disc plx-18__disc--b"></span><span class="plx-18__strip"></span></div>
          <div class="plx-18__copy">
            <p class="plx-18__num">02</p>
            <h2 class="plx-18__h2">Develop</h2>
            <p class="plx-18__p">C-41 at 38 degrees, replenished daily and tested against a control strip every morning. Push and pull to plus or minus two stops on request.</p>
          </div>
        </section>
        <section class="plx-18__panel">
          <div class="plx-18__art" aria-hidden="true"><span class="plx-18__disc plx-18__disc--c"></span><span class="plx-18__strip"></span></div>
          <div class="plx-18__copy">
            <p class="plx-18__num">03</p>
            <h2 class="plx-18__h2">Scan</h2>
            <p class="plx-18__p">Flat scans at 3600 dpi with no automatic correction, plus a corrected set if you want one. You get both, always, at no extra cost.</p>
          </div>
        </section>
        <section class="plx-18__panel">
          <div class="plx-18__art" aria-hidden="true"><span class="plx-18__disc plx-18__disc--d"></span><span class="plx-18__strip"></span></div>
          <div class="plx-18__copy">
            <p class="plx-18__num">04</p>
            <h2 class="plx-18__h2">Print</h2>
            <p class="plx-18__p">Optical prints up to 30 by 40 centimetres, wet-mounted. Turnaround is four working days, or same-week for standing accounts.</p>
          </div>
        </section>
      </div>
    </div>
  </div>
  <footer class="plx-18__foot">
    <p class="plx-18__p">Drop-off Tuesday to Saturday. Mail-in envelopes are free — ask for one with your next order.</p>
  </footer>
</div>
```
## CSS
```css
.plx-18 {
  width: 100%;
  min-height: 100vh;
  display: block;
  position: relative;
  box-sizing: border-box;
  --font-display: "Avenir Next Condensed", "Oswald", "Arial Narrow", system-ui, sans-serif;
  --font-body: system-ui, -apple-system, "Segoe UI", sans-serif;
  --char: #211f1c;
  --char-2: #2b2824;
  --ink: #f6f1e6;
  --muted: #a89e8c;
  --accent: #ffb703;
  background: var(--char);
  color: var(--ink);
  font-family: var(--font-body);
  overflow-x: clip;
}

.plx-18 * {
  box-sizing: border-box;
}

.plx-18__head {
  padding: clamp(32px, 6vw, 76px) clamp(18px, 4vw, 54px) clamp(22px, 4vw, 44px);
  max-inline-size: 46rem;
}

.plx-18__brand {
  margin: 0 0 14px;
  font: 700 10px/1 var(--font-body);
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--accent);
}

.plx-18__h1 {
  margin: 0 0 10px;
  font-family: var(--font-display);
  font-size: clamp(28px, 6.2vw, 56px);
  font-weight: 600;
  line-height: 1.02;
  letter-spacing: 0.01em;
  text-transform: uppercase;
}

.plx-18__lede {
  margin: 0;
  font-size: 14px;
  line-height: 1.55;
  color: var(--muted);
}

.plx-18__runway {
  block-size: 360svh;
}

.plx-18__viewport {
  position: sticky;
  top: 0;
  block-size: 100svh;
  overflow: clip;
}

.plx-18__track {
  display: flex;
  block-size: 100%;
  inline-size: 400%;
  will-change: transform;
}

.plx-18__panel {
  inline-size: 25%;
  block-size: 100%;
  min-inline-size: 0;
  padding: clamp(16px, 3vw, 40px);
  display: grid;
  align-content: center;
  gap: clamp(14px, 2.4vw, 26px);
  border-inline-end: 1px solid #3a352e;
  background: var(--char-2);
  overflow: clip;
}

.plx-18__panel:nth-child(even) {
  background: #262320;
}

.plx-18__art {
  position: relative;
  block-size: clamp(120px, 26vh, 240px);
  will-change: transform;
}

.plx-18__disc {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 4%;
  inline-size: clamp(90px, 18vh, 170px);
  aspect-ratio: 1;
  border-radius: 50%;
  border: clamp(8px, 2vh, 16px) solid var(--accent);
  box-shadow: 8px 8px 0 0 #12100e;
}

.plx-18__disc--b {
  border-color: #f27e3f;
}

.plx-18__disc--c {
  border-color: #6fd3c7;
}

.plx-18__disc--d {
  border-color: #f6f1e6;
}

.plx-18__strip {
  position: absolute;
  inset-block-end: 6%;
  inset-inline: 12% 4%;
  block-size: clamp(28px, 6vh, 52px);
  background: repeating-linear-gradient(90deg, #12100e 0 10px, transparent 10px 26px) top / 100% 6px repeat-x, repeating-linear-gradient(90deg, #12100e 0 10px, transparent 10px 26px) bottom / 100% 6px repeat-x, #4a443b;
  box-shadow: 8px 8px 0 0 #12100e;
}

.plx-18__copy {
  min-inline-size: 0;
  max-inline-size: 34rem;
}

.plx-18__num {
  margin: 0 0 8px;
  font-family: var(--font-display);
  font-size: clamp(30px, 6vw, 58px);
  font-weight: 600;
  line-height: 1;
  color: var(--accent);
}

.plx-18__h2 {
  margin: 0 0 10px;
  font-family: var(--font-display);
  font-size: clamp(22px, 4.6vw, 38px);
  font-weight: 600;
  letter-spacing: 0.01em;
  text-transform: uppercase;
}

.plx-18__p {
  margin: 0;
  font-size: clamp(14px, 3vw, 16px);
  line-height: 1.66;
  color: #cdc4b3;
  text-wrap: pretty;
}

.plx-18__foot {
  padding: clamp(34px, 6vw, 80px) clamp(18px, 4vw, 54px) 120px;
  border-top: 1px solid #3a352e;
}

.plx-18__foot .plx-18__p {
  max-inline-size: 34rem;
  color: var(--muted);
}

@supports (animation-timeline: scroll()) {
  .plx-18__track {
    animation: plx-18-slide linear both;
    animation-timeline: scroll(root);
    animation-range: 6% 92%;
  }

  .plx-18__art {
    animation: plx-18-counter linear both;
    animation-timeline: scroll(root);
    animation-range: 6% 92%;
  }
}

@keyframes plx-18-slide {
  from {
    transform: translateX(0);
  }

  to {
    transform: translateX(-75%);
  }
}

@keyframes plx-18-counter {
  from {
    transform: translateX(40px);
  }

  to {
    transform: translateX(-40px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .plx-18__runway {
    block-size: auto;
  }

  .plx-18__viewport {
    position: static;
    block-size: auto;
  }

  .plx-18__track {
    animation: none !important;
    transform: none !important;
    display: grid;
    inline-size: 100%;
    will-change: auto;
  }

  .plx-18__panel {
    inline-size: 100%;
    block-size: auto;
    min-block-size: 70svh;
    border-inline-end: 0;
    border-bottom: 1px solid #3a352e;
  }

  .plx-18__art {
    animation: none !important;
    transform: none !important;
  }
}
```

How this works

The runway sets the pace. Four panels need three viewport-widths of travel, so the wrapper is 360svh tall and the sticky viewport inside it is 100svh. Scroll progress through the runway maps to translateX(-75%) on the track. Change the panel count and you change the runway height — that is the only arithmetic in the pattern.

Never hijack the wheel. The old way to do this was a wheel listener that converted deltas into scrollLeft, which breaks trackpad inertia, keyboard paging and every accessibility expectation. Here the page scrolls normally; the horizontal movement is a CSS transform reading the page's own scroll progress. Keyboard Page Down works, browser find-in-page works, and nothing is intercepted.

Counter-move the art for depth. Each panel's art plane translates with a smaller amplitude in the opposite direction, so as a panel crosses the viewport its interior shifts against the frame. Without that, horizontal panels feel like a slide deck advancing rather than a scene passing.

Reduced motion should change the layout, not just stop it. A track frozen mid-transform hides three of the four panels, so the reduced-motion branch unpins the viewport, drops translateX, and lets the panels stack vertically. The content is identical; only the choreography is gone.

Make it yours

  • Add a panel and raise the runway by 90svh to keep the same pace.
  • Change the track's end translateX if your panel widths are not full-viewport.
  • Reverse the sign for a right-to-left sequence.
  • Increase the art plane's counter amplitude for stronger in-panel depth.
  • Swap --accent to retint the numerals, rules and shadows.
  • Set panel width to 72vw so the next panel is always partly visible.

Gotchas — read before shipping

  • A wheel listener that converts vertical deltas into scrollLeft destroys trackpad inertia and keyboard paging — drive a transform from scroll progress instead.
  • Runway height and track distance must agree; a mismatch either leaves dead scroll at the end or clips the last panel.
  • Any overflow: hidden ancestor breaks the sticky viewport and the track stops pinning.
  • Reduced motion needs a layout change — a frozen track leaves most panels off screen.

Browser support

ChromeSafariFirefoxEdge
115+18+130+115+

Requires the scroll-driven animations module (Baseline 2024) for the horizontal movement. Older browsers get the reduced-motion layout: a clean vertical stack of the same four panels, no listener, no broken track.

Techniques used in this demo

Search CodeFronts

Loading…