wui
组件

动效 Motion

基于 motion 构建的可选动画基础组件和预设库。

基础示例

I slide up and fade in when I mount.
pnpm dlx wui@latest add @wui/motion
components/ui/motion.tsx
"use client"

import * as React from "react"
import { Slot } from "radix-ui"
import {
  motion,
  useReducedMotion,
  type HTMLMotionProps,
  type Transition,
  type Variants,
} from "motion/react"

export type MotionPreset =
  | "fade"
  | "scale"
  | "slide-up"
  | "slide-down"
  | "slide-left"
  | "slide-right"
  | "pop"
  | "blur"

/** Enter animations, expressed as `hidden` → `visible` variants. */
export const motionPresets: Record<MotionPreset, Variants> = {
  fade: { hidden: { opacity: 0 }, visible: { opacity: 1 } },
  scale: {
    hidden: { opacity: 0, scale: 0.95 },
    visible: { opacity: 1, scale: 1 },
  },
  "slide-up": {
    hidden: { opacity: 0, y: 12 },
    visible: { opacity: 1, y: 0 },
  },
  "slide-down": {
    hidden: { opacity: 0, y: -12 },
    visible: { opacity: 1, y: 0 },
  },
  "slide-left": {
    hidden: { opacity: 0, x: 12 },
    visible: { opacity: 1, x: 0 },
  },
  "slide-right": {
    hidden: { opacity: 0, x: -12 },
    visible: { opacity: 1, x: 0 },
  },
  pop: {
    hidden: { opacity: 0, scale: 0.8 },
    visible: { opacity: 1, scale: 1 },
  },
  blur: {
    hidden: { opacity: 0, filter: "blur(6px)" },
    visible: { opacity: 1, filter: "blur(0px)" },
  },
}

/** Reusable transition presets. */
export const motionTransitions = {
  smooth: { duration: 0.3, ease: "easeOut" },
  spring: { type: "spring", stiffness: 400, damping: 30, mass: 0.7 },
  snappy: { type: "spring", stiffness: 700, damping: 35 },
} satisfies Record<string, Transition>

export type MotionTransition = keyof typeof motionTransitions

const MotionSlot = motion.create(Slot.Root)

export interface MotionProps
  extends Omit<HTMLMotionProps<"div">, "variants" | "transition"> {
  /** Named enter animation. @default "fade" */
  preset?: MotionPreset
  /** A named transition preset or a custom motion Transition. @default "smooth" */
  transition?: MotionTransition | Transition
  /** Delay before animating, in seconds. */
  delay?: number
  /** Animate when scrolled into view instead of on mount. @default false */
  inView?: boolean
  /** Only animate the first time it enters the viewport. @default true */
  once?: boolean
  /** Merge motion onto the single child element instead of rendering a div. */
  asChild?: boolean
}

/**
 * A small opt-in wrapper that animates its children with a named preset.
 * Respects `prefers-reduced-motion` (renders statically when set). Use
 * `asChild` to animate an existing element (e.g. a Button) without an extra
 * wrapper node.
 */
function Motion({
  preset = "fade",
  transition = "smooth",
  delay,
  inView = false,
  once = true,
  asChild = false,
  ...props
}: MotionProps) {
  const reduceMotion = useReducedMotion()
  const Comp = (asChild ? MotionSlot : motion.div) as React.ElementType

  if (reduceMotion) {
    return <Comp {...props} />
  }

  const resolvedTransition =
    typeof transition === "string" ? motionTransitions[transition] : transition

  const activation = inView
    ? { initial: "hidden", whileInView: "visible", viewport: { once } }
    : { initial: "hidden", animate: "visible" }

  return (
    <Comp
      data-slot="motion"
      variants={motionPresets[preset]}
      transition={{ ...resolvedTransition, delay }}
      {...activation}
      {...props}
    />
  )
}

export { Motion }

组件作用

Motion 是 wui 的可选动画层。你可以包裹任意内容,为其添加自然的入场动画;也可以使用 asChild 为现有组件(如 Button)添加动画,而不增加额外的包装节点。组件内置一组命名 presetstransitions,以确保应用中的动画风格一致。当用户偏好减少动态效果时, 它会自动禁用动画

由于 Motion 是独立的 registry 条目,不使用动画的组件不会引入相关依赖;只有需要动画时 才会拉取 motion

组件属性

PropTypeDefaultDescription
preset"fade" | "scale" | "slide-up" | "slide-down" | "slide-left" | "slide-right" | "pop" | "blur"fadeNamed enter animation.
transitionTransition | "smooth" | "spring" | "snappy"smoothA named transition preset or a custom motion Transition.
delaynumberDelay before animating, in seconds.
inViewbooleanfalseAnimate when scrolled into view instead of on mount.
oncebooleantrueOnly animate the first time it enters the viewport.
asChildbooleanfalseMerge motion onto the single child element instead of rendering a div.

它还接受 motion.div 的所有属性,例如 whileHoverwhileTapstyle

事件

PropTypeDefaultDescription
onAnimationStart(definition) => void动画开始时触发。
onAnimationComplete(definition) => void动画结束时触发。

扩展使用

动画预设

Loading…

滚动进入视口时播放(inView

Loading…

为现有组件添加动画(asChild

Loading…

减少动态效果

Motion 会读取 prefers-reduced-motion,当用户要求减少动态效果时静态渲染子元素, 无需额外处理。