数字递增

关键数字进入视野时从零开始增长,最后缓缓停下,让最终的数值落得有分量。

  • GSAP
  • 文字
  • 进场
  • 收录于
12,480已入驻团队

AI 提示词

用 GSAP 制作一个数字递增的统计数据。把一个普通对象 { n: 0 } 在 1.6s 内补间到目标值 12480,缓动为 "power3.out",每次更新时把 Math.round(n) 用 toLocaleString() 格式化后写入数字元素。当 IntersectionObserver 检测到统计数字可见 50% 时开始补间,并添加一个“重播”按钮重新开始。数字使用 tabular-nums,避免计数时宽度抖动。把最终数值放在 aria-label 中,让屏幕阅读器只读一个数字而不是每一帧。在 prefers-reduced-motion 下直接显示最终数值。组件卸载时 revert GSAP context。说明文字为“已入驻团队”。

源代码

Demo.tsx
'use client'

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

export default function CountUp({ labels }: { labels: Record<string, string> }) {
  const num = useRef<HTMLSpanElement>(null)
  const tween = useRef<gsap.core.Tween | null>(null)
  const target = Number(labels.value)

  useEffect(() => {
    const el = num.current
    if (!el) return
    const show = (n: number) => (el.textContent = Math.round(n).toLocaleString())
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return void show(target)
    const state = { n: 0 }
    show(0)
    const ctx = gsap.context(() => {
      tween.current = gsap.to(state, {
        n: target,
        duration: 1.6,
        ease: 'power3.out',
        paused: true,
        onUpdate: () => show(state.n),
      })
    })
    const io = new IntersectionObserver(
      ([e]) => {
        if (!e.isIntersecting) return
        tween.current?.play()
        io.disconnect()
      },
      { threshold: 0.5 },
    )
    io.observe(el)
    return () => {
      io.disconnect()
      ctx.revert()
    }
  }, [target])

  return (
    <div className={s.wrap}>
      <span ref={num} className={s.num} aria-label={target.toLocaleString()}>
        {target.toLocaleString()}
      </span>
      <span className={s.caption}>{labels.caption}</span>
      <button type="button" className={s.replay} onClick={() => tween.current?.restart()}>
        {labels.replay}
      </button>
    </div>
  )
}
demo.module.css
.wrap {
  display: grid;
  justify-items: center;
  gap: 6px;
}

.num {
  font: 700 clamp(2.4rem, 8vw, 3.2rem) / 1 var(--font-display);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  color: var(--stage-ink);
}

.caption {
  margin-bottom: 10px;
  font: 500 var(--text-xs) / 1 var(--font-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--stage-muted);
}

.replay {
  height: 32px;
  padding: 0 12px;
  border: 1px solid var(--stage-rule);
  border-radius: var(--radius-sm);
  background: var(--stage-surface);
  color: var(--stage-ink);
  font: 500 var(--text-sm) / 1 var(--font-body);
  white-space: nowrap;
  cursor: pointer;
}

.replay:hover { border-color: var(--stage-accent); }
.replay:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

投稿创意

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

你想投稿的是?

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