wui
组件

视口进入动效 In View

当元素滚动进入视口可见范围时,自动触发淡入、位移或自定义变体动画的交叉观察(Intersection Observer)组件。

第三方依赖 · motion

基础用法

向下滚动页面,元素在进入视口交叉范围时平滑淡入并向上浮现:

Loading…

安装与引入

通过 CLI 自动添加组件,或手动复制源码至项目中:

pnpm dlx @wui-design/cli@latest add @wui/in-view
安装基础依赖与动效库
pnpm add motion lucide-react class-variance-authority clsx tailwind-merge
复制组件源码到 components/ui/in-view.tsx
components/ui/in-view.tsx
"use client"

import * as React from "react"
import {
  motion,
  useInView as useMotionInView,
  useReducedMotion,
  type HTMLMotionProps,
  type Transition,
  type UseInViewOptions,
  type Variants,
} from "motion/react"

const defaultVariants: Variants = {
  hidden: { opacity: 0, y: 16 },
  visible: { opacity: 1, y: 0 },
}

export interface InViewProps extends Omit<
  HTMLMotionProps<"div">,
  "children" | "variants"
> {
  /** Content animated when it enters the viewport. */
  children: React.ReactNode
  /** Hidden and visible animation states. */
  variants?: Variants
  /** Motion transition used between states. */
  transition?: Transition
  /** Intersection observer options such as `margin` and `amount`. */
  viewOptions?: UseInViewOptions
  /** Only play the entrance animation once. @default true */
  once?: boolean
  /** HTML element rendered by the component. @default "div" */
  as?: React.ElementType
}

/** Animates content when it enters the viewport. */
function InView({
  children,
  variants = defaultVariants,
  transition = { duration: 0.45, ease: "easeOut" },
  viewOptions,
  once = true,
  as = "div",
  ...props
}: InViewProps) {
  const ref = React.useRef<HTMLElement>(null)
  const reduceMotion = useReducedMotion()
  const isInView = useMotionInView(ref, { ...viewOptions, once })
  const MotionComponent = React.useMemo(() => motion.create(as), [as])

  return (
    <MotionComponent
      ref={ref}
      data-slot="in-view"
      initial="hidden"
      animate={reduceMotion || isInView ? "visible" : "hidden"}
      variants={variants}
      transition={reduceMotion ? { duration: 0 } : transition}
      {...props}
    >
      {children}
    </MotionComponent>
  )
}

export { InView }

属性 Props

属性类型默认值说明
childrenReact.ReactNode—受视口交叉状态触发进场动效的子内容。
variantsVariants{ hidden: { opacity: 0, y: 16 }, visible: { opacity: 1, y: 0 } }包含 hidden(初始离场状态)与 visible(进场状态)的 Motion 变体对象。
transitionTransition{ duration: 0.45, ease: "easeOut" }在 hidden 与 visible 状态之间过渡时所使用的动画参数。
viewOptionsUseInViewOptions—IntersectionObserver 配置选项,包含 `margin`(视口边距阈值,如 `-10% 0px`)和 `amount`(可见比例,如 0.5)。
oncebooleantrue是否仅在第一次进入视口时执行一次动画(为 false 时每次进出视口均重新触发)。
asReact.ElementType"div"渲染的 HTML 标签类型(如 `div`、`section`、`article` 等)。
classNamestring—应用于外层动画容器的 CSS 类名。

事件 Events

属性类型默认值说明
onAnimationStart() => void—进场动效开始执行时触发。
onAnimationComplete() => void—进场动效执行完成时触发。

使用场景与设计规范

InView 适用于落地页特性卡片流式呈现、数据图表首屏揭示、长文章图片渐进加载:

  • 避免页面重入重复闪烁:建议大多数业务展示使用默认的 once={true},保证用户向上回滚时不重复触发进场动效。
  • 配置视口提前量(margin):通过设置 viewOptions={{ margin: "-10% 0px" }},让元素在距离视口还有一定距离时就开始计算并自然进场,避免突兀。

场景示例

图片揭示

自定义 clipPath 与 scale 变体,让大图进入视口时自下而上揭开并从轻微放大回落:

Loading…

瀑布流/阶梯式卡片进场(Stagger)

为列表项赋予递增的 delay,配合模糊对焦变体实现阶梯式浮现,as="li" 保持列表语义:

Loading…

无障碍与交互 Accessibility

  • 减少动效环境处理:当系统启用 prefers-reduced-motion: reduce 时,组件保持与服务端一致的初始结构,挂载后立即以零时长切换到 visible 终态,不播放位移过程。
  • SEO 友好:组件服务端渲染(SSR)默认包含完整 DOM 树与文字内容,不会对搜索引擎爬虫造成不可见问题。