Horizontal Scroll Section

Scrolling down pins the section and slides a row of panels sideways, then lets the page continue once the row ends.

  • JS
  • Layout
  • Scroll
  • Added
Scroll ↓
01Plan
02Design
03Build
04Launch

AI prompt

Create a horizontal scroll section driven by vertical scroll, in React and CSS, without a scroll library. Make an outer section several times the viewport height (4 panels → 400vh) and inside it a position: sticky; top: 0 wrapper of exactly one viewport height with overflow hidden. Inside the wrapper, a flex track holds the panels side by side. On scroll, compute progress as the distance scrolled into the section divided by (section height − viewport height), clamped 0 to 1, and set the track transform to translateX(−progress × (track width − viewport width)) via a CSS custom property, so it never re-renders React. Give each panel a large number and a label. Under prefers-reduced-motion, lay the panels out as a normal vertical stack instead. The panels read "Plan", "Design", "Build", and "Launch".

Source

Demo.tsx
'use client'

import { useRef } from 'react'
import s from './demo.module.css'

export default function HorizontalScroll({ labels }: { labels: Record<string, string> }) {
  const track = useRef<HTMLDivElement>(null)
  const panels = labels.panels.split('|')

  const onScroll = (e: React.UIEvent<HTMLDivElement>) => {
    const el = e.currentTarget
    const t = track.current
    if (!t) return
    const p = Math.min(Math.max(el.scrollTop / (el.scrollHeight - el.clientHeight || 1), 0), 1)
    t.style.setProperty('--x', `${-p * (t.scrollWidth - el.clientWidth)}px`)
  }

  return (
    <div className={s.frame} tabIndex={0} onScroll={onScroll} style={{ '--n': panels.length } as React.CSSProperties}>
      <div className={s.section}>
        <div className={s.sticky}>
          <span className={s.label}>{labels.hint} ↓</span>
          <div ref={track} className={s.track}>
            {panels.map((p, i) => (
              <div key={p} className={s.panel}>
                <span className={s.n}>{String(i + 1).padStart(2, '0')}</span>
                <strong className={s.name}>{p}</strong>
              </div>
            ))}
          </div>
        </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);
}

.section { height: calc(var(--n) * 100%); }

.sticky {
  position: sticky;
  top: 0;
  display: grid;
  align-content: center;
  gap: 12px;
  height: 240px;
  overflow: hidden;
}

.label {
  padding-left: 20px;
  font: 500 var(--text-xs) / 1 var(--font-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--stage-muted);
}

.track {
  --x: 0px;
  display: flex;
  gap: 12px;
  width: max-content;
  padding: 0 20px;
  transform: translateX(var(--x));
}

.panel {
  display: grid;
  align-content: space-between;
  width: 180px;
  height: 140px;
  padding: 14px;
  border: 1px solid var(--stage-rule);
  border-radius: 14px;
  background: var(--stage-surface);
}

.panel:nth-child(even) { background: var(--stage-ink); }
.panel:nth-child(even) .name { color: var(--stage-bg); }

.n { font: 500 var(--text-sm) / 1 var(--font-mono); color: var(--stage-accent); }
.name { font: 600 var(--text-2xl) / 1 var(--font-display); color: var(--stage-ink); }

@media (prefers-reduced-motion: reduce) {
  .section { height: auto; }
  .sticky { position: static; height: auto; padding: 16px 0; }
  .track { flex-direction: column; width: auto; transform: none; }
  .panel { width: auto; }
}

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.