客户:「可以,但是不仅仅应该是调整玻璃这个效果吧,整体的 UI 视觉升级这只是一个小点吧」
玻璃只是清单里的一小条。本轮是整体升级,六大板块,玻璃排最后。
依据:进阶调研实测报告(Chrome 153 headless 实测,含 text-box / text-autospace / grid-template-rows / scroll-driven animation / mask-composite 在真实浏览器里的验证结果)。
「不俗」靠的是「没有一样东西做错」;「惊艳」靠的是「有一样东西做得比别人深」。
上一轮 42 条数值全部正确 —— 这正是问题:正确 = 无特征。 做得越像 Vercel/Linear,越像模板。要学他们的约束方法,不是他们的视觉皮肤。
惊艳感的三个真正来源(按性价比排序):
这一块是 90% 中文看板都没做的,做到了立刻不一样。全部有实测数据支撑。
问题:中文字体自带半行距,padding:16px 得到的视觉留白实际是 16px + 字体半行距(中文约 0.4em),
永远对不准 4pt 栅格 —— 这才是「看着规矩但就是不齐」的真因。
实测:40px 中文文本行盒高度 55px → 30px(-45%)。
/* 所有标题、KPI 数字、单行标签都加 */
.stat-value, h1, h2, h3, .card-title, .metric {
text-box: trim-both cap alphabetic;
line-height: 1;
}
落地范围:所有「单行文本容器」——大数字、标题、标签、按钮文字。 不要加在多行正文上(会破坏行距)。
/* 单位:小一号、灰一档、留 0.12em */
.u { margin-left: .12em; font-size: .92em; color: var(--text-2); }
数据层插入:text.replace(/(\d+(?:\.\d+)?)\s*(%|万|元|件|天|份|人)/g, '$1<span class="u">$2</span>')
效果:82份 → 82 大而黑 + 份 小而灰,数字立刻成为视觉主角。
实测结论:Chrome 153 对「S5场景与卖点设计TOP3」应用 text-autospace 后,
渲染宽度 436.33px,与不加完全一致,getComputedStyle 仍返回 no-autospace。
MDN 标 Baseline 2025,但 W3C 明确各浏览器行为不一致、WebKit 只单侧加距。
可靠做法:数据层插 U+2009 THIN SPACE(约 1/5 em)。
function spaceCJK(s) {
return s
.replace(/([\u4e00-\u9fa5])([A-Za-z0-9])/g, '$1\u2009$2')
.replace(/([A-Za-z0-9])([\u4e00-\u9fa5])/g, '$1\u2009$2');
}
禁止:用手插全角空格或普通空格冒充中英间距(1em 太宽)。
--font-sans:
"Inter var", "Inter",
"PingFang SC", "Noto Sans SC", "Microsoft YaHei UI", "Microsoft YaHei",
sans-serif;
--font-num:
"Inter var", "SF Pro Display", -apple-system, "Helvetica Neue", sans-serif;
为什么不能 system-ui 打头:Windows 上拉丁字母会走雅黑的西文字形,字重和度量都不理想。 顺序是设计决策:Inter 在前接管拉丁字母与数字,中文只在其缺字时回落。
注意:不要引入几 MB 的中文字体包。Inter 只加载 latin subset,中文走系统字体。
正文 line-height: 1.75(W3C CLReq:中文行间隙为字符框 50%-100%,75% 取中位)
行宽上限:48em(约 48 字);卡片内描述 32-36em
正文 letter-spacing: 0 ← 中文是满格方块,加字距反而散
12-13px 全中文小标题:+0.02em ← 只为提清晰度
≥28px 大标题:-0.01em ~ -0.02em
中文负字距上限 -0.03em,超过笔画会粘连(西文 h1 可用 -4.75%,中文不行)。
.icon-label > svg { transform: translateY(-0.5px); } /* 中文视觉重心比拉丁低 */
.num-inline { position: relative; top: -0.02em; } /* 数字与汉字 baseline 混排 */
核心规则(Tufte):sparkline 只表达形状,不替代数字。
「A sparkline shows shape, not values: there is no axis, and the dot marks the latest point. Print the number it summarizes beside it.」
❌ 环比上月增长 12.4%,呈上升趋势
✅ 12.4% ↗ [sparkline] ← 删掉 12 个汉字
<div class="spark" aria-hidden="true">
<i style="--v:.2"></i><i style="--v:.45"></i><i style="--v:.3"></i>
<i style="--v:.7"></i><i style="--v:.55"></i><i style="--v:.9"></i>
<i style="--v:1" data-now></i>
</div>
.spark { display:flex; align-items:flex-end; gap:2px; height:20px; width:64px; }
.spark i {
flex:1; min-width:2px; border-radius:1px;
height:calc(var(--v) * 100%);
background:currentColor;
opacity:.35; /* 历史柱:低不透明度 */
}
.spark i[data-now] {
opacity:1; /* 最新一根:实色,形成"落点" */
border-radius:1px 1px 0 0;
}
尺寸规范:行内 1em 高 / 表格单元格 16px 高 60-80px 宽 / KPI 卡内 20-24px 高。
<svg class="ring" viewBox="0 0 36 36" width="36" height="36" aria-hidden="true">
<circle cx="18" cy="18" r="15.9" class="ring-track"/>
<circle cx="18" cy="18" r="15.9" class="ring-fill" style="--pct:68"/>
</svg>
.ring { transform: rotate(-90deg); } /* 起点拨到 12 点 */
.ring-track { fill:none; stroke:rgba(0,0,0,.06); stroke-width:3; }
.ring-fill {
fill:none; stroke:var(--accent); stroke-width:3; stroke-linecap:round;
stroke-dasharray:100; /* r=15.9 → 周长≈99.9 */
stroke-dashoffset:calc(100 - var(--pct));
transition: stroke-dashoffset 600ms cubic-bezier(.4,0,.2,1);
}
尺寸:36px 卡内嵌 / 44px 列表行 / 56px+ 主 KPI。低于 32px 别用环(看不出弧长差),改用小横条。
.bars .row { display:grid; grid-template-columns:64px 1fr 40px; align-items:center; gap:8px;
font-size:12px; line-height:1; }
.bars .track { height:4px; border-radius:2px; background:rgba(0,0,0,.06); overflow:hidden; }
.bars .fill { height:100%; border-radius:2px; background:currentColor;
width:calc(var(--v)*1%);
transition:width 500ms cubic-bezier(.4,0,.2,1); }
数值:track 高 4px(3px 看不清圆角,6px 显笨)、圆角=高度/2、行距 6px、标签列 64px。
.heat { display:grid; grid-auto-flow:column; grid-template-rows:repeat(7,10px); gap:2px; }
.heat i { width:10px; height:10px; border-radius:2px;
background: color-mix(in oklab, var(--accent) calc(var(--v)*100%), var(--surface-2)); }
用 color-mix(in oklab, ...) 而不是手写 5 个色阶 —— 保证任意强调色都生成视觉等距的 5 档。
格子 10px + gap 2px 是 GitHub contribution graph 的成熟尺寸。
.kv { display:flex; align-items:baseline; gap:8px; font-size:13px; }
.kv::before { content:""; order:1; flex:1;
border-bottom:1px dotted color-mix(in srgb, currentColor 20%, transparent);
transform:translateY(-3px); }
.kv .k { order:0; color:var(--text-2); }
.kv .v { order:2; font-variant-numeric:tabular-nums; color:var(--text-1); }
比「标签\n值」两行省一半高度。
卡片内 padding = 16px (--space-lg)
卡片内元素之间 gap = 12px (--space-md)
卡片与卡片之间 = 24px (--space-xl) ← 1.5×
区块(section)之间 = 48px (--space-xl×2) ← 3×
页面顶 → 首个区块 = 48px(不能更大)
上一轮的缺陷 1 根因:告警框与下方区块同属区块级,必须同为 24px。 不是数值错了,是跨级关系没区分。
:root {
--density-space-xs: 4px; --density-space-sm: 8px; --density-space-md: 12px;
--density-space-lg: 16px; --density-space-xl: 24px;
}
[data-density="compact"] {
--density-space-xs: 2px; --density-space-sm: 6px; --density-space-md: 8px;
--density-space-lg: 12px; --density-space-xl: 16px;
}
.card { padding: var(--density-space-lg); }
一个 data-density 属性重写 8 个 token,全站自动跟随。
不是「间距小」,是「同类项贴着、异类项留白」。 Linear 的 issue 列表每行内元素间距 4-8px(极密),但组与组之间 24px+。
检验方法:把页面缩成 25% 缩略图看。
content-visibility: auto(Chrome 153 实测支持),折叠区默认不渲染上一轮实测:阴影有 8 个变体,要求 3 档。这是工程问题,不是审美问题。
/* Tier 1 Primitive(组件禁止直接引用) */
:root {
--sh-1: 0 1px 2px rgba(16,24,40,.04);
--sh-2: 0 2px 8px -2px rgba(16,24,40,.06);
--sh-3: 0 12px 32px -8px rgba(16,24,40,.12);
--ring-1: 0 0 0 1px rgba(16,24,40,.06);
}
/* Tier 2 Semantic(只有 3 档,强制收敛的唯一手段) */
:root {
--elev-1: var(--sh-1), var(--ring-1); /* 静置 */
--elev-2: var(--sh-2), var(--ring-1); /* 悬停 */
--elev-3: var(--sh-3), 0 0 0 1px rgba(16,24,40,.08); /* 浮层 */
}
/* Tier 3 Component(组件只认语义名) */
.card { box-shadow: var(--elev-1); }
.card:hover { box-shadow: var(--elev-2); }
.popover { box-shadow: var(--elev-3); }
grep 拦截:
grep -rn "box-shadow" src/ | grep -v "elev-\|--sh-\|--elev-" && echo "❌ 有脱离 token 的阴影"
运行时断言(加进 scripts/check.mjs 或验收脚本):
const seen = new Set();
document.querySelectorAll('*').forEach(el => {
const v = getComputedStyle(el).boxShadow;
if (v && v !== 'none') seen.add(v);
});
console.assert(seen.size <= 3, `阴影变体 ${seen.size} 个,超过 3 档`, [...seen]);
把 1px 边框做成 shadow 的一层,而不是 border:
--ds-shadow-border-base: 0 0 0 1px #00000014;
好处:border 参与盒模型(hover 改宽度会抖动布局),box-shadow 不参与 ——
hover 加描边不会让卡片内容位移 1px。
:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--bg-1), /* 内环:和卡片同色,制造空隙 */
0 0 0 4px var(--accent); /* 外环:可见指示 */
}
❌ 页面加载时 0 → 1284 滚 2 秒 (AI 味 / 模板感)
✅ 数据刷新时 1284 → 1301,只有十位和个位滚 (高级)
✅ 首屏不动,等真实数据到达后再滚一次 (可信)
.num {
display:inline-block;
font-variant-numeric: tabular-nums; /* 必须:保证位宽不跳 */
height:1em; overflow:hidden; line-height:1;
mask-image: linear-gradient(#000 70%, #0000); /* 上下淡出,模拟机械感 */
}
.num > .slot { display:block; transition: transform 500ms cubic-bezier(.4,0,.2,1);
transform: translateY(calc(var(--i,0) * -1em)); }
参数:500-750ms、cubic-bezier(.4,0,.2,1),绝不用 ease-in(会显得拖沓)。
.card {
transition:
box-shadow 180ms cubic-bezier(.4,0,.2,1),
background-color 180ms cubic-bezier(.4,0,.2,1),
color 240ms cubic-bezier(.4,0,.2,1), /* 文字慢 60ms,避免"字在闪" */
transform 150ms cubic-bezier(.4,0,.2,1);
}
文字颜色若与背景同步,会视觉上抢跑,显得廉价。
.card { position:relative; isolation:isolate; }
.card::before {
content:""; position:absolute; inset:0; padding:1px; border-radius:inherit;
background: radial-gradient(180px circle at var(--x,50%) var(--y,50%),
color-mix(in srgb, var(--accent) 60%, transparent), transparent 60%);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor; mask-composite: exclude;
opacity:0; transition: opacity 240ms cubic-bezier(.4,0,.2,1);
pointer-events:none;
}
.card:hover::before { opacity:1; }
card.addEventListener('pointermove', (e) => {
const r = card.getBoundingClientRect();
card.style.setProperty('--x', (e.clientX - r.left) + 'px');
card.style.setProperty('--y', (e.clientY - r.top) + 'px');
});
半径 180px、不透明度 60%、衰减点 60% —— 三个数一起决定「光斑够近才亮」。 整卡发光 = 廉价。
把卡片底部密集的决策信息默认收起,hover 展开:
.card .detail {
display:grid; grid-template-rows:0fr; opacity:0;
transition: grid-template-rows 220ms cubic-bezier(.4,0,.2,1), opacity 180ms;
}
.card .detail > div { overflow:hidden; min-height:0; }
.card:hover .detail, .card:focus-within .detail { grid-template-rows:1fr; opacity:1; }
0fr → 1fr 是唯一正确的 auto-height 动画方案。
禁止 max-height:999px —— 模板感重灾区,速度失真。
@supports (animation-timeline: view()) {
.reveal { animation: rise linear both; animation-timeline: view();
animation-range: entry 10% cover 30%; }
@keyframes rise { from{opacity:0; transform:translateY(12px);} to{opacity:1; transform:none;} }
}
@media (prefers-reduced-motion: reduce) { .reveal { animation:none; opacity:1; } }
偏移 12px,不是 40px。 超过 24px 就开始像 AOS 模板库。
.label {
word-break: keep-all; overflow-wrap: anywhere;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden; line-height: 1.4;
}
flex 子项补 min-width: 0。
「S5 场景与卖点设计」19 字在 260px 卡内折成两行而不是截断(2 行 = 42px)。
Vercel 设计团队的 hard reject 清单明确包含 glass effects,理由是: 全页玻璃 = 性能灾难 + 内容糊。
折中方案(客户已批准):
内容卡片减薄:改成更实的白底(rgba(255,255,255,.72))+ 1px 发丝描边 + 内高光,只留极轻微透明
/* 浮层(保留玻璃) */
.glass-surface { background: rgba(255,255,255,.62);
backdrop-filter: blur(28px) saturate(180%); }
/* 内容卡(减薄,靠描边和内高光做质感,不靠糊) */
.glass-card {
background: rgba(255,255,255,.72);
backdrop-filter: blur(8px); /* 轻微 */
box-shadow: inset 0 1px 0 rgba(255,255,255,.9), var(--elev-1);
}
Vercel 设计团队原话:Hard reject decorative gradients, gradient text, glows, blobs, stripes, textures, grid backgrounds, glass effects, colored side rails, ornamental shadows, and fake depth.
background-clip:text + 紫蓝渐变)border-left:4px solid red)→ 改 6% 染色背景 + 1px 同色半透明描边backdrop-filter: blur()(本轮处理)box-shadow: 0 0 40px accent)100vh 满屏英雄区 + 大字口号transform: scale(1.05) hover → 挤压邻元素max-height: 999px 展开动画#000 / #FFF → 用近中性(#FAFAFA / #F2F2F2 / #EBEBEB / #171717)transition: all ← Vercel 明令禁止逐条自查并报告:现有代码命中哪几条。
三要素:
保留骨架 —— 空态的卡片框仍在,只把内容换提示。卡片消失 = 布局跳动 = 廉价
.empty {
display:grid; place-items:center; gap:8px;
min-height:120px; /* 与有数据时近似等高 */
color:var(--text-3); font-size:13px;
border:1px dashed color-mix(in srgb, currentColor 24%, transparent);
border-radius:10px;
}
.alert {
border-left: none;
background: color-mix(in srgb, #E5484D 6%, var(--surface-1)); /* 6% 染色,不是 12% */
box-shadow: var(--elev-1), 0 0 0 1px color-mix(in srgb, #E5484D 16%, transparent);
border-radius:10px; padding:12px 14px; font-size:13px; line-height:1.6;
}
6% 染色是关键数值。13%(常见默认)会让整个区块变粉,立刻廉价。
[data-density] / CSS 变量),便于回退text-box 落地范围的元素数量 + 一个 40px 标题的实测行盒高度(应约 30px,原 55px)82份 中 份 的 computed font-size 与 colordata-density 切换是否生效(切 compact 报卡片 padding)grid-template-rows 是否 0fr→1fr,时长transition: all 剩余数量(必须 0)scrollWidth === clientWidthnode --test 46/46 + node scripts/check.mjs 全绿docs/screenshots/v11/:
01-桌面-工作台 / 02-中文排版对比 / 03-sparkline落地 /
04-数字滚动 / 05-hover展开 / 06-光斑边框 / 07-空态 / 08-错误态 /
09-手机-工作台 / 10-手机-类型页 / 11-修前修后对比
P0(差异点,做了立刻不一样)
P1(手感)
P2(打磨)
先做 P0。中文排版是这一轮的核心价值,其他都是配菜。