zz design system
mono & squared
组件 / 导航

导航

导航族的全部状态都由 1px 描边、一条竖导轨和文字色承担:当前项既换颜色也换导轨, 因此「我在哪」不是只靠颜色说的。吸顶栏是全站唯一抬到 --zz-z-nav 的成员; 标签条的选中下划线用 inset 阴影画,阴影不占盒模型,所以切换时没有 1px 的跳动。

吸顶栏

栏体是 position: sticky; top: 0,底为 90% 的 --zz-bg 再叠 12px 模糊:滚过的内容透得出来但读不清。下边线是 1px --zz-border-subtle, 所以它不靠阴影也能和内容分层。下面这段舞台是可滚动的,滚一下就看得见效果。

向下滚动这段内容,栏体会一直吸在顶部。当前页的链接是aria-current="page",它的底换成 --zz-surface-2、文字换成--zz-text —— 和悬停同一套表现,因此指针停在哪都不会误读成「我在这」。

栏体内的元素:字标 0.875rem,链接 0.8125rem 高 2rem,横向内距 0.625rem。

内层最大宽 72rem,横向内距 1rem,40rem 起 1.5rem —— 与页面容器同一条轴。

窄屏不折叠菜单,标签带自己横向滚动:比藏进抽屉少一次交互,也不会丢掉当前项。

这块舞台加了 isolation: isolate:栏体是 --zz-z-nav,不隔离的话 它会和文档站的顶栏同级抢绘制顺序。把组件放进可滚动的局部容器时,照做。

侧栏

每个链接自带 1px border-inline-start,首尾相接成一条连续导轨;当前项把导轨和 文字一起换成 --zz-brand,不加底也不加粗。分组标题是 0.625rem 的大写 eyebrow, 不参与导轨。

标签条

标签条本身是横向可滚动的一行,底部一条 1px 分隔线。选中态是box-shadow: inset 0 -2px 0 var(--zz-brand),未选中时留着同样几何的透明阴影占位, 所以来回切标签不会让整行抖 1px。用左 / 右箭头(或 Home / End)切换。

标签条只负责切换,不负责布局:把面板放在标签条外面,面板就能占满整栏宽度。
.zz-tabs 是标签条,.zz-tabs__tab 是每个标签,.zz-tabs__panel 是面板。
悬停只把文字提到 --zz-text;选中是品牌色文字 + 2px 品牌色下划线;禁用压到 50% 并移除指针事件。
左 / 右箭头在标签之间移动焦点并顺带选中,Home / End 跳到首尾;面板自身可聚焦,长内容能用方向键滚动。

脚本是渐进增强:无 JS 时标签条不能切换,但所有面板都还在 DOM 里、读起来是完整的; 面板的 hidden 由脚本补上,不要在标记里写死,否则关掉 JS 就只剩第一个面板。

面包屑

0.75rem 的等宽行:分隔符是 --zz-text-muted,路径项是--zz-text-secondary,末项用 --zz-text 并带aria-current="page"。末项永远是不可点击的纯文本。

分页

页码是 2.25rem 的正方形,观感沿用 .zz-btn--outline + .zz-btn--icon-sm: 1px --zz-border-strong 描边、--zz-surface 底。当前页填--zz-brand、文字 --zz-brand-ink;两端是上一页 / 下一页。 超过 7 页时折叠成 1 … 4 5 6 … 12,省略号与页码同宽,数字列始终对齐。

第二例停在第 1 页,「上一页」渲染成不透明度 50% 的不可聚焦文本,而不是指向自己的死链。

用法

---
import Navbar from '@/components/ui/Navbar.astro'
import Sidenav from '@/components/ui/Sidenav.astro'
import Button from '@/components/ui/Button.astro'
---

<Navbar label="主导航">
  <a slot="brand" class="zz-navbar__brand" href="/">
    <span class="zz-navbar__mark" aria-hidden="true">zz</span>
    <span>design system</span>
  </a>
  <a class="zz-navbar__link" href="/">概览</a>
  <a class="zz-navbar__link" href="/components/nav" aria-current="page">组件</a>
  <Button slot="actions" variant="outline" size="sm">文档</Button>
</Navbar>

<Sidenav
  groups={[
    { title: '组件', items: [{ href: '/components/nav', label: '导航', current: true }] },
  ]}
  label="侧栏导航"
/>
---
import Tabs from '@/components/ui/Tabs.astro'
import Breadcrumb from '@/components/ui/Breadcrumb.astro'
import Pagination from '@/components/ui/Pagination.astro'
---

<Tabs label="示例标签条">
  <button class="zz-tabs__tab" type="button" role="tab" id="tab-a"
          aria-controls="panel-a" aria-selected="true">甲</button>
  <button class="zz-tabs__tab" type="button" role="tab" id="tab-b"
          aria-controls="panel-b" aria-selected="false" tabindex="-1">乙</button>
</Tabs>
<div class="zz-tabs__panel" role="tabpanel" id="panel-a" aria-labelledby="tab-a" tabindex="0">…</div>
<div class="zz-tabs__panel" role="tabpanel" id="panel-b" aria-labelledby="tab-b" tabindex="0">…</div>

<Breadcrumb items={[{ href: '/', label: '首页' }, { label: '导航' }]} />

<Pagination page={5} total={12} hrefFor={(n) => '/components/nav?page=' + n} />

类名与 token

类名作用token
.zz-navbar吸顶栏:sticky、下边线、90% 地面色 + 12px 模糊--zz-z-nav --zz-bg --zz-border-subtle
.zz-navbar__bar内层:高 --zz-nav-h、最大宽 72rem、横向内距 1rem(40rem 起 1.5rem)--zz-nav-h
.zz-navbar__link0.8125rem 链接,高 2rem;悬停出 --zz-surface-2 底,aria-current="page" 同色并提亮文字--zz-surface-2 --zz-text-secondary --zz-text
.zz-navbar__brand / __mark左端字标;mark 是 1.5rem 的品牌色方块--zz-brand --zz-brand-ink
.zz-navbar__nav / __actions标签带(窄屏横向滚动,焦点环内缩 2px)/ 右端操作组(自动靠右)—
.zz-sidenav__link竖排链接:1px 导轨 + 0.3125rem / 0.75rem 内距;当前项导轨与文字变品牌色--zz-border-subtle --zz-brand
.zz-sidenav__title0.625rem 大写分组标题,不参与导轨--zz-text-muted
.zz-tabs标签条容器:横向、可滚动、底部 1px 分隔线--zz-border-subtle
.zz-tabs__tab2.25rem 高标签;悬停提亮文字;aria-selected="true" 时 inset 2px 品牌色下划线 + 品牌色文字--zz-brand --zz-text
.zz-tabs__panel面板:上内距 1rem,0.875rem 次级正文--zz-text-secondary
.zz-breadcrumb / __list0.75rem 路径行,可换行,间距 0.5rem—
.zz-breadcrumb__link / __sep路径项次级色、悬停提亮;aria-current="page" 用 --zz-text;分隔符是 muted--zz-text-secondary --zz-text --zz-text-muted
.zz-pagination__item2.25rem 方形页码;当前页品牌填充 + --zz-brand-ink 文字;aria-disabled 压到 50%--zz-border-strong --zz-surface --zz-brand --zz-brand-ink
.zz-pagination__gap省略号占位,与页码同宽--zz-text-muted
通用所有过渡都是 var(--zz-dur) var(--zz-ease),只列需要过渡的属性--zz-dur --zz-ease

可访问性

  • 当前项一律是 aria-current="page",视觉只是它的结果;读屏用户听到的是同一件事, 颜色永远不是唯一的信号。
  • 吸顶栏与侧栏都是带 aria-label 的 <nav>;一页里出现多个<nav> 时名称不能省,否则读屏的跳转列表里全是「导航」。
  • 标签条用 role="tablist" / role="tab" / role="tabpanel"三件套:aria-selected 报选中,aria-controls 与aria-labelledby 把标签和面板连起来,面板自身 tabindex="0" 便于键盘滚动。
  • roving tabindex:只有选中的标签是 tabindex="0",其余是 -1。 因此 Tab 键一次就离开标签条进入面板,标签之间用左 / 右箭头(Home / End 跳首尾), 焦点会跟着选中一起走。
  • 分页在边界处渲染成不可聚焦的 <span aria-disabled="true">:既不是死链, 也不会让键盘撞进一个按不动的元素。省略号 aria-hidden,读屏只听到页码。
  • :active 用 scale(0.96) 而非位移:标签与页码这类按钮型控件有按下反馈, 纯文字链接(栏体链接、侧栏、面包屑)不缩放 —— 文字缩放会抖,这也是文档站顶栏的既有做法。
  • 横向滚动容器里的焦点环保留,只是内缩 2px(否则外扩的环会被裁掉)—— 这是让它可见,不是抹掉它。
  • prefers-reduced-motion: reduce 下这一族的所有过渡关闭;导航本来就是瞬时改色, 关掉不影响任何信息的传达。