组件
进度条 Progress
细腻克制的线性与环形进度指示器,支持确定性百分比与不确定动态状态。
基础用法
最基础的进度条用法。传入 value 展示任务当前的完成百分比;数值变化时进度以无回弹的弹簧曲线平滑过渡:
Loading…
安装与引入
通过 CLI 自动添加组件,或手动复制源码至项目中:
pnpm dlx @wui-design/cli@latest add @wui/progress安装基础依赖与 Radix 原语
pnpm add radix-ui motion class-variance-authority clsx tailwind-merge复制组件源码到
components/ui/progress.tsx"use client"
import * as React from "react"
import { Progress as ProgressPrimitive } from "radix-ui"
import {
motion,
useReducedMotion,
useSpring,
useTransform,
type MotionValue,
} from "motion/react"
import { cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
const progressVariants = cva("relative", {
variants: {
variant: {
linear: "w-full overflow-hidden rounded-full bg-muted",
circular: "inline-flex shrink-0 items-center justify-center",
},
size: {
sm: "",
default: "",
lg: "",
},
},
compoundVariants: [
{ variant: "linear", size: "sm", className: "h-1" },
{ variant: "linear", size: "default", className: "h-1.5" },
{ variant: "linear", size: "lg", className: "h-2" },
{ variant: "circular", size: "sm", className: "size-8" },
{ variant: "circular", size: "default", className: "size-11" },
{ variant: "circular", size: "lg", className: "size-14" },
],
defaultVariants: { variant: "linear", size: "default" },
})
const progressColor = {
primary: "text-primary",
blue: "text-info",
success: "text-success",
warning: "text-warning",
destructive: "text-destructive",
} as const
const valueTextSize = {
sm: "text-[9px]",
default: "text-[11px]",
lg: "text-xs",
} as const
/** Settles value changes quickly without overshooting past the real progress. */
const valueSpring = { stiffness: 220, damping: 32, mass: 0.8 }
const sweepEase = [0.65, 0, 0.35, 1] as const
export interface ProgressProps
extends Omit<React.ComponentProps<typeof ProgressPrimitive.Root>, "color"> {
/** Layout of the progress indicator. @default "linear" */
variant?: "linear" | "circular"
/** Thickness preset for linear progress and diameter preset for circular progress. @default "default" */
size?: "sm" | "default" | "lg"
/** Semantic accent used by the completed portion. @default "primary" */
color?: keyof typeof progressColor
/** Shows the current percentage inside a circular indicator. The number rolls with the bar. @default false */
showValue?: boolean
/** Extra classes applied to the completed portion. */
indicatorClassName?: string
}
function useSmoothedPercentage(percentage: number) {
const reduceMotion = useReducedMotion()
const smoothed = useSpring(percentage, valueSpring)
React.useEffect(() => {
if (reduceMotion) smoothed.jump(percentage)
else smoothed.set(percentage)
}, [percentage, reduceMotion, smoothed])
return smoothed
}
/** A linear or circular progress indicator with determinate and indeterminate states. */
function Progress({
className,
indicatorClassName,
value,
max = 100,
variant = "linear",
size = "default",
color = "primary",
showValue = false,
...props
}: ProgressProps) {
const indeterminate = value == null
const percentage = indeterminate
? 0
: Math.min(100, Math.max(0, (value / max) * 100))
const smoothed = useSmoothedPercentage(percentage)
return (
<ProgressPrimitive.Root
data-slot="progress"
data-size={size}
data-variant={variant}
data-color={color}
className={cn(
progressVariants({ variant, size }),
progressColor[color],
className
)}
value={value}
max={max}
{...props}
>
{variant === "circular" ? (
<CircularIndicator
size={size}
indeterminate={indeterminate}
smoothed={smoothed}
showValue={showValue}
className={indicatorClassName}
/>
) : (
<LinearIndicator
indeterminate={indeterminate}
smoothed={smoothed}
className={indicatorClassName}
/>
)}
</ProgressPrimitive.Root>
)
}
function LinearIndicator({
indeterminate,
smoothed,
className,
}: {
indeterminate: boolean
smoothed: MotionValue<number>
className?: string
}) {
const reduceMotion = useReducedMotion()
const x = useTransform(smoothed, (latest) => `${latest - 100}%`)
if (indeterminate) {
return (
<ProgressPrimitive.Indicator key="indeterminate" asChild>
<motion.div
data-slot="progress-indicator"
className={cn(
"absolute inset-y-0 left-0 w-2/5 rounded-full bg-current",
className
)}
initial={{ x: reduceMotion ? "75%" : "-100%" }}
animate={reduceMotion ? { opacity: 0.6 } : { x: "250%" }}
transition={
reduceMotion
? { duration: 0 }
: {
duration: 1.35,
ease: sweepEase,
repeat: Infinity,
repeatDelay: 0.15,
}
}
/>
</ProgressPrimitive.Indicator>
)
}
return (
<ProgressPrimitive.Indicator key="determinate" asChild>
<motion.div
data-slot="progress-indicator"
className={cn("h-full w-full rounded-full bg-current", className)}
style={{ x }}
/>
</ProgressPrimitive.Indicator>
)
}
function CircularIndicator({
size,
indeterminate,
smoothed,
showValue,
className,
}: {
size: NonNullable<ProgressProps["size"]>
indeterminate: boolean
smoothed: MotionValue<number>
showValue: boolean
className?: string
}) {
const reduceMotion = useReducedMotion()
const strokeWidth = size === "sm" ? 9 : size === "lg" ? 7 : 8
const radius = 50 - strokeWidth / 2
const circumference = 2 * Math.PI * radius
const dashOffset = useTransform(
smoothed,
(latest) => circumference * (1 - latest / 100)
)
const label = useTransform(smoothed, (latest) => Math.round(latest))
return (
<>
<svg
data-slot="progress-ring"
className={cn(
"size-full -rotate-90",
indeterminate &&
"animate-spin [animation-duration:1.1s] motion-reduce:animate-none"
)}
viewBox="0 0 100 100"
aria-hidden="true"
>
<circle
cx="50"
cy="50"
r={radius}
fill="none"
stroke="currentColor"
strokeOpacity="0.14"
strokeWidth={strokeWidth}
/>
<ProgressPrimitive.Indicator
key={indeterminate ? "indeterminate" : "determinate"}
asChild
>
{indeterminate ? (
<motion.circle
data-slot="progress-indicator"
className={className}
cx="50"
cy="50"
r={radius}
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
initial={{ strokeDashoffset: circumference * 0.78 }}
animate={
reduceMotion
? undefined
: {
strokeDashoffset: [
circumference * 0.78,
circumference * 0.3,
circumference * 0.78,
],
}
}
transition={{
duration: 1.6,
ease: "easeInOut",
repeat: Infinity,
}}
/>
) : (
<motion.circle
data-slot="progress-indicator"
className={className}
cx="50"
cy="50"
r={radius}
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
style={{ strokeDashoffset: dashOffset }}
/>
)}
</ProgressPrimitive.Indicator>
</svg>
{showValue && !indeterminate ? (
<motion.span
data-slot="progress-value"
className={cn(
"absolute font-semibold tabular-nums text-foreground",
valueTextSize[size]
)}
>
{label}
</motion.span>
) : null}
</>
)
}
export { Progress, progressVariants }
属性 Props
Progress 支持以下核心配置属性:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| value | number | null | — | 当前进度值(0 至 max 之间)。传入 null 时呈现不确定加载状态(Indeterminate)。 |
| max | number | 100 | 进度条的最大量程基准值。 |
| variant | "linear" | "circular" | "linear" | 进度条的形态布局。linear 为水平线形条,circular 为矢量环形圆环。 |
| size | "sm" | "default" | "lg" | "default" | 尺寸规格。对于线性条控制轨道高度(sm: 4px, default: 6px, lg: 8px);对于环形控制直径。 |
| color | "primary" | "blue" | "success" | "warning" | "destructive" | "primary" | 已完成进度的语义色彩主题。 |
| showValue | boolean | false | 在环形进度(circular)中心是否渲染百分比数字文本,数字与圆环同步滚动变化。 |
| indicatorClassName | string | — | 应用于进度指示条/圆弧的额外 CSS 类名。 |
| className | string | — | 应用于外层容器的额外 CSS 类名。 |
事件 Events
Progress 属于只读展示类状态组件,不直接派发自定义用户交互事件。数值由业务状态驱动更新,进度条会自动同步计算无障碍属性与动画补间:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| onClick | (event: React.MouseEvent<HTMLDivElement>) => void | — | 在进度条容器上点击时触发的原生 DOM 事件。 |
使用场景与设计规范
Progress 用于明确告知用户某一异步任务的实际进展或资源占用情况。
- 确定性(Determinate)vs 不确定性(Indeterminate):
- 确定性进度(
value={number}):适用于任务总耗时或总数据量已知(如已下载 35MB / 共 100MB)。 - 不确定性进度(
value={null}):适用于任务正在后台进行但无法预估具体剩余量(如正在建立安全加密握手),此时线性进度条以往返扫过的光带、环形进度以旋转并伸缩的弧线表达等待。
- 确定性进度(
- 色彩与语义阈值规范:
primary/blue:常规进行中的业务流程(如下载、更新)。success:任务成功完成或系统健康度处于良好区间。warning:资源使用量接近警戒水位(如存储占用超过 80%)。destructive:流程发生错误截断或资源配额严重超限。
- 形态选型建议:
- 水平线性(Linear):适合页面顶部加载条、卡片列表项或文件上传列表。
- 环形(Circular):适合仪表盘小部件、紧凑指标卡片或头像周围的状态环。
场景示例
语义色彩与环形形态
支持多种业务语义颜色配置,环形模式下可开启 showValue 展示中心百分比:
Loading…
尺寸规格对比
线性轨道与环形圆环均提供 sm、default 与 lg 三种尺寸:
Loading…
动态任务流(文件上传)
在实际业务流程中,通过状态驱动进度条增长,并在各阶段切换颜色与图标:
Loading…
不确定状态与阶段切换
任务无法预估耗时时传入 value={null},拿到总量后再切换为具体数值,完成时切换为 success 颜色:
Loading…
仪表盘资源监控看板
组合多个小型进度条监控服务器 CPU、存储与系统 SLA 健康度:
Loading…
无障碍与交互 Accessibility
- ARIA 标准规范:底层基于 Radix UI 原语,根元素挂载
role="progressbar",并自动同步计算aria-valuenow、aria-valuemin与aria-valuemax。 - 平滑动画与动效降级:进度值由
motion弹簧驱动,线性轨道与环形描边、中心数字保持同步;检测到prefers-reduced-motion时直接跳转到最新百分比,不确定状态也会停止循环动画。 - 读屏器文案提示:建议在未展示明确可见标题时,为组件添加
aria-label(如aria-label="数据同步进度")。