wui
组件

平滑滚动 Smooth Scroll

用 Lenis 为页面或自定义容器提供惯性平滑滚动基座。

第三方依赖 · lenis

基础用法

左右两栏内容相同:左侧为浏览器原生滚动,右侧由 SmoothScroll 接管。用滚轮分别滚动,对比惯性与停顿的手感;右栏顶部的进度条通过 useLenis 读取实例进度。

Loading…
pnpm dlx @wui-design/cli@latest add @wui/smooth-scroll
components/ui/smooth-scroll.tsx
"use client"

import * as React from "react"
import { ReactLenis, type LenisProps } from "lenis/react"
import type { LenisOptions } from "lenis"

export interface SmoothScrollProps extends Omit<LenisProps, "options"> {
  /** Content rendered inside the Lenis context or custom scroll container. */
  children?: React.ReactNode
  /** Lenis instance options. */
  options?: LenisOptions
  /** Disable interpolation while preserving the content wrapper. @default false */
  disabled?: boolean
  /** Respect the operating system reduced-motion preference. @default true */
  respectReducedMotion?: boolean
}

/** Provides Lenis smooth scrolling for the document or a custom container. */
function SmoothScroll({
  children,
  root = true,
  options,
  disabled = false,
  respectReducedMotion = true,
  className,
  ...props
}: SmoothScrollProps) {
  const [reduceMotion, setReduceMotion] = React.useState(false)

  React.useEffect(() => {
    if (!respectReducedMotion) return
    const media = window.matchMedia("(prefers-reduced-motion: reduce)")
    const update = () => setReduceMotion(media.matches)
    update()
    media.addEventListener("change", update)
    return () => media.removeEventListener("change", update)
  }, [respectReducedMotion])

  if (disabled || reduceMotion) {
    return root ? (
      <>{children}</>
    ) : (
      <div className={className} {...props}>
        {children}
      </div>
    )
  }

  return (
    <ReactLenis
      root={root}
      className={className}
      options={{ autoRaf: true, ...options }}
      {...props}
    >
      {children}
    </ReactLenis>
  )
}

export { SmoothScroll }

使用场景

SmoothScroll 封装 Lenis 的 React Provider。默认 root 模式接管文档滚动;设置 root={false} 可以创建独立滚动容器。 options 会直接传给 Lenis,并默认开启 autoRaf。页面级平滑滚动和局部内容滚动应分别选择一种,不要重复包裹同一滚动区域。

子组件可以通过 lenis/react 导出的 useLenis 订阅滚动,例如驱动阅读进度:

import { useLenis } from "lenis/react"
import { motion, useMotionValue } from "motion/react"

function Progress() {
  const progress = useMotionValue(0)
  useLenis((lenis) => progress.set(lenis.progress))
  return <motion.div className="h-0.5 origin-left bg-foreground" style={{ scaleX: progress }} />
}

按照 Lenis 官方建议,在应用入口引入配套样式:

import "lenis/dist/lenis.css"

属性

属性类型默认值说明
childrenReactNode—要放入 Lenis 上下文或自定义滚动容器中的内容。
optionsLenisOptions—Lenis 实例的配置。
disabledbooleanfalse是否停用插值滚动,但保留内容包装结构。
respectReducedMotionbooleantrue是否遵循操作系统的减少动态效果偏好。
rootboolean | "asChild"true是否在页面上初始化全局 Lenis 实例。
autoRafbooleantrue是否自动使用 requestAnimationFrame 驱动 Lenis。

使用说明

平滑滚动应作为页面级能力只挂载一次,不要在普通卡片中层层嵌套。模态框、代码编辑器和需要原生滚动的区域可使用 Lenis 的 data-lenis-prevent 属性。组件会尊重用户的“减少动态效果”偏好,此时保留内容并停止滚动插值,恢复为普通滚动行为。

Lenis 的完整选项、推荐 CSS 和集成方式请参考官方 React 文档。