横向滚动区块

向下滚动时区块被固定,一排面板横向滑过,滑到尽头后页面才继续往下。

  • JS
  • 布局
  • 滚动
  • 收录于
向下滚动 ↓
01规划
02设计
03开发
04上线

AI 提示词

用 React 和 CSS 制作由纵向滚动驱动的横向滚动区块,不使用滚动库。外层 section 高度为视口高度的若干倍(4 个面板 → 400vh),内部是一个 position: sticky; top: 0、高度正好一个视口、overflow hidden 的容器。容器内用 flex 轨道把面板并排放置。滚动时计算进度:已滚入区块的距离除以(区块高度 − 视口高度),并限制在 0 到 1 之间,通过 CSS 自定义属性把轨道的 transform 设为 translateX(−进度 × (轨道宽度 − 视口宽度)),避免 React 重新渲染。每个面板显示一个大号数字和一个标签。在 prefers-reduced-motion 下改为普通的纵向堆叠布局。面板文字为“规划”“设计”“开发”“上线”。

源代码

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; }
}

投稿创意

看到想收录的动效或主题?简单描述一下,会打开一个预先填好的 GitHub Issue,确认后即可提交。

你想投稿的是?

需要 GitHub 账号,投稿内容会公开。