跳到主要内容
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
开始使用WebReactSwiftUIJetpack ComposeFlutterReact Native预设你自己的设计系统与 AI 代理一起使用 Tanrei品牌设计令牌设计系统与定制Theme Studio颜色字体排印图标形状层级动效交互状态行为布局无障碍文字颜色令牌字阶令牌形状令牌层级令牌动效令牌状态令牌间距与度量令牌组件按钮主操作按钮分段按钮按钮组带菜单的按钮工具栏复制按钮复选框单选按钮开关纸片(Chip)菜单滑块滑块字段日历DateTimePicker字段组单选组复选框组下拉选择框下拉选择字段菜单栏文件上传文件选择器 进阶放置区文本输入框 进阶文本字段纸片字段字段组合框 进阶组合框字段数字输入框 进阶数字字段颜色输入框 进阶颜色字段一次性验证码输入框 进阶一次性验证码字段嵌入式文本输入框 进阶聊天输入栏卡片分隔线模态层 进阶对话框列表列表行底部面板侧边面板轮播代码块表格弹出框步骤折叠面板描述列表图示应用外壳页头树视图时间线图片预览调整手柄可调整大小的框滚动区域徽标进度指示器计量条数字滚动器加载指示器消息条(Snackbar)工具提示键盘快捷键标签(Tag)行内代码提示块(Callout)头像空状态标签组(Tag group)骨架屏文件项色块跑马灯文本标签页标签页视图应用栏导航栏导航抽屉搜索链接分页上一页/下一页链接步骤条面包屑目录链接卡片FAB 菜单状态Tanrei 是如何构建的参与贡献许可与致谢
首页组件骨架屏

骨架屏

在内容加载期间,站在内容将要出现的位置上:用低调的占位符画出即将到来的内容的形状——几行文本、一个块、一个圆——并保住内容将要占据的框,所以内容到达时什么都不会移动。

content_copy以 Markdown 格式查看
类别
信息传达
令牌
comp.skeleton
层级
基础

概览

Web
tanrei-skeleton
schedule
SwiftUI
TanreiSkeleton
radio_button_unchecked
Jetpack Compose
TanreiSkeleton
radio_button_unchecked
Flutter
TanreiSkeleton
radio_button_unchecked
React Native
Skeleton
radio_button_unchecked

生成自 docs/status/status.json, specs/skeleton.spec.json

加载中的人物信息和卡片

视图常常比它的内容先到。骨架屏在内容将要出现的位置上,保住即将到来的内容的形状:几行文本、代表图片或控件的块、代表人物图片的圆。内容到达时,就住进为它保留的框里,用户正在看的、正要按下的东西都不会移动。

它由一个区域和其中的占位符组成。区域只向辅助技术说明一次——“正在加载项目”,忙碌——占位符本身什么也不说。它要等一小会儿才显示,所以短暂的等待中看到的是一块大小合适的空白,而不是一闪而过的形状。

在 Web 上,它是 <tanrei-skeleton>,里面是你自己的、带有 class="tanrei-skeleton-placeholder" 的元素。其他平台尚未实现。

骨架屏、加载指示器、进度指示器、空状态

有四个组件回答“这里还什么都没有”。用哪一个,取决于对这次等待、对将要到来的内容知道些什么。

情况 使用 原因
形状已知的内容将填满视图的某个区域,而且等待长到看得见。 骨架屏 它保住布局,所以内容到达时什么都不会移动,还能看出各样东西将在哪里。
时长未知的短暂等待,结果没有可以保留的形状,或者很小:在按钮、行、字段、状态行里。正在进行的操作,例如保存或发送。 加载指示器 一个标记就够了。为一行或一个图标画形状只是噪音。
进度可以测量的任务,或者要花好几秒的任务。 进度指示器 骨架屏不说明进行到了哪里,而几秒之后用户就需要知道。
什么都不会来:列表是空的,搜索没有找到任何结果。 空状态 骨架屏承诺会有内容。
加载失败。 在原处放一个错误状态的提示块。 永远不结束的骨架屏是在说谎。
内容已经在那里,只是它的脚本、图片或字体来得晚。 四者都不用。组件自己保住自己的框。 骨架屏不知道另一个组件的大小,那个组件自己知道。

用法

等待开始时,把骨架屏放在内容将要出现的位置;等待结束时,用内容替换它。给它一个说明正在加载什么的 label。占位符是你自己的、带有这个类的空元素:一个 <span>,可以放在任何位置;或者一个 <div>,用在它是某个块(例如另一个 <div>)的子元素的地方。在标题或段落里,它是 <span>。

<link rel="stylesheet" href="@tanrei/web/skeleton.css" />

<!-- 作为骨架屏直接子元素的占位符会上下堆叠,中间留有间隙。 -->
<tanrei-skeleton label="正在加载项目">
  <span class="tanrei-skeleton-placeholder" data-shape="block" style="block-size: 160px"></span>
  <span class="tanrei-skeleton-placeholder" style="inline-size: 12.5rem"></span>
  <span class="tanrei-skeleton-placeholder" data-lines="3"></span>
</tanrei-skeleton>

<!-- 除此之外的一切都是你自己的布局,占位符放在其中内容将要出现的位置。 -->
<tanrei-skeleton label="正在加载作者信息">
  <div class="byline">
    <span class="tanrei-skeleton-placeholder" data-shape="circle" style="block-size: 40px"></span>
    <div>
      <h3><span class="tanrei-skeleton-placeholder" style="inline-size: 10rem"></span></h3>
      <p><span class="tanrei-skeleton-placeholder" data-lines="2"></span></p>
    </div>
  </div>
</tanrei-skeleton>

<!-- 页面首次呈现的一部分,由服务器写出:立即显示,在任何脚本运行之前就是正确的。 -->
<tanrei-skeleton show-delay="0" role="progressbar" aria-busy="true" aria-label="正在加载仪表盘">
  …
</tanrei-skeleton>
<noscript>仪表盘需要 JavaScript 才能显示。</noscript>

<script type="module">
  import '@tanrei/web/components/skeleton';
</script>
import { Skeleton } from '@tanrei/react';
import '@tanrei/web/skeleton.css';

{project ? (
  <ProjectCard project={project} />
) : (
  <Skeleton label="正在加载项目">
    <span className="tanrei-skeleton-placeholder" data-shape="block" style={{ blockSize: 160 }} />
    <span className="tanrei-skeleton-placeholder" style={{ inlineSize: '12.5rem' }} />
    <span className="tanrei-skeleton-placeholder" data-lines={3} />
  </Skeleton>
)}

在 Web 上,脚本运行之前,样式表就是整个组件:形状、显示之前的等待、脉冲、减弱动态效果时的静止,以及强制颜色模式下的边线。元素补上样式表做不到的事:提供给辅助技术的角色和名称、把骨架屏里的东西排除在焦点顺序之外,以及在看不见它的时候暂停。

规格

三种形状:文本、块、圆

文本占位符采用它所在位置的文本样式:在标题里、在段落里

构成

部件 说明
1 container The region that stands in the content's place: a block as wide as the place it stands in and as tall as what it holds, with no fill, no edge, no padding and no elevation. It is what assistive technology is told about, once, and it carries no state: a skeleton takes no input.
2 placeholder A quiet filled shape in the place of one piece of the content: one or more lines of text, a block (a picture, a card, a control) or a circle (a person's picture, a round mark). It has the size the content will have and says nothing by itself.

属性

属性 类型 默认值 说明
label string "" What is loading, in the product's language ("Loading projects"): the name assistive technology is given for the whole skeleton, once. Never drawn. Left empty, it reads the product's words; Tanrei ships none of its own. Not read while decorative is true.
decorative boolean false Whether something else already says this wait: a loading indicator beside it, a status text, a region the product itself exposes as busy and names. True keeps the whole skeleton from assistive technology, so that one wait is said once; it changes nothing that is drawn.
showDelay number null How long, in milliseconds, this skeleton waits from the moment it is inserted before it is drawn and exposed to assistive technology; 0 shows it as it is inserted. Null takes comp.skeleton.motion.show-delay, the system's answer for every skeleton. Set it to 0 where the skeleton is part of the first presentation of a page or a view, which has nothing else to show; leave it where content is fetched in a view that is already there.

条目

该组件重复排列的条目种类。测试样例按顺序列出条目;各平台以各自的惯用方式为每个种类命名。

placeholder

One shape in the place of one piece of the content: lines of text, a block or a circle.

属性 类型 默认值 说明
shape text | block | circle "text" What the placeholder stands for. Text is one or more lines of the text style in effect where it stands. Block is a box: a picture, a card, a control, a row. Circle is a round box: a person's picture, a round mark.
lines number 1 For text: how many lines the text will take, a whole number of 1 or more. The placeholder is exactly that many line boxes high. Not read for a block or a circle.
inlineSize number null The placeholder's size along the line, in points. Null leaves it to the layout: the whole inline size of its place for text and for a block, the side of the square for a circle. A size the product's own layout gives the placeholder (a share of a row, a track of a grid) is used as it is and needs no prop.
blockSize number null The placeholder's size across the line, in points: the height of a block, and the side of a circle. Null is one line of the text style in effect, or the size the product's own layout gives. Not read for text, whose height is its lines.
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 span TanreiSkeletonPlaceholder TanreiSkeletonPlaceholder TanreiSkeletonPlaceholder SkeletonPlaceholder
shape · 属性 data-shape shape shape shape shape
lines · 属性 data-lines lines lines lines lines
inlineSize · 属性 inline-size width width width width
blockSize · 属性 block-size height height height height
备注 The product's own element with class="tanrei-skeleton-placeholder" — a <span>, or a <div> where it is a block's child — empty, so that it has nothing to expose. data-shape is text (the default, and what a missing or unknown value draws), block or circle; data-lines the number of lines. inlineSize and blockSize are the CSS properties inline-size and block-size on the element (in its style, or by a rule of the product's): a number of the spec is that many CSS pixels, and any other length, a percentage or an aspect-ratio the product writes there is a size its layout gives (SKL-4). The text style is the inherited font, so the placeholder goes where the text will be — inside the heading, the paragraph or the cell — and is 1lh high a line. TanreiSkeletonPlaceholder(.text, lines: 3), TanreiSkeletonPlaceholder(.circle, height: 40); a .frame or an .aspectRatio the product applies is a size its layout gives. The text style is the font of the environment (.font(...) on an ancestor), measured with the reader's text size. TanreiSkeletonPlaceholder(shape, lines, width, height, modifier) inside the skeleton's scope; a size from modifier is a size the layout gives. The text style is LocalTextStyle, measured with the font scale. TanreiSkeletonPlaceholder.text(lines: 3), .block(height: 160), .circle(size: 40); inside an Expanded, a SizedBox or an AspectRatio it takes that size. The text style is DefaultTextStyle, with MediaQuery.textScalerOf. <SkeletonPlaceholder shape="text" lines={3} />; style gives a size the layout gives (flex, a percentage, aspectRatio). React Native has no inherited text style, so the product passes one of Tanrei's text styles as textStyle, and the body style is used where it gives none (SKL-3).

状态

行为

以 EARS 形式写成的需求。每个平台的实现都按这些 ID 逐条测试。需求原文取自规格文件,原样以英文呈现。

ID 需求
SKL-1 THE skeleton SHALL lay itself out as a block that takes the whole inline size of the place it stands in and is as tall as what it holds: it SHALL lay out the placeholders that stand directly in it in one column, in the order given, each at its own size and at the start edge, comp.skeleton.container.gap apart, and SHALL leave placeholders that stand inside a layout of the product's own (a row, a grid, a card's own structure) to that layout, changing neither their place nor their size. THE skeleton SHALL draw no fill, no edge and no elevation, SHALL bring no padding and no outer space of its own, SHALL set no height of its own and SHALL NOT scroll or clip.
SKL-2 THE skeleton SHALL draw each placeholder as one box, or for text one box a line, filled with comp.skeleton.placeholder.color, with the corners of its shape (comp.skeleton.placeholder.shape.text for each line of text, comp.skeleton.placeholder.shape.block for a block, comp.skeleton.placeholder.shape.circle for a circle), never larger than half the box's smaller side, and with no edge, no elevation, no shadow and nothing inside it. THE skeleton SHALL draw nothing else: no glyph, no mark, no text, and never its label.
SKL-3 WHILE a placeholder's shape is text THE skeleton SHALL size it by the text style in effect where it stands — the style text placed there would be drawn in, and sys.typescale.body where the platform has no such style and the product gave none: the placeholder SHALL be exactly as tall as lines line boxes of that style, and in each line box THE skeleton SHALL draw one bar as tall as the style's font size, centred in the line box. Each bar SHALL be as long as the placeholder's inline size (inlineSize, else the whole inline size of its place), except that WHILE lines is 2 or more the last bar SHALL be comp.skeleton.placeholder.last-line-inline-size of that size, from the start edge. The placeholder SHALL NOT read blockSize.
SKL-4 WHILE a placeholder's shape is block THE skeleton SHALL draw one box whose inline size is inlineSize, else the size the product's layout gives it, else the whole inline size of its place, and whose block size is blockSize, else the size the product's layout gives it (a stretched cell, a ratio the product set), else one line box of the text style in effect where it stands (SKL-3). The block SHALL NOT read lines.
SKL-5 WHILE a placeholder's shape is circle THE skeleton SHALL draw one square box whose side is blockSize, else inlineSize, else the size the product's layout gives it, else one line box of the text style in effect where it stands (SKL-3), and SHALL draw it round. WHERE the place is narrower than the side THE skeleton SHALL make the square as large as the place allows and keep it square: a circle is never drawn as an oval. The circle SHALL NOT read lines.
SKL-6 THE skeleton SHALL keep the box of every placeholder, and so its own, unchanged from the moment it is inserted to the moment it is removed — while it waits to be shown (SKL-7), while it is shown and while it stirs (SKL-12) — and SHALL NOT guess what is coming: it draws the placeholders the product gave, in the sizes the product gave, and nothing of the content. That the content takes the box its placeholders held is the product's to arrange, by placing each placeholder where its content will be and as that content will be placed.
SKL-7 WHEN the skeleton is inserted into a view THE skeleton SHALL wait for its show delay (SKL-9) before it is shown, and WHILE it waits SHALL draw nothing, SHALL run no animation, SHALL be absent from the accessibility tree (no role, no name, no busy state), and SHALL still take the box it takes when shown (SKL-1 to SKL-5), so that nothing around it moves when it appears; a show delay of 0 SHALL show it as it is inserted.
SKL-8 WHEN its show delay ends THE skeleton SHALL be shown: it SHALL draw its placeholders at once, at their resting opacity and without an entrance of their own, expose itself as SKL-15 says without announcing itself (SKL-16), and start the animation of SKL-12 at the beginning of a pulse.
SKL-9 THE skeleton SHALL take its show delay from showDelay, a number of milliseconds used as given (the motion dial does not scale it). WHILE showDelay is null THE skeleton SHALL use comp.skeleton.motion.show-delay in its place; IF showDelay is negative or not a finite number THE skeleton SHALL use the token likewise and in debug builds report the value it ignored.
SKL-10 THE skeleton SHALL treat its show delay as a waiting time, not motion: WHERE motion is reduced it SHALL keep it, on every platform, including one whose reduced-motion setting removes every animation; and it SHALL keep counting it while the skeleton cannot be seen (scrolled out of view, in a hidden view), because the delay measures the wait, not what is seen.
SKL-11 WHEN the skeleton is removed before its show delay ends THE skeleton SHALL have drawn, exposed and announced nothing, and WHEN it is removed while shown it SHALL disappear at once, without a motion of its own: how the content enters is the content's. WHEN it is inserted again THE skeleton SHALL wait its whole show delay again; hiding it without removing it (a hidden ancestor, scrolling) SHALL neither restart nor pause the delay (SKL-10). WHEN showDelay changes while the delay is running THE skeleton SHALL finish the running delay as it started and use the new value from its next insertion.
SKL-12 WHILE the skeleton is shown THE skeleton SHALL stir as comp.skeleton.motion.animation names, without end. pulse SHALL ease the opacity of every placeholder's fill from its resting value to comp.skeleton.placeholder.dim-opacity of it over comp.skeleton.motion.dim-duration with comp.skeleton.motion.dim-easing and back over the same time on the same curve, every placeholder of the skeleton in step with the others. wave SHALL put each point of each placeholder through the same fall and return, each point later than the skeleton's start edge by a share of one whole pulse equal to its distance from that edge over the skeleton's inline size, so that one dimmer band travels from the start edge to the end edge through all the placeholders as through one surface. none SHALL leave the placeholders still at their resting opacity. In every manner THE skeleton SHALL change the fill's opacity only: no box moves, grows or changes shape, and nothing brighter than the resting fill is drawn.
SKL-13 WHERE reduced motion is requested THE skeleton SHALL NOT stir, whatever comp.skeleton.motion.animation names: it SHALL draw its placeholders still, at their resting opacity. The shapes, the label and the busy state still say that the place is waiting, and the show delay is kept (SKL-10).
SKL-14 WHILE the skeleton is not visible (scrolled out of view, in a hidden view, or in an app in the background) THE skeleton SHALL NOT run its animation, and SHALL resume it when it becomes visible.
SKL-15 WHILE the skeleton is shown and decorative is false THE skeleton SHALL expose itself to assistive technology as one element: the progressbar role with label as its name, no current value, and the platform's busy or indeterminate state. THE skeleton SHALL expose nothing of what it holds: no placeholder, no count of placeholders or of lines, and nothing of a layout the product placed in it.
SKL-16 THE skeleton SHALL NOT be focusable and SHALL NOT be a stop in the focus order, SHALL NOT announce itself when it is shown, when it changes or when it is removed, and SHALL NOT move keyboard or screen-reader focus. Saying that the content has arrived, where the product wants it said, is the product's, as it is beside a loading indicator.
SKL-17 WHILE decorative is true THE skeleton SHALL keep itself and everything it holds from assistive technology — no role, no name, no busy state — and SHALL draw, wait and stir as it otherwise would. A skeleton that stands inside another skeleton SHALL behave as decorative whatever its prop says: one wait is said once.
SKL-18 THE skeleton SHALL take its name from label, and WHILE label is empty from the product's words (the key loadingLabel); it has no words of its own in any language. WHILE both are empty and decorative is false THE skeleton SHALL still be drawn and still be exposed as SKL-15 says, without a name, and in debug builds report once that a skeleton has no label, naming the prop.
SKL-19 THE skeleton SHALL NOT take pointer, touch or keyboard input and SHALL NOT change anything it draws under a pointer, a press or focus: it has no hovered, focused, pressed, selected or disabled state and draws no state layer and no focus indicator. Whatever the product places in it besides placeholders SHALL be drawn as given, SHALL take no input and no focus, and SHALL NOT be exposed to assistive technology: a skeleton holds the shape of content, never content.
SKL-20 WHERE the platform draws with a person's own forced colors (the web's forced-colors mode, and a platform's equivalent where it has one) THE skeleton SHALL draw each placeholder, and each line of a text placeholder, as the outline of its shape, sys.border.hairline wide and inside its box, in the platform's text color, with no fill, and SHALL NOT stir: a fill is not drawn in that mode, and a placeholder that vanished would leave a blank place that says nothing.
SKL-21 WHILE the text size is enlarged THE skeleton SHALL grow its text placeholders with the text style they stand in, and every placeholder whose size is a line box with them, so that the skeleton still holds the box the enlarged content will take; it SHALL keep a size given as inlineSize or blockSize as given, SHALL never draw a placeholder wider than its place, SHALL keep its gap, and SHALL need no horizontal scrolling at a width of 320 points with text at 200 %.
SKL-22 WHERE the layout direction is right-to-left THE skeleton SHALL place its placeholders at the right edge, begin the shortened last bar of a text placeholder (SKL-3) and every placeholder narrower than its place at the right, and run a wave from the right edge to the left; a block and a circle are otherwise drawn as they are in a left-to-right layout.
SKL-23 THE skeleton SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the skeleton is shown THE skeleton SHALL draw itself for the new direction at once, without a motion of its own, keeping its place in its animation.
SKL-24 WHEN label, decorative or a placeholder's shape, lines, inlineSize or blockSize changes, or a placeholder is added or removed, while the skeleton waits or is shown THE skeleton SHALL draw and expose the new value at once, without a motion, without an announcement, and without restarting its show delay or its animation. THE skeleton SHALL neither show nor hide itself and SHALL NOT know when the content arrives: the product puts the skeleton in the content's place when the wait starts and takes it away when the wait ends, whatever the wait ends in.
SKL-25 WHEN a placeholder's shape names a value this spec does not have THE skeleton SHALL draw it as text; IF its lines is less than 1 or not a finite number THE skeleton SHALL use 1, and SHALL use the whole number below a lines that is not whole; IF its inlineSize or blockSize is negative or not a finite number THE skeleton SHALL treat it as null; WHEN comp.skeleton.motion.animation names a manner the skeleton does not know THE skeleton SHALL pulse. In each case it SHALL report the value it ignored in debug builds.
SKL-26 IF the skeleton holds no placeholder THE skeleton SHALL draw nothing, take no space and expose nothing, and in debug builds report that a skeleton holds no placeholder: a busy region with no shape in it is a blank place, and a wait whose shape is not known is a loading indicator's.

各平台名称对照

对等并不意味着 API 完全相同。同一份契约,由各平台以自己的惯用方式表达。

规格 Web SwiftUI Jetpack Compose Flutter React Native
组件 tanrei-skeleton TanreiSkeleton TanreiSkeleton TanreiSkeleton Skeleton
label · 属性 label label label label label
decorative · 属性 decorative isDecorative decorative decorative decorative
showDelay · 属性 showDelay showDelay showDelay showDelay showDelay
备注 Form: the light-DOM form a component around the product's own elements takes. <tanrei-skeleton label="Loading projects"> is a custom element with **no shadow root** around the product's own layout and its placeholders, and one stylesheet, @tanrei/web/skeleton.css, draws everything — the column and its gap, the placeholders, the show delay, the animation, reduced motion and forced colors — with selectors that start at tanrei-skeleton and at the placeholder's class, wrapped in :where(). **This is the point of the form: a skeleton is what a page shows before its script has run**, so it must be whole before the element is defined and where script never runs; a shadow root would draw nothing until the module arrived, which is the wait the skeleton is for. The show delay without script is an animation with a delay (animation: <appear> 0s linear var(--tanrei-comp-skeleton-motion-show-delay) both from visibility: hidden), so the box is held and nothing is drawn or exposed until it ends (SKL-7); the show-delay attribute sets a custom property the stylesheet reads, and attribute selectors cover show-delay="0" before definition. pulse is an opacity animation of the placeholders with animation-direction: alternate; wave is one mask or background gradient sized to the host and positioned per placeholder (background-attachment does not serve here; the element measures the placeholders' offsets and writes them as custom properties once defined, and before definition a wave is drawn as a pulse — say so in the docs); prefers-reduced-motion: reduce removes the animation and keeps the delay (SKL-10, SKL-13); forced-colors: active replaces the fill with an outline in CanvasText (SKL-20). The choice of manner reaches the stylesheet as the custom property that carries its name, read by a style query or, where those are missing, by the element. A text placeholder of several lines is one element calc(var(--lines) * 1lh) high whose bars are drawn as backgrounds; whether the bars can have eased corners and a shortened last bar from CSS alone in three engines, or need the element to write one child a line once defined, is the build's to find out — the box must be the same either way. What the element adds once defined, and only then: role="progressbar", aria-busy="true" and aria-label on the host when the delay ends, unless decorative or nested (then aria-hidden="true"); inert on the host, so that nothing inside takes focus or is read (SKL-19); pausing the animation while the host is not visible (SKL-14); the debug reports. A page that must be right before its script runs, or without it, writes role, aria-busy and aria-label on the element itself, and the element leaves what it finds; the placeholders are empty elements and expose nothing either way. The React wrapper renders the element around its children. Where script never runs the skeleton never ends unless the server replaces it: a product whose content arrives only by script gives the content, or a sentence, in <noscript>. Proposed spelling, for the platform's agent to settle. TanreiSkeleton("Loading projects", isDecorative:, showDelay:) { … }, a view builder that holds placeholders directly (stacked, SKL-1) or inside the product's own stacks and grids. Not the platform's own redaction of a view, whose look is the platform's and which needs the real view to exist: the same idea drawn from Tanrei's tokens. Exposed as one accessibility element with the label, the progress trait's indeterminate form (updatesFrequently is not set) and its children ignored; accessibilityHidden while decorative or waiting. accessibilityReduceMotion stops the animation; the show delay is a task, not an animation. Not built on this platform yet: the stage color a fixture's surface names, which the text-lines-on-container-highest fixture draws on. TanreiSkeleton(label, decorative, showDelay) { … }, a column scope. clearAndSetSemantics { contentDescription = label; progressBarRangeInfo = ProgressBarRangeInfo.Indeterminate } once shown, and clearAndSetSemantics { } with invisibleToUser() while decorative or waiting. The animation reads the system's animator duration scale: where it is 0 the placeholders are still, and the show delay is a coroutine delay that the scale does not shorten (SKL-10). Not built on this platform yet: the stage color a fixture's surface names, which the text-lines-on-container-highest fixture draws on. TanreiSkeleton(label: …, decorative:, showDelay:, children: […]), or child: for a layout of the product's own. Semantics(label: label, excludeSemantics: true) with the progress indicator's indeterminate semantics once shown; ExcludeSemantics and IgnorePointer around everything it holds. MediaQuery.disableAnimationsOf stops the animation; the show delay is a Timer. Not built on this platform yet: the stage color a fixture's surface names, which the text-lines-on-container-highest fixture draws on. <Skeleton label="Loading projects">…</Skeleton>. accessible, accessibilityRole="progressbar", accessibilityLabel and accessibilityState={{ busy: true }} once shown; importantForAccessibility="no-hide-descendants" and accessibilityElementsHidden while decorative or waiting; pointerEvents="none". AccessibilityInfo.isReduceMotionEnabled stops the animation. Follows the app's direction where an ancestor's is not known. Not built on this platform yet: the stage color a fixture's surface names, which the text-lines-on-container-highest fixture draws on.

令牌

令牌 引用 解析值(浅色)
--tanrei-comp-skeleton-container-gap sys.space.space100 8px
--tanrei-comp-skeleton-motion-animation — 0
--tanrei-comp-skeleton-motion-dim-duration sys.motion.duration.extra-long4 1000ms
--tanrei-comp-skeleton-motion-dim-easing sys.motion.easing.standard cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-skeleton-motion-show-delay sys.motion.role.show-delay 300ms
--tanrei-comp-skeleton-placeholder-color sys.color.surface-variant #eeeeee
--tanrei-comp-skeleton-placeholder-dim-opacity — 0.5
--tanrei-comp-skeleton-placeholder-last-line-inline-size — 0.6
--tanrei-comp-skeleton-placeholder-shape-block sys.shape.corner.medium 8px
--tanrei-comp-skeleton-placeholder-shape-circle sys.shape.corner.full 9999px
--tanrei-comp-skeleton-placeholder-shape-text sys.shape.corner.extra-small 3px

测试样例

作为基准的渲染结果。所有平台都以相同的 ID 为每个样例截取快照,以便相互比较。

生成自 specs/skeleton.spec.json, tokens/comp/skeleton.tokens.json, design/tokens/

指南

它保住框

骨架屏的意义在于什么都不移动。从插入的那一刻到移除的那一刻,它的框和每个占位符的框始终不变:等待显示时、出现时、动起来时都一样。

骨架屏什么也不猜。它按你给的尺寸,画出你给的占位符。让内容正好住进占位符保留的框,要由你来安排:把每个占位符放在它的内容将要出现的位置,摆放方式也与那份内容相同。

  • 文本占位符放在文本将要所在的标题、段落或单元格里面。它采用那里的文本样式,高度恰好等于那么多行文本的高度,读者放大文本时也是如此。
  • 用 data-lines 说明文本将占几行。你的内容越规整,就猜得越准。少了一行的骨架屏会让页面移动一行,这仍然比一块空白造成的移动要小。
  • 把块的尺寸设为图片或控件的尺寸:一个数值、一个比例,或者布局给它的尺寸。圆只需要一个数值,即它的边长。

按下“加载”:骨架屏把位置保住两秒半,然后内容住进它的框

加载

三层令牌

参考令牌保存原始值。系统令牌为值的用途命名。组件令牌把一个部件绑定到其中之一。

这一行位于文章下方,不会移动。

形状

data-shape 代表什么 它的尺寸
text(默认) 一行或多行文本。 它所在位置的文本样式的 data-lines 个行框。每一行是一根与字号等高的条。有多行时,最后一行较短,因为段落很少正好在边缘结束。inline-size 设置条的长度。
block 图片、卡片、控件、行。 inline-size 和 block-size,或者布局给它的尺寸:被拉伸的单元格、aspect-ratio。不给尺寸时,它与所在位置等宽,高一行。
circle 人物图片、圆形标记。 一条边长:block-size 或 inline-size。不给尺寸时,它高一行。

作为骨架屏直接子元素的占位符上下堆叠,间隙为 8。其他任何排法——两行文本旁边放一个圆、由卡片组成的网格——都是你在骨架屏里自己做的布局。没有现成的卡片骨架屏或列表骨架屏:屏幕是你的,只有你了解它们。

在你自己的行里,文本占位符或块需要从这一行得到宽度,就像内容将来那样:flex: 1、网格的一条轨道,或者一个 inline-size。

一次等待,一个信号

把一个区域的所有占位符放进同一个骨架屏,配一个 label。骨架屏里面的骨架屏不出声。

如果已经有别的东西在说明同一次等待——状态行里的加载指示器、一段状态文本、一个由你自己标记为忙碌并命名的区域——就设置 decorative。这时骨架屏的画法不变,但不提供给辅助技术,这样同一次等待不会被读两遍。

状态行已经说明了这次等待,所以它下面的占位符是装饰性的

正在搜索…

它先等待再显示

从插入的那一刻算起,骨架屏等待 300 毫秒之后才被绘制并暴露给辅助技术,在此期间它的框已经保住。更早结束的等待,显示的是一块自始至终大小合适的空白。

  • 在等待开始时插入骨架屏,在内容到达时移除它。延迟从插入时算起。隐藏骨架屏不会让延迟重新开始。
  • 如果骨架屏是页面或视图首次呈现的一部分,而那里没有别的东西可以显示,就设置 show-delay="0"。
  • 延迟是等待,不是动效:减弱动态效果时它保持不变。

它总会以某种结果结束

用内容替换骨架屏;什么都没有来时,用空状态替换;加载失败时,用错误状态的提示块替换。它自己从不超时:什么时候放弃,只有你知道。

骨架屏不会朗读内容的到达。在到达应当被听到的地方——回应一次搜索的结果、用户请求的列表——请通过你自己的实时区域(live region),或者发出请求的那个组件的状态文本来说明。只是带着内容打开的视图,不需要朗读。

动效

默认情况下,占位符以脉冲的方式变化:填充变淡到一半再恢复,所有占位符一起,每两秒一次。变化的只有不透明度。什么都不移动,也没有任何东西画得比静止时的填充更亮。

设计系统可以用令牌 comp.skeleton.motion.animation 为它所有的骨架屏另选一种方式:pulse、wave(同样的变淡,作为一条带子从起始边缘到末尾边缘穿过整个骨架屏)或 none。没有针对单个实例的设置。

减弱动态效果时,占位符静止不动。形状、标签和忙碌状态已经说明这个位置正在等待。看不见骨架屏的时候,它的动画不运行。

在 Web 上,脚本运行之前

骨架屏是页面在脚本运行之前显示的东西,所以 @tanrei/web/skeleton.css 不靠元素就能把它完整画出来。有几件事需要元素,在元素定义之前,或者在脚本永远不运行的地方,它们会有所不同:

  • 角色和名称。必须在脚本运行之前就正确的页面,自己在元素上写 role="progressbar"、aria-busy="true" 和 aria-label,元素会原样保留已经写在那里的这些属性。
  • 不是 0 的 show-delay。样式表按设计系统的延迟等待。
  • 一个文本占位符里超过 24 行。
  • wave 方式,它会被画成脉冲。
  • 位于未显示的视图里的骨架屏,在视图显示时才开始等待。

在脚本永远不运行的地方,除非服务器替换它,否则骨架屏永远不会结束。只能靠脚本到达的内容,需要在它的骨架屏旁边放一个 <noscript>。

推荐

  • 让占位符具有即将到来的内容的形状和尺寸。
  • 把文本占位符放在文本将要所在的元素里面。
  • 一次等待用一个骨架屏,配一个标签。
  • 等待结束时替换它,无论等待以什么结果结束。

避免

  • 不要把用户应当阅读或使用的任何东西放进骨架屏。它里面的东西不接受输入,并且对辅助技术隐藏。已经知道的标题、已经可用的按钮,要放在它的外面。
  • 不要把它用于控件或行里的短暂等待,也不要用于正在进行的操作。那是加载指示器的事。
  • 不要用它盖住已经在那里的内容。
  • 不要手工画灰色方块。

无障碍

角色
progressbar
标注
label is the accessible name of the whole skeleton: what is loading. Once shown, the skeleton is one element — a progressbar with no value, in the platform's busy (indeterminate) state — and its placeholders are hidden; during its show delay, and while decorative is true, it is not exposed at all. It is exposed exactly as a loading indicator is, because it says the same thing: something is loading here.
最小目标
即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px

键盘操作

按键 动作

备注

  • Not interactive: the skeleton takes no input and is never focusable (SKL-16, SKL-19), so there is no target of its own to size and minimumTarget holds trivially.
  • One wait, said once. Every placeholder of a region belongs to one skeleton with one label; a skeleton inside another is silent (SKL-17). Where the same wait is already said — a loading indicator in a status row, a status text, a region the product itself marks busy and names — set decorative, or the wait is read twice.
  • The skeleton does not announce that it appeared, and does not announce that the content arrived (SKL-16): it is removed with its busy state. Where arrival should be heard (results in answer to a search, a list that was asked for), the product says so through its own live region or the status text of the component that asked; a view that simply opens with its content needs no announcement.
  • A wait shorter than the show delay shows and exposes nothing (SKL-7): no shapes that flash for a sighted person, and no progressbar that appears and vanishes for a screen-reader user.
  • Nothing a person should read or use goes inside a skeleton: what it holds is kept from input and from assistive technology (SKL-19). A heading that is already known, a button that already works, stand outside it.
  • The fill is decoration, held to no contrast ratio: comp.skeleton.placeholder.color is a low-emphasis fill, and in the default system it has the tone of the highest container step, so a placeholder on a surface of that tone is hard to see. The wait is never said by the fill alone: the label says it to assistive technology, and forced colors draw every placeholder as an outline (SKL-20).
  • Motion: a pulse dims the fill by half and returns in 2 seconds in the default system, far below the three-per-second flash threshold; nothing moves and nothing brightens. Reduced motion stops it entirely (SKL-13): a large region that pulses is more than a small mark that does, and the shapes already say that the place is waiting.
  • A skeleton that never ends tells a lie. The product replaces it with the content, with an empty state where nothing came, or with a callout of the error status where loading failed, and sets its own limit on the wait; a task that takes many seconds, or whose progress can be measured, is a progress indicator's.
  • Text size, text spacing and reflow: SKL-21. A text placeholder follows the text style it stands in, so it grows with the reader's text size; a size given in points does not.

生成自 specs/skeleton.spec.json, tokens/sys/measure.tokens.json

组件负责的部分

  • 显示之后,骨架屏对辅助技术来说是一个元素:一个没有数值的进度条,处于忙碌状态,以 label 命名。它的暴露方式与加载指示器相同,因为它说的是同一件事。
  • 它的占位符、占位符的数量和行数都不暴露,你放在其中的布局也不暴露。
  • 等待显示期间,它完全不暴露。比延迟更短的等待,不会出现一个刚出现就消失的进度条。
  • 设置了 decorative,或者位于另一个骨架屏里面时,它不提供给辅助技术。
  • 它不可聚焦,也不是焦点顺序中的停靠点,它里面的任何东西同样如此。它不朗读自己,也从不移动焦点。
  • 减弱动态效果时,它静止不动。延迟保持不变。
  • 在强制颜色模式下,每个占位符以及文本的每一行,都用文本颜色画成其形状的边线。这个模式下不绘制填充,而消失的占位符会留下一块空白。
  • 文本放大时,文本占位符随它的文本样式一起变大,所以骨架屏保住的仍是放大后的内容所占的框。以数值给出的尺寸保持不变。没有任何东西比它所在的位置更宽;在宽度为 320、文本放大到 200% 时,不会出现横向滚动。
  • 在从右到左的布局中,占位符靠右,较短的最后一行从右侧开始,波浪从右向左行进。
  • 未知的形状、无法使用的行数或尺寸、没有占位符的骨架屏,以及没有标签的骨架屏,会在调试构建中报告。

需要你做的事

  • 用页面的语言给出一个说明正在加载什么的 label(“正在加载项目”),或者用 setWords({ loadingLabel: … }) 为所有骨架屏只给一次这段文字。组件不提供任何文字。
  • 已经有别的东西在说明这次等待时,设置 decorative。
  • 在内容到达算得上是消息的地方,说明内容已经到达。
  • 绝不要让骨架屏一直留着。用内容、空状态或错误来替换它。
  • 填充是装饰,不受任何对比度要求的约束。在最高容器色调的表面上,它很难看清。不要只靠它来说明某个位置正在等待。放在这样的表面上时,请在图层中把 comp.skeleton.placeholder.color 指向更强的角色。