首页
/ Material UI Progress 组件实战指南:CircularProgress 与 LinearProgress 全解析

Material UI Progress 组件实战指南:CircularProgress 与 LinearProgress 全解析

2026-09-04 14:33:27作者:范靓好Udolf

本篇技术指南围绕 Material UI 的进度指示组件(Progress)展开,系统讲解 CircularProgressLinearProgress 的四种 variant(indeterminatedeterminatebufferquery)、value/min/max/valueBuffer 等核心参数的使用方式,以及自定义样式、延迟出现与无障碍配置。读完本文,你将掌握在不同业务场景(加载等待、文件上传、缓冲预取)下选型并落地进度条的完整方案,并理解其动画背后的 CSS 实现机制与性能限制。

进度指示器(Progress indicators,通常俗称 spinner)用于表达未定长的等待时间或展示流程的完成长度,典型场景包括应用加载、表单提交、更新保存等。按语义分为两类:

  • Determinate(确定型):显示操作还剩多久,需要真实进度值;
  • Indeterminate(不确定型):表达一段无法预估的等待时间。

官方文档 progress.md 中还强调了一个设计前提:组件动画尽可能依赖 CSS 实现,这样即使在 JavaScript 加载完成之前,动画也能正常工作。

CircularProgress:环形进度

不确定型(默认形态)

CircularProgress 不传任何 variant 时默认渲染不确定型 spinner(源码中 variant 默认值为 'indeterminate',见 CircularProgress.js)。文档示例如下(示例源文件为 CircularIndeterminate.tsx):

import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';

export default function CircularIndeterminate() {
  return (
    <Box sx={{ display: 'flex' }}>
      <CircularProgress aria-label="Loading…" />
    </Box>
  );
}

注意根元素渲染为带 role="progressbar"span,因此必须通过 aria-labelaria-labelledby 提供可访问名称(详见“无障碍”一节)。

从源码结构看,不确定型动画由两个 CSS keyframes 驱动:circularRotateKeyframe 对整个根元素做 1.4s 线性旋转,circularDashKeyframe 则对内部 circle 做 1.4s 的 ease-in-out 收缩动画(CircularProgress.js)。这解释了文档“动画依赖 CSS”的表述——动画定义在样式层,与 React 渲染循环解耦。

颜色与尺寸

color 支持主题调色板中的任意命名色(primarysecondaryerrorinfosuccesswarninginherit 等),默认为 'primary'size 接受数字(按像素处理)或带单位的字符串(如 '3rem'),默认为 40thickness 控制圆环粗细,默认为 3.6(见 CircularProgress.d.ts)。

尺寸示例(CircularSize.tsx)中即演示了 size={25}size="3rem"thickness={4} 的组合用法;颜色示例(CircularColor.tsx)则展示了 color="secondary" 等写法。

确定型与自定义刻度

指定操作加载进度时,使用 variant="determinate",并用 value 显示实际进度。示例(CircularDeterminate.tsx)展示了静态值 25/50/75/100 以及一个每 800ms 递增 10 的动态进度:

import * as React from 'react';
import Stack from '@mui/material/Stack';
import CircularProgress from '@mui/material/CircularProgress';

export default function CircularDeterminate() {
  const [progress, setProgress] = React.useState(0);

  React.useEffect(() => {
    const timer = setInterval(() => {
      setProgress((prevProgress) => (prevProgress >= 100 ? 0 : prevProgress + 10));
    }, 800);

    return () => {
      clearInterval(timer);
    };
  }, []);

  return (
    <Stack spacing={2} direction="row">
      <CircularProgress variant="determinate" value={25} aria-label="Export data" />
      <CircularProgress variant="determinate" value={50} aria-label="Export data" />
      <CircularProgress variant="determinate" value={75} aria-label="Export data" />
      <CircularProgress variant="determinate" value={100} aria-label="Export data" />
      <CircularProgress variant="determinate" value={progress} aria-label="Export data" />
    </Stack>
  );
}

默认情况下进度值期望落在 0–100 区间。通过 minmax 可以自定义刻度范围(min 默认 0、max 默认 100),例如电梯楼层这类非百分比场景:

<CircularProgress
  variant="determinate"
  min={10}
  max={20}
  value={progress}
  aria-label="Loading"
/>

(对应示例 CircularCustomScale.tsx

确定型的渲染原理值得深入一层:源码中内部 SVG 逻辑尺寸为常量 SIZE = 44determinate 分支会按 2 * Math.PI * ((SIZE - thickness) / 2) 计算圆周长,再通过 stroke-dashoffset = ((max - value) / range) * circumference 把进度值映射为虚线偏移,并给根元素加 transform: rotate(-90deg) 使起点位于 12 点方向(CircularProgress.js)。开发模式下,若 min/max/value 不满足 min < maxmin <= value <= max,会输出 console.error 警告;对 indeterminate 变体传入 min/max 也会收到“这些 props 不会生效”的警告(CircularProgress.js)。

常显轨道(enableTrackSlot)

默认环形进度没有背景轨道。传入 enableTrackSlot 属性会额外挂载一个 track slot,展示一圈半透明背景,且该轨道会复用 sizethickness 保持一致:

<CircularProgress enableTrackSlot size="30px" aria-label="Loading…" />
<CircularProgress enableTrackSlot variant="determinate" value={70} aria-label="Export data" />

(对应示例 CircularEnableTrack.tsx

从源码看,track 是一个 stroke: currentColorcircle,透明度取 theme.palette.action.activatedOpacityCircularProgress.js),并且带 aria-hidden="true" 避免辅助技术重复朗读。

与按钮、FAB 的交互集成

文档还给出了把 CircularProgressButtonFAB 组合成“点击触发加载态”的集成示例(CircularIntegration.tsx),典型模式是:点击按钮后把 spinner 替换进按钮内容区,等待请求完成后恢复。

显示数值标签

把视觉进度值与环形组件叠加显示时,用绝对定位的 Typography 盖在 spinner 上即可。官方示例 CircularWithValueLabel.tsx 的写法:

<CircularProgress
  variant="determinate"
  size={70}
  value={progress}
  aria-label="Loading photos"
  sx={{ position: 'absolute', left: '50%', ml: '-35px', top: '50%', mt: '-35px' }}
/>

即通过 left: 50% + 负 margin(半径)把圆形 spinner 精确居中到容器交点。

LinearProgress:条形进度

不确定型与 query 变体

LinearProgress 默认同样是不确定型进度条(LinearProgress.js)。其不确定动画由两段错峰的 left/right 位移 keyframes(indeterminate1Keyframe 与延迟 1.15s 的 indeterminate2Keyframe)组成,周期均为 2.1s(LinearProgress.js),这也是 Material Design 经典的“双条追逐”效果。

要反转不确定动画的滚动方向,使用 variant="query"。从源码结构看,queryindeterminate 共用两套 bar 动画,区别仅在于根元素额外应用了 transform: 'rotate(180deg)'LinearProgress.js)——这正是“反转方向”的实现方式。示例见 LinearIndeterminate.tsxLinearQuery.tsx

颜色用法与 CircularProgress 一致,color="primary"(默认)与 color="secondary" 之外还支持任意主题色,见 LinearColor.tsx

确定型与 buffer 缓冲变体

variant="determinate"value 即显示实际进度(LinearDeterminate.tsx)。variant="buffer" 则额外用 valueBuffer 显示一段“缓冲进度”,官方文档明确要求 valueBuffer 必须大于 value

<LinearProgress
  variant="buffer"
  value={progress}
  valueBuffer={buffer}
  aria-label="Loading…"
/>

示例 LinearBuffer.tsx 模拟了视频缓冲:真实进度每 100ms 加 1,缓冲值按随机步长领先增长,到达 100 后双双复位。

buffer 变体的渲染结构可以结合源码理解:根元素内最多渲染三层——Dashed(点状缓冲纹理层,带 3s 的 bufferKeyframe 透明度/背景位移动画)、Bar1(真实进度条)、Bar2(缓冲进度条,颜色取调色板色加亮 62% 的浅色调,见 getColorShade 函数,LinearProgress.js)。进度条的位移由 translateX(((value - min) / range) * 100 - 100%) 计算,且 RTL 环境下会自动取反(LinearProgress.js)。

开发模式的参数校验同样严格:

  • determinate/buffer 变体缺少 value:报错 “You need to provide a value prop…”;
  • buffer 变体缺少 valueBuffer:报错 “You need to provide a valueBuffer prop…”;
  • min/max/value/valueBuffer 不满足 min < maxmin <= value <= valueBuffer <= max:报错并给出实际收到的数值;
  • indeterminate/query 变体传入 min/max:警告“这些 props 不会生效”。

(校验逻辑见 LinearProgress.js

数值标签与自定义 aria-valuetext

进度 value 也可以显示在进度条旁边(LinearWithValueLabel.tsx),通常用一个 LinearProgressWithLabel 包装组件同时渲染 TypographyLinearProgress

当进度值不是百分比语义时(例如楼层数),默认辅助技术会按百分比朗读 aria-valuenow,此时应使用 aria-valuetext 提供准确描述。官方示例 LinearWithAriaValueText.tsx 的模式值得直接借鉴:

function LinearProgressWithLabelAndValue({ max, min, value, ...rest }) {
  const progressText = `Elevator at floor ${value} out of ${max}.`;
  const progressId = React.useId();
  return (
    <div>
      <Typography id={progressId} variant="body2" color="text.secondary" sx={{ mr: 1 }}>
        Elevator status
      </Typography>
      <Box sx={{ display: 'flex', alignItems: 'center' }}>
        <Box sx={{ width: '100%', mr: 1 }}>
          <LinearProgress
            variant="determinate"
            aria-labelledby={progressId}
            aria-valuetext={progressText}
            min={min}
            max={max}
            value={value}
            {...rest}
          />
        </Box>
        <Box sx={{ whiteSpace: 'nowrap' }}>
          <Typography variant="body2" sx={{ color: 'text.secondary' }}>
            {progressText}
          </Typography>
        </Box>
      </Box>
    </div>
  );
}

注意 aria-labelledby 指向可见文字标签的 id(这里用 React.useId() 生成,避免多实例冲突),aria-valuetext 承载完整语义。

自定义样式

官方文档的 Customization 章节(对应示例 CustomizedProgressBars.tsx)给出了三类典型定制,覆盖文档页 customization 所讲的 overrides 思路:

  1. 仿 Facebook 风格的圆形 spinner:用 disableShrink 关闭收缩动画、enableTrackSlot 显示轨道、animationDuration: '550ms' 加快旋转,并通过 sx 精确选择 circularProgressClasses.circlestrokeLinecap: 'round')与 circularProgressClasses.track(灰色描边)两个 slot:
<CircularProgress
  variant="indeterminate"
  disableShrink
  enableTrackSlot
  sx={(theme) => ({
    color: '#1a90ff',
    animationDuration: '550ms',
    [`& .${circularProgressClasses.circle}`]: {
      strokeLinecap: 'round',
    },
    [`& .${circularProgressClasses.track}`]: {
      opacity: 1,
      stroke: (theme.vars || theme).palette.grey[200],
    },
  })}
  size={40}
  thickness={4}
  aria-label="Loading…"
/>
  1. 渐变圆形进度:先渲染一个 0 尺寸 <svg> 定义 <linearGradient id="my_gradient">,再用 sx={{ 'svg circle': { stroke: 'url(#my_gradient)' } }} 把描边替换为渐变。

  2. 带圆角的条形进度:用 styled(LinearProgress) 覆盖 height/borderRadius,并通过 linearProgressClasses.colorPrimarylinearProgressClasses.bar 分别控制轨道底色(含深色模式分支)和进度条颜色:

const BorderLinearProgress = styled(LinearProgress)(({ theme }) => ({
  height: 10,
  borderRadius: 5,
  [`&.${linearProgressClasses.colorPrimary}`]: {
    backgroundColor: theme.palette.grey[200],
    ...theme.applyStyles('dark', { backgroundColor: theme.palette.grey[800] }),
  },
  [`& .${linearProgressClasses.bar}`]: {
    borderRadius: 5,
    backgroundColor: '#1a90ff',
    ...theme.applyStyles('dark', { backgroundColor: '#308fe8' }),
  },
}));

这些工具类名由 circularProgressClasses.tslinearProgressClasses.ts 统一生成:Circular 提供 rootsvgtrackcirclecircleDisableShrink 等 key;Linear 提供 rootdashedbarbar1bar2 及各 variant/color 组合 key,定制时应优先使用这些稳定类名而非自行选择 DOM。

延迟出现:1 秒响应阈值

文档指出,围绕响应时间有 3 个重要阈值。ButtonBase 的涟漪效果让用户感觉 UI 即时响应;通常 0.1s–1.0s 之间的延迟不需要额外反馈;超过 1.0s 后应显示 loader 以维持用户的思考流不断。因此最佳实践是“先等约 1 秒,确认任务确实耗时后再弹出进度条”。

官方示例 DelayingAppearance.tsxFade 过渡实现延迟:

<Fade
  in={loading}
  style={{ transitionDelay: loading ? '800ms' : '0ms' }}
  unmountOnExit
>
  <CircularProgress aria-label="Loading…" />
</Fade>

关键点:出现时给 transitionDelay 设 800ms(若加载很快则 spinner 始终不出现),消失时延迟设为 0ms(立即收起),unmountOnExit 保证不占用布局空间。示例中第二个按钮进一步模拟了“查询”场景:点击后进入 progress 状态延迟 800ms 出现 spinner,2 秒后转为 success 并展示 “Success!” 文本。

无障碍要求

进度条必须通过以下两种方式之一获得可访问名称:

  • 设置 aria-labelledby,指向可见文本标签的 id(推荐,标签同时向视觉用户解释进度含义);
  • 或使用 aria-label 属性。

组件源码中,determinate/buffer 变体还会自动输出 aria-valuenowaria-valueminaria-valuemaxCircularProgress.jsLinearProgress.js)。组件类型定义中另补充了一条区域级建议:当进度条描述的是页面某个区域的加载状态时,应在该区域上设置 aria-busy="true",并用 aria-describedby 指向该进度条,加载完成后再移除。

已知限制与规避方案

高 CPU 负载下的动画异常

文档明确警告:在主线程承受重负载时,可能丢失 stroke dash 收缩动画,或看到 CircularProgress 环宽随机抖动(文档中附有演示视频)。原因是动画依赖合成器与主线程的协作,而重计算会阻塞渲染。官方建议是把 CPU 密集操作放入 Web Worker 或分批执行,避免阻塞主渲染线程。

当确实无法做到时,可以用 disableShrink 属性缓解问题——它直接移除收缩动画,只保留旋转动画(源码中 disableShrink 仅在 variant="indeterminate" 时生效,对其他变体传入会收到 PropTypes 警告,见 CircularProgress.js):

<CircularProgress disableShrink aria-label="Loading…" />

(对应示例 CircularUnderLoad.tsx

高频更新的 200ms 延迟

LinearProgress 使用 CSS transform 的过渡来平滑不同 value 之间的变化,默认过渡时长为 200ms。如果父组件更新 value 的频率过快,渲染更新与进度条完全到位之间至少会存在 200ms 的视觉延迟。

若需要达到每秒 30 次以上重渲染(约 33ms/帧),文档建议直接禁用过渡:

.MuiLinearProgress-bar {
  transition: none;
}

这一点与源码吻合:bar1/bar2 的基础样式中由 getTransitionStyles(theme, 'transform', { duration: '0.2s', easing: 'linear' }) 注入了 0.2s 的 transform 过渡(LinearProgress.js),而 determinate/buffer 变体又用 .${TRANSITION_DURATION}s(即 0.4s 的 TRANSITION_DURATION = 4 秒级数值拼出)覆盖——高频数据流场景下,通过全局 CSS 或 sx 移除该 transition 是最直接的取舍。

小结

Material UI 的 Progress 组件族以 CSS 动画为底座、以 variant 参数为语义开关,覆盖了“等待”(indeterminate)、“进行中”(determinate)、“预取缓冲”(buffer)、“反向查询”(query)四类状态。实际项目中的关键决策点可以归纳为:

场景 组件 / variant 关键 props
未知时长的加载等待 CircularProgress / LinearProgress variant="indeterminate"(默认)
有真实进度的操作 CircularProgress / LinearProgress variant="determinate" + value(配合 min/max 自定义刻度)
视频/文件缓冲 LinearProgress variant="buffer" + value + valueBuffer(须 valueBuffer > value
常量轨道的 spinner CircularProgress enableTrackSlot
高负载环境 CircularProgress disableShrink
快于 30 帧/秒的更新 LinearProgress 覆盖 .MuiLinearProgress-bar { transition: none; }

配合“延迟 1 秒再显示”的交互策略与 aria-label/aria-labelledby/aria-valuetext 的无障碍配置,即可在任意 React 应用中落地既流畅又合规的进度反馈。所有示例代码均可在仓库 docs/data/material/components/progress/ 目录中逐一对应到源文件,组件实现位于 packages/mui-material/src/CircularProgress/packages/mui-material/src/LinearProgress/,其单元测试分别位于 CircularProgress.test.jsLinearProgress.test.js

登录后查看全文
热门项目推荐
相关项目推荐