zz design system
mono & squared
组件 / 状态反馈

反馈

反馈类组件回答三个问题:发生了什么(Alert / Toast)、还要多久(Progress / Spinner)、 这里为什么是空的(EmptyState)。它们共用同一条规则 —— 状态色只出现在一像素级的元素上: 2px 左线、1.5px 弧顶、进度填充条;面、描边与文字一律回到主题 token, 所以深色与亮色两套主题下语义强度一致。

提示块 Alert

静态提示用 2px 状态色左线标记严重程度,底是 --zz-surface、1px--zz-border-subtle 描边。标题 0.875rem / 600 / 状态色,正文 0.8125rem /--zz-text-secondary。

信息

镜像已同步到最近的三个区域。

成功

构建通过,产物大小 42.1 kB。

注意

这个仓库的构建缓存将在 7 天后失效。

失败

部署失败:目标环境返回 503。

不带标题时只剩正文;一行内联提示用 --info 默认值即可。

不带标题的一行提示。
正文里可以混排 行内代码 与链接。
import Alert from '@/components/ui/Alert.astro'

<Alert variant="info" title="信息">镜像已同步到最近的三个区域。</Alert>
<Alert variant="danger" title="失败">部署失败:目标环境返回 503。</Alert>

<!-- 由 JS 动态插入时才给 role -->
<Alert variant="danger" title="失败" role="alert">连接已断开。</Alert>

进度 Progress

轨道 0.375rem 高,底 --zz-surface-2(凹槽),填充 --zz-brand。 宽度用内联样式表达 —— 这是约定里唯一允许内联样式的场景。

value=24 · 24%

value=64 · 64%

value=100 · 100%

不确定态(省略 value)用位移动画扫过轨道;prefers-reduced-motion下退化成一条静止的满轨道,进度语义仍由 aria-label 承担。

import Progress from '@/components/ui/Progress.astro'

<Progress value={64} label="构建进度" />
<Progress value={3} max={8} label="第 3 / 8 步" />
<Progress label="正在同步镜像" />      <!-- 不确定态 -->
<Progress indeterminate label="等待响应" />

加载指示 Spinner

1.5px 描边圆环,弧顶用 --zz-brand,0.75s 线性旋转。三档尺寸 0.875 / 1.25 / 1.75rem:--sm 跟在按钮文字或标签行里, 默认档用于块级加载,--lg 用于整区或整页的等待。

整块内容还没渲染时,把 Spinner 放进 .zz-empty 的居中位置、配一行说明, 比满屏骨架屏更符合这套语言。

正在读取提交记录…

import Spinner from '@/components/ui/Spinner.astro'

<Spinner size="sm" label="同步中" />
<Spinner label="加载中" />
<Spinner size="lg" label="加载中" />

<Button variant="outline"><Spinner size="sm" label="同步中" />同步中</Button>

通知 Toast

右下角堆叠:.zz-toast-region 固定在 1rem 边距处、用var(--zz-z-dropdown) 压在内容之上,卡片之间 0.5rem。卡片底--zz-surface + --zz-elevation + 1px 描边,内距0.75rem 1rem,最小宽 16rem,进入动画 200ms。

下面这个舞台上把 region 改成了静态定位,方便在文档里看外观;实际使用时它就是固定的。

默认通知:三个文件已加入暂存区。

部署完成

prod-7f3a 已上线,用时 42s。
import Toast from '@/components/ui/Toast.astro'

<Toast>默认通知:三个文件已加入暂存区。</Toast>
<Toast variant="success" title="部署完成">prod-7f3a 已上线,用时 42s。</Toast>
<Toast variant="danger" title="同步失败" role="alert">远端拒绝写入。</Toast>

<!-- 多条堆叠:容器写在布局末尾,只写一次 -->
<div class="zz-toast-region">
  <Toast variant="success" title="部署完成">prod-7f3a 已上线。</Toast>
  <Toast variant="danger" title="同步失败" role="alert">远端拒绝写入。</Toast>
</div>

空状态 EmptyState

2rem 图标(--zz-text-muted)、1rem sans 标题、0.8125rem--zz-text-muted 说明,末尾一个可选操作位。组件自己不画底与描边, 所以放进卡片、面板或页面区块都成立。

还没有部署记录

推送到 main 分支后,第一次构建会自动出现在这里。

没有匹配的结果

换一个关键词,或清空筛选条件。

import EmptyState from '@/components/ui/EmptyState.astro'

<EmptyState title="还没有部署记录" description="推送到 main 分支后,第一次构建会自动出现在这里。">
  <svg slot="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
    <path d="M4 7.5h16v11H4zM12 3.5v4M8.5 11.5h7" />
  </svg>
  <Button slot="actions" size="sm">手动触发构建</Button>
</EmptyState>

主题

状态色跨主题恒定,面与文字跟着主题走。下面这块显式声明data-theme="light",与页面当前主题无关 —— 组件本身不感知主题。

成功

构建通过,产物大小 42.1 kB。

失败

部署失败:目标环境返回 503。

类名

类名作用
.zz-alert提示块骨架:2px 左线 + 1px --zz-border-subtle 描边 + --zz-surface 底,内距 0.75 / 1 / 0.75 / 0.875rem
.zz-alert--info | success | warning | danger把左线换成 --zz-info / --zz-success / --zz-warning / --zz-danger(无变体时默认 info)
.zz-alert__title0.875rem sans 600,颜色跟随变体状态色
.zz-progress0.375rem 轨道,底 --zz-surface-2,overflow: hidden
.zz-progress__bar填充条:--zz-brand,宽度过渡 150ms
.zz-progress--indeterminate35% 宽的扫动条,1.1s 循环;减弱动效时改为静止满轨道
.zz-spinner1.25rem 圆环,1.5px --zz-border-strong 描边 + --zz-brand 弧顶,0.75s 线性旋转
.zz-spinner--sm | lg0.875 / 1.75rem
.zz-toast-region固定右下角(1rem)的堆叠容器,var(--zz-z-dropdown),0.5rem 间距,不吃指针事件
.zz-toast通知卡:最小宽 16rem、内距 0.75/1rem、1px --zz-border-subtle 描边 + --zz-elevation 浮起、200ms 进入动画
.zz-toast--success | danger2px 状态色左线(默认是等宽的透明左线,换色时几何不跳)
.zz-toast__title0.875rem sans 600 --zz-text
.zz-empty居中的纵向堆叠,内距 2.5rem / 1.5rem,不画底与描边
.zz-empty__icon2rem 图标位,--zz-text-muted,整体 aria-hidden
.zz-empty__title / .zz-empty__desc1rem sans 600 --zz-text / 0.8125rem --zz-text-muted
.zz-empty__actions操作位:横向 0.5rem 间距,居中

token

反馈组件不新增变量:状态色只上线条与填充,面、描边、文字全部走主题 token。

token用在哪
--zz-info / --zz-success / --zz-warning / --zz-danger提示块的 2px 左线与标题、通知的 2px 左线
--zz-surface提示块与通知卡的底
--zz-surface-2进度轨道(凹槽)
--zz-border-subtle提示块与通知卡的 1px 描边
--zz-border-strong圆环的未完成部分
--zz-brand进度填充、圆环弧顶
--zz-text / --zz-text-secondary / --zz-text-muted标题 / 正文 / 空状态说明与图标
--zz-elevation通知卡浮起(深色 1px 零模糊描边,亮色补一层轻投影)
--zz-z-dropdown通知堆叠容器:200,压过顶栏的 100,低于模态的 300
--zz-dur / --zz-ease进度宽度过渡与通知进入动画的缓动

可访问性

  • 颜色不是唯一线索:状态线只有 2px,而状态色跨主题恒定 —— 亮色主题下 success / warning 与浅底的对比不足以单独承担含义,所以严重程度必须同时写在标题文案里(「失败」「注意」), 灰度打印与色觉障碍下才不丢信息。
  • 静态出现在页面里的 Alert 不要加 role:加了会随页面加载播报一遍。 只有由 JS 动态插入时才用 role="alert"(失败、立刻打断)或role="status"(成功、礼貌播报)。
  • Progress 是 role="progressbar" + aria-valuemin / max / now, 并且必须有 aria-label 说清在等什么。不确定态要省略aria-valuenow(否则读屏会以为进度停在 0%)。
  • Spinner 用 role="status" + aria-label,文案描述「正在做什么」 而不是「转圈」;旋转本身不承载信息,所以减弱动效下静止也不丢语义。
  • 图标型触发器要自己带 aria-label,并且它必须落在真正渲染出的元素上: Astro 组件不会透传自己没声明的属性 —— 所以上面那个只有 Spinner 的按钮写成原生的<button class="zz-btn zz-btn--ghost zz-btn--icon-sm">。
  • 通知用 role="status",失败类改用 role="alert"。通知不要几秒后 无声消失:要么带关闭按钮,要么至少停留到用户有下一步动作;关键错误不要只发通知。
  • 空状态的图标是装饰,整块 aria-hidden;说明文案要给出「接下来做什么」, 操作位给一个主操作(最多再配一个次要链接),它们是这块内容里唯一可聚焦的元素,全局焦点环照常生效。
  • prefers-reduced-motion: reduce 下:进度扫动停下并铺满轨道、圆环停止旋转、 通知直接出现不做位移 —— 状态本身仍然可见、可读。