首页
/ Material UI Skeleton 组件深度解析:骨架屏原理、变体、动画与尺寸推断

Material UI Skeleton 组件深度解析:骨架屏原理、变体、动画与尺寸推断

2026-09-04 09:35:13作者:咎岭娴Homer

Skeleton(骨架屏)是 Material UI(MUI)用于在数据加载完成前展示内容占位预览的组件,通过模拟最终页面布局来降低用户等待时的挫败感,提升页面的感知响应速度。阅读本文后,你将掌握 Skeleton 组件的四种形状变体(text/circular/rectangular/rounded)、三种动画模式(pulse/wave/关闭)的完整用法,理解其尺寸推断(children 推断与排版推断)的底层实现机制,并能从源码层面解释骨架屏颜色、圆角、动画帧的实现原理与无障碍注意事项。

使用场景:用骨架屏替代加载旋转器

组件数据可能无法立即可用。在数据到达之前渲染一个与最终布局形状一致的占位块,能让用户感觉"事情正在立刻发生",信息随后逐步呈现到屏幕上。官方文档 skeleton.md 明确指出,骨架屏是传统 spinner(旋转加载器)的替代方案:它不展示抽象的动画小部件,而是通过"预示即将出现的内容"来降低认知负载。

该组件被设计为直接内嵌在你的业务组件中使用,典型的加载态切换写法如下(源自文档 Usage 章节):

{
  item ? (
    <img
      style={{
        width: 210,
        height: 118,
      }}
      alt={item.title}
      src={item.src}
    />
  ) : (
    <Skeleton variant="rectangular" width={210} height={118} />
  );
}

更完整的实战示例可以参考仓库中的 YouTube 卡片加载演示 YouTube.tsx:它用 loading ? Array.from(new Array(3)) : data 构造一个与真实数据等长的数组,加载时渲染 <Skeleton variant="rectangular" width={210} height={118} /> 加两行文字骨架(<Skeleton /><Skeleton width="60%" />),数据到达后无缝切换为 img + Typography,占位区域与内容区域尺寸完全一致,避免了布局跳动(CLS)。

四种形状变体(variant)

组件支持 4 种形状变体,由 variant prop 控制(默认 text),类型定义为 OverridableStringUnion<'text' | 'rectangular' | 'rounded' | 'circular', ...>,见 Skeleton.d.ts

  • text(默认):表示单行文本。高度不是靠 height 控制,而是通过字号(font size)调整——在排版上下文中它使用 em 单位自适应父级字体;
  • circular:正圆(头像等场景);
  • rectangular:直角矩形,完全无圆角,尺寸由你接管;
  • rounded:带主题圆角的矩形,尺寸同样由你控制。

官方示例 Variants.tsx 展示了四种变体的标准写法:

<Stack spacing={1}>
  {/* For variant="text", adjust the height via font-size */}
  <Skeleton variant="text" sx={{ fontSize: '1rem' }} />

  {/* For other variants, adjust the size with `width` and `height` */}
  <Skeleton variant="circular" width={40} height={40} />
  <Skeleton variant="rectangular" width={210} height={60} />
  <Skeleton variant="rounded" width={210} height={60} />
</Stack>

源码中的变体样式实现

从源码结构看,四种变体分别映射为 CSS 变体(variants),定义在 Skeleton.js

  • textheight: 'auto',配合 transform: scale(1, 0.60) 纵向压缩为一条文本条;圆角通过主题 shape.borderRadius 计算为 radius / 0.6(补偿纵向压缩后的视觉比例),并且 &:empty:before { content: '"\00a0"' } 用一个不间断空格保证空 span 也有渲染高度。这也是为什么文档说 text 变体"通过字号调整高度"——它继承父级 font-size,基准高度为 1.2em(见 Skeleton.js 第 111 行);
  • circularborderRadius: '50%'
  • roundedborderRadius 取主题 shape.borderRadius
  • rectangular:无额外圆角样式(默认 0)。

每个变体对应一个 utility class(MuiSkeleton-textMuiSkeleton-circular 等),由 skeletonClasses.ts 统一生成,可通过 classes prop 覆盖。

动画模式(animation)

默认情况下骨架屏做**脉冲(pulsate)**动画,也可以改为波浪(wave)或完全关闭动画。示例 Animations.tsx

<Box sx={{ width: 300 }}>
  <Skeleton />                {/* 默认 pulse */}
  <Skeleton animation="wave" />
  <Skeleton animation={false} />
</Box>

animation prop 的取值类型为 'pulse' | 'wave' | false(见 Skeleton.d.ts 第 11-16 行)。

脉冲动画实现

pulse 是一个 2 秒的透明度往复关键帧动画,源码中的关键帧定义在 Skeleton.js 第 30-42 行

@keyframes pulse {
  0%   { opacity: 1; }
  50%  { opacity: 0.4; }
  100% { opacity: 1; }
}
/* animation: 2s ease-in-out 0.5s infinite */

即骨架块在 0.5 秒延迟后,以 ease-in-out 曲线无限循环地在完全不透明与 40% 透明度之间往复。

波浪动画实现

wave 动画的实现更巧妙:它在根元素上设置 position: relative; overflow: hidden,并通过 ::after 伪元素铺设一条从左到右扫过的横向渐变光带(渐变色取 theme.palette.action.hover)。关键帧定义在 Skeleton.js 第 44-57 行

@keyframes wave {
  0%   { transform: translateX(-100%); }
  50%  { transform: translateX(100%); } /* +0.5s of delay between each loop */
  100% { transform: translateX(100%); }
}

细节上有两个工程考量(见 Skeleton.js 第 185-209 行):

  • WebkitMaskImage: '-webkit-radial-gradient(white, black)' 用于修复 Safari 的一个渲染 bug;
  • ::after 初始 transform: translateX(-100%),注释标明目的是"Avoid flash during server-side hydration"——避免服务端渲染首帧时出现闪烁。

Facebook 风格卡片演示 Facebook.tsx 展示了 wave 动画在真实卡片中的综合运用:头像位置用 <Skeleton animation="wave" variant="circular" width={40} height={40} />,标题/副标题用 height={10} 的细条,正文媒体区用 <Skeleton sx={{ height: 190 }} animation="wave" variant="rectangular" />

减少动态效果(reduced motion)支持

源码中通过 getReducedMotionStyles(来自 transitions/utils)读取主题的 transitions.disable 设置:当用户偏好减少动态效果时,pulse 会置为 animation: nonewave 则会同时关闭 ::after 的动画并隐藏光带(见 Skeleton.js 第 97-103 行)。这意味着骨架屏动画会自动尊重操作系统的"减弱动态效果"偏好。

尺寸推断:width/height 之外的两种取巧方式

widthheight prop(支持 number | string,最终写入内联 style,见 Skeleton.js 第 266-270 行)外,组件还支持两种尺寸推断。

从排版推断(typography-based sizing)

对文本场景,由于 text 变体高度用 em 单位表达,骨架可以直接作为 Typography 的子元素,高度自动跟随排版规格:

<Typography variant="h1">{loading ? <Skeleton /> : 'h1'}</Typography>

演示 SkeletonTypography.tsxh1h3body1caption 四个规格做了对比:加载态渲染 <Skeleton />,完成态渲染规格名,可以直观看到骨架条高度随字号逐级变化。文档同时提醒:从排版推断尺寸时应保持 variant="text"(这正是默认值),如果只需要换形状而保留排版尺寸,用 sx 微调 borderRadius 即可,而不是改用其他变体。

从 children 推断(children-based sizing)

对其他组件,你可能不想重复声明宽高。此时把真实组件作为 children 传入,Skeleton 会渲染它并据此推断宽高——子元素本身会被隐藏:

loading ? (
  <Skeleton variant="circular">
    <Avatar />
  </Skeleton>
) : (
  <Avatar src={data.avatar} />
);

演示 SkeletonChildren.tsx 还展示了两种实用技巧:

// 宽度撑满父容器:
<Skeleton width="100%">
  <Typography>.</Typography>
</Skeleton>

// 用 padding-top 比例占位符推断图片宽高比(16:9):
<Skeleton variant="rectangular" width="100%">
  <div style={{ paddingTop: '57%' }} />
</Skeleton>

源码中的 children 推断机制

从源码结构看,children 推断由三组条件样式协同完成(Skeleton.js 第 147-166 行),并以 hasChildren(即 Boolean(other.children),见 第 254 行)作为开关:

  1. & > * { visibility: 'hidden' }:子元素照常参与布局(撑起宽高),但视觉上不可见;
  2. 有 children 且未指定 width 时 → maxWidth: 'fit-content',让骨架宽度贴合内容;
  3. 有 children 且未指定 height 时 → height: 'auto',覆盖默认的 1.2em 基准高度。

对应的 utility classes 是 withChildrenfitContentheightAutoskeletonClasses.ts 第 19-24 行)。Skeleton.test.js 中的用例(如 width="100" + children、height="100" + children 的组合)验证了 prop 显式指定时会覆盖推断行为。

注意一个语义细节:width/height 是写在内联 style 上的,优先级高于推断样式。因此 <Skeleton width="100%"><Typography>.</Typography></Skeleton> 中骨架宽度是 100% 而非内容宽度。

颜色定制与深色背景

骨架屏颜色可以通过修改 background-color CSS 属性定制(官方推荐用 sx prop),在黑色背景上尤其有用,否则骨架屏会"隐形"。示例 SkeletonColor.tsx

<Box sx={{ bgcolor: '#121212', p: 8, width: '100%', display: 'flex', justifyContent: 'center' }}>
  <Skeleton
    sx={{ bgcolor: 'grey.900' }}
    variant="rectangular"
    width={210}
    height={118}
  />
</Box>

从源码看默认颜色并非写死的灰色:在 CSS 变量主题下取 theme.vars.palette.Skeleton.bg,否则由主文本色按模式混合得到——亮色模式 theme.alpha(theme.palette.text.primary, 0.11),暗色模式 0.13(见 Skeleton.js 第 107-110 行)。

无障碍(Accessibility)

官方文档 skeleton.md 的 Accessibility 章节给出了三条明确结论:

  • ARIA:无。骨架屏不附加任何 ARIA 属性,因为它只是视觉占位,不承载内容语义(数据到达后由真实内容提供可访问名称);
  • 键盘:骨架屏不可聚焦(not focusable),不会打断 Tab 导航序列;
  • 颜色亮度:骨架背景色取"可见所需的最低亮度",即在良好环境光、良好屏幕且无视觉障碍的条件下刚好可辨识,避免在正文上制造高对比度的视觉干扰。

补充一点来自源码的无障碍事实:如前所述,transitions.disable(减少动态效果)场景下 pulse/wave 动画会自动禁用,进一步照顾前庭敏感用户。

API 速查

结合 Skeleton.d.tsskeletonClasses.ts 的类型定义,组件的公开 API 如下:

Prop 类型 默认值 说明
animation 'pulse' | 'wave' | false 'pulse' 动画模式;false 完全禁用动画
variant 'text' | 'rectangular' | 'rounded' | 'circular' 'text' 形状变体
width number | string - 宽度;内联样式写入,覆盖 children 推断
height number | string - 高度;text 变体下默认基准为 1.2em
children React.ReactNode - 传入真实组件以推断宽高(子元素被隐藏渲染)
component React.ElementType 'span' 根节点组件(默认 span)
classes Partial<SkeletonClasses> - 覆盖 utility class 样式,key 包括 roottextrectangularroundedcircularpulsewavewithChildrenfitContentheightAuto
sx SxProps<Theme> - 系统 prop,可用于改 bgcolor

组件实现细节补充:SkeletonforwardRef 组件(Skeleton.js 第 236 行),支持 ref 转发;默认 props 可通过 DefaultPropsProvideruseDefaultProps({ name: 'MuiSkeleton' }))在全局注入,方便应用层面统一设置默认变体或动画。

小结与延伸阅读

本文以 skeleton.md 文档为骨架,覆盖了 Usage、Variants、Animations、Inferring dimensions、Color 与 Accessibility 全部章节,并结合以下仓库文件展开实现细节:

实用要点回顾:文字占位保持 variant="text" 并跟随排版;固定尺寸用 width/height;不想重复尺寸就用 children 推断;深色背景用 sx={{ bgcolor: ... }};动画可关闭且自动响应 reduce-motion 偏好。

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