环形进度

圆环填充到新的数值,中间的数字同步向上计数。

  • JS
  • SVG 与图形
  • 点击
  • 收录于
0%
资料完成度

AI 提示词

用 SVG 为 React 制作环形进度条。在 120×120 的 viewBox 中画两个 r = 52 的圆:一个灰色调的轨道,以及一个 stroke-dasharray 等于周长(2πr)、stroke-dashoffset = 周长 × (1 − 数值) 的进度圆。把进度圆旋转 −90°,让它从 12 点钟方向开始,并使用 stroke-linecap: round。用 requestAnimationFrame 在 900ms 内以 ease-out 三次曲线从旧值动画到新值,每一帧同时更新 dashoffset 和中间的百分比文字(tabular-nums)。以 role="progressbar" 暴露组件,设置 aria-valuenow、aria-valuemin 0、aria-valuemax 100,以及 aria-label“资料完成度”。添加一个按钮随机设置新数值。在 prefers-reduced-motion 下直接跳到新数值。

源代码

Demo.tsx
'use client'

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

const R = 52
const C = 2 * Math.PI * R

export default function CircularProgress({ labels }: { labels: Record<string, string> }) {
  const [shown, setShown] = useState(0)
  const [goal, setGoal] = useState(0)
  const target = useRef(0)
  const frame = useRef(0)

  const animateTo = (next: number) => {
    cancelAnimationFrame(frame.current)
    const from = target.current
    target.current = next
    setGoal(next)
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return setShown(next)
    const t0 = performance.now()
    const tick = (now: number) => {
      const t = Math.min((now - t0) / 900, 1)
      setShown(from + (next - from) * (1 - (1 - t) ** 3))
      if (t < 1) frame.current = requestAnimationFrame(tick)
    }
    frame.current = requestAnimationFrame(tick)
  }

  useEffect(() => {
    // start on the next frame so the ring animates in from 0 after mount
    frame.current = requestAnimationFrame(() => animateTo(72))
    return () => cancelAnimationFrame(frame.current)
  }, [])

  const pct = Math.round(shown)

  return (
    <div className={s.wrap}>
      <div className={s.ring} role="progressbar" aria-valuenow={goal} aria-valuemin={0} aria-valuemax={100} aria-label={labels.label}>
        <svg viewBox="0 0 120 120" aria-hidden="true">
          <circle className={s.track} cx="60" cy="60" r={R} />
          <circle className={s.bar} cx="60" cy="60" r={R} strokeDasharray={C} strokeDashoffset={C * (1 - shown / 100)} />
        </svg>
        <span className={s.value}>{pct}%</span>
      </div>
      <span className={s.label}>{labels.label}</span>
      <button type="button" className={s.button} onClick={() => animateTo(10 + Math.floor(Math.random() * 90))}>
        {labels.again}
      </button>
    </div>
  )
}
demo.module.css
.wrap { display: grid; justify-items: center; gap: 8px; }

.ring { position: relative; width: 120px; height: 120px; }
.ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }

.track, .bar { fill: none; stroke-width: 10; }
.track { stroke: var(--stage-rule); }
.bar { stroke: var(--stage-accent); stroke-linecap: round; }

.value {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font: 600 var(--text-2xl) / 1 var(--font-display);
  font-variant-numeric: tabular-nums;
  color: var(--stage-ink);
}

.label { font-size: var(--text-xs); color: var(--stage-muted); }

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

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

投稿创意

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

你想投稿的是?

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