一个旋转的 IconLoader(animate-spin),带 role="status"。底下是纯 svg,
不是 Base UI 部件 —— 没有任何 state,className 只收字符串,其余原生 svg
属性(含 ref)原样透传。尺寸默认 size-4、颜色继承文字色,两者都从
className 改。
它也是全库加载反馈的那颗零件:Button 的 pending、
LoadingOverlay 的默认内容、
DataTable 与
InfiniteSelect 的「加载更多」行,中间转的
都是它。
使用
import { Spinner } from '@gedatou/cadenza-ui'<Spinner />尺寸
改尺寸用逻辑工具类:block-6 inline-6 给一个固定大小,
block-[1em] inline-[1em] 让它跟着所在处的字号走(Button 的 pending 用的就是
后者)。上面的 hero 三档演示的就是这条。
为什么不是再写一个 size-*:size-4 是烘进组件的类,同族的工具类特异性一样,
谁赢只看产物里的先后,不保险;逻辑工具类(block-* / inline-*)在生成的样式表里
排在物理的 size-* 之后,所以覆盖稳定生效。
颜色同理走 className 的文字色类(text-primary…)—— 不传就继承所在处的文字色。
每个 spinner 带 data-slot="spinner",需要从外部按容器统一定位(比如让某一片区域
里的所有 spinner 一起变大)时当选择器用。
Button
Button 的 pending 默认组合就是它:标签原地被磨砂融开,
spinner 浮在暗纱上,按钮宽度不变。
这里 spinner 以 aria-hidden 装饰性注入:spinner 只是装饰,状态由按钮自己的
aria-busy 说。颜色写死 text-foreground 而不是继承 —— 按钮里继承来的是标签色,
会让 spinner 和被磨砂的标签糊在一起。两种注入方式的分界见无障碍。
无障碍
两种用法的边界:独立出现(页面区块加载中)保留 role="status" 让它自己播报;
装饰性出现(塞进已有自身状态语义的组件里)传 aria-hidden,别让两个 live region
抢话。
库内两边都有例子:
aria-label 默认是英文 'Loading' —— 家法的英文兜底(同 SearchField 的
'Search'、DataPagination 的 'Rows per page'):只进 aria、不上屏,传译文即覆盖,
因为底下的 svg 在默认值之后才展开 props。
什么时候用 Spinner
裸 Spinner 只负责画一个转圈。「一整片在加载」这件事另有组件,别拿它硬凑:
动作面只有 pending、内容面只有 loading,两个词是对应词不是别名 —— 选组件时先认
自己在哪一面。
换图标没有 prop 口子:图标写死是 IconLoader,它躺在 vendored 的 shadcn 源码里
(src/primitives 有逐字节校验,不从那里改)。要别的图标就直接放你自己的 svg ——
上面几个消费点收的都是 children(LoadingOverlay 的 children、DataTable 与
InfiniteSelect 的加载槽位),不认 Spinner 这个具体类型。
Props
顺序规则:必填 → 非受控默认值 → 受控值 → 回调 → 行为开关 → 外观 → className。
SpinnerProps 类型一并导出。