跳到正文
册页

基础

空间与形状

页面的边距、几种度量宽度,以及五个圆角。

度量宽度以 ch 而不是 px 为上限,所以它跟随它所排的字。--measure-record 是列表记录描述文字的天花板,不是宽度:更窄的栏依然胜出。

只有一条阶梯,以及带动整条阶梯的一个系数:每一级都是 --radius-factor 的倍数,所以主题只改一个数,各级之间的比例不变。这正是嵌套能对上的前提——两条相隔 p 的圆角边只有在“内圆角 = 外圆角 − p”时才是同心的,--radius-row 和 --radius-frame 分别给出了这两个方向。50% 的圆是几何形状而不是圆角,它压根不在这条阶梯上。

每个组件都用逻辑属性书写——ps-/pe- 而不是 pl-/pr-,start-/end- 而不是 left-/right-——所以从右到左的文档不需要自己的样式表就能镜像。构建时有测试会因为一个物理属性而失败:事后给四十个组件补方向支持,是没人会排期的清扫,而且它出错时没有任何声音。上面任何一个例子都可以切到 RTL 看看。

接着读形状图标高度

布局

布局 tokens
样例Token值备注
--maxw1288px—
--col8.3333%—
--row52px—
--padclamp(18px, calc(4.4 * var(--fluid)), 64px)—
--secclamp(54px, calc(7 * var(--fluid)), 96px)—
--sec-tightclamp(40px, calc(5 * var(--fluid)), 68px)—
--bar-hcalc(var(--control-h-md) + 0.75rem)—
--secondary-textvar(--ink-3-aa)—
--page-padvar(--pad)─── Layout ─────────────────────────────────────────────────────────────
--w-pagevar(--maxw)—
--w-reading46remCaps the prose track only, which is what a 73ch measure is for.
--measure-record66chThe cap on a listed record's description — a row, a card, a CV entry. In `ch` rather than px so it tracks the type it is set in, and one token rather than the six hand-picked widths surfaces had drifted into. A ceiling, not a width: a narrower column still wins.
--scroll-offsetcalc(var(--bar-h) + 1.5rem)How far below the viewport top an anchored heading comes to rest — a fixed masthead plus a line of air. Both halves are now named: the masthead is `--bar-h`, so a bar that moves takes its anchors with it, where `88px` was a number somebody derived once by hand and nothing kept in step.

圆角

圆角 tokens
样例Token值备注
--radius-factor1—
--radius-gatemin(1, var(--radius-factor))Clamps the factor at 1 for the ADDING direction only. A frame's air is a fixed number of pixels, so `--radius-lg + 16px` would leave a rounded frame around a square panel the moment the theme went square; gated, it collapses to nothing along with everything else.
--radius-xscalc(4px * var(--radius-factor))a mark inside a tight box
--radius-smcalc(6px * var(--radius-factor))chips, keys, list rows
--radiuscalc(8px * var(--radius-factor))inputs, code, small plates
--radius-lgcalc(12px * var(--radius-factor))cards, dialogs, menu panels
--radius-pillcalc(999px * var(--radius-factor))capsules and counters
--radius-rowmax(0px, calc(var(--radius-lg) - 0.375rem))a row in a 6px-padded panel
--radius-framecalc(var(--radius-lg) + 1rem * var(--radius-gate))a frame 16px outside a panel

密度

第二条主题轴,也是唯一的另一条。在任何容器上写 data-density="compact",它下面所有控件一起收紧,不需要逐个告知。默认状态下一个中号控件是 44px,也就是 WCAG 2.5.5 要求的指针目标;紧凑模式降到 36px,仍然宽裕地通过 2.5.8,但不再满足 2.5.5。它是给鼠标驱动的密集桌面工具用的,是一笔真实的取舍而不是白拿。上面任何一个例子都可以切过去看。

密度 tokens
样例Token值备注
--control-h-sm2.25rem36px
--control-h-md2.75rem44px — WCAG 2.5.5
--control-h-lg3rem48px
--control-px-sm1rem—
--control-px-md1.5rem—
--control-px-lg1.75rem—
--control-py-sm0.5remThe vertical padding is part of the axis too, and has to be: a control whose padding alone exceeds the compact height never shrinks, because `min-height` is a floor and not a ceiling.
--control-py-md0.75rem—
--control-py-lg0.75rem—
--control-lh1.2And so is the LEADING, which is the term that guard was missing. `min-height` is a floor, and what has to stay under it is the whole box: the line, the padding and the border. The padding was scaled and the line was left inheriting the body's 1.6 — so `sm` measured 20.8 + 16 + 2 = 38.8 against a 36px token, `md` 46 against 44 and `lg` 50 against 48. Every text button in the system was two to three pixels taller than the number documenting it, and the floor never bound because the box had already cleared it. One value on both densities: the font sizes do not move with density, only the padding and the heights do, so this does not have to. It is not a reading leading — a control's label is one line, and 1.2 leaves every size at every density inside its own height with room to spare. The test in `tokens.test.ts` is what keeps that true.
--control-gap0.625rem—
--field-px0.875remText inputs are measured separately: they are read as much as they are hit, so their padding tracks the type rather than the target.
--field-py0.625rem—

图标

图标 tokens
样例Token值备注
--ico-s14px─── Icons ─── three sizes, and no weight: a Remix Icon glyph is a filled path.
--ico-m16px—
--ico-l20px—

层叠顺序

层叠顺序 tokens
样例Token值备注
--z-rule1─── Stacking ─── seven ranks, named for what sits at each. Every overlay in the package portals to document.body, so all of them are siblings in the ROOT stacking context and the rank is the whole of the decision — there is no ancestor left to nest one inside another. Read it as a sentence: a sticky header clears the page, a drawer clears the header, a scrim covers both, a modal sits on its scrim, an ANCHORED panel clears the modal it was opened from, and a toast clears everything because it is the only rank that has to survive a modal asking a question. --z-anchored is the one that is not obvious, and it is the one that was wrong. A popover, a menu or a select listbox is opened FROM something, and the surface it is most often opened from is a modal — a select inside a modal form is one of the most ordinary shapes in an application. Ranked below the modal, as it was, the panel is painted behind the very dialog that summoned it. The rank it replaced, --z-palette, was read by nothing: a command palette is a Dialog, so it lands at --z-modal, and its order against a second modal is settled by document order — which is the right answer, because it moves a scrim and its panel together.
--z-sticky10—
--z-drawer100—
--z-scrim200—
--z-modal210—
--z-anchored220—
--z-toast300—
--z-dropdownvar(--z-anchored)─── Stacking ─── two component-facing names onto the seven ranks. ────── The other five ranks are read by their own names; only these two had a component-facing spelling that outlived the rank it pointed at. --z-dropdown pointed at --z-drawer, 100, which put every anchored panel under the modal at 210 that opened it.
--z-overlayvar(--z-scrim)—