构成主义 Web 技术实现手册
每个配方都给出:要解决什么视觉问题 → 可复制代码 → 坑 → 兼容性。 兼容性结论来自 MDN 页面上的 Baseline 标识(本环境实测抓取),不是凭印象。
0. 兼容性核查表(实测结果)
| 特性 | MDN Baseline 状态 | 用途 | 备注 |
|---|---|---|---|
clip-path |
✅ Widely available(2020-01 起全浏览器) | 斜切、楔形、裁切揭示 | 主力工具 |
mix-blend-mode |
✅ Widely available(2020-01 起,部分子特性有差异) | 套印、双色调、文字压图 | 会创建层叠上下文 |
background-blend-mode |
✅ Widely available | 背景层叠(纸+网点) | 只混合自身背景层 |
text-wrap |
✅ Widely available(部分子特性有差异) | balance / pretty 断行 |
标题必用 |
shape-outside |
✅ Widely available(部分子特性有差异) | 文字绕几何形 | 需配合 float |
mask |
✅ Widely available | 网点遮罩、渐隐 | 加 -webkit- 前缀更稳 |
writing-mode |
✅ Widely available(部分子特性有差异) | 竖排文字 | 中文竖排可用 |
font-variant-numeric |
✅ Widely available | 表格数字对齐 | 编号列必用 |
SVG feTurbulence |
✅ Widely available | 颗粒/噪声 | 内联为 data-URI |
SVG textPath |
✅ Widely available(部分子特性有差异) | 沿路径/沿圆排字 | 必须配 <path id> |
filter: contrast() |
✅ Widely available | 照片加工 | — |
container-type |
✅ Widely available(部分子特性有差异) | 组件级响应式 | — |
:has() |
✅ Widely available(部分子特性有差异) | 父级/兄弟选择 | — |
@property |
⚠️ 未验证 | 注册自定义属性做动画 | 使用前请自行在 MDN 确认 |
animation-timeline(滚动驱动动画) |
⚠️ 未验证 | 滚动联动动效 | 使用前请自行确认 |
text-spacing-trim |
⚠️ 未验证 | 中文标点压缩 | 使用前请自行确认 |
后三项在本环境抓取 MDN 时未取到 Baseline 标识,不做断言。它们是加分项,不是构成主义风格的必需品——不依赖它们也能 100% 实现这套风格。
1. 纸张底 + 印刷颗粒
问题:纯白背景是"屏幕感",构成主义必须是"纸感"。但直接用纹理图片会有:体积大、拉伸变形、色彩偏移。
解法:SVG feTurbulence 内联为 data-URI,程序生成颗粒。
:root {
--kv-paper: #EFE9DC;
/* feTurbulence 生成的噪声纹,baseFrequency 越小颗粒越大 */
--kv-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.42'/%3E%3C/svg%3E");
}
body {
background-color: var(--kv-paper);
background-image: var(--kv-grain);
background-size: 160px 160px;
background-blend-mode: multiply; /* 让噪声成为"墨点"而不是"白霜" */
background-attachment: fixed; /* 可选:滚动时纸不动,更像印刷品 */
}
坑:
- baseFrequency 调大 → 颗粒更细;调到 0.9+ 会接近纯噪声,反而更脏。0.7–0.85 是甜区。
- opacity 必须压到 0.04–0.08 区间(这里靠 SVG 内部 opacity + background-blend-mode: multiply 双重压低)。超过 0.1 就从"纸"变成"脏屏幕"。
- 🔴 URL 编码:# 必须写成 %23。 这是最容易忘、且一忘就整个滤镜失效的坑(#a 会被浏览器当成 URL fragment 截断)。同理 % → %25(所以 100% 要写 100%25),< → %3C,> → %3E。内部属性用单引号,外层用双引号。
- 🔴 性能:内联 SVG feTurbulence 是"每次重绘都要重新光栅化滤镜"的。全屏常驻(比如挂在 body 上)在生产环境是有代价的 —— 滚动、缩放、合成都会触发重算。
- 全屏常驻颗粒层 → 换成无缝 PNG/WebP 贴图(GPU 平铺,成本极低)::root { --kv-grain: url("./grain-160.png"); }
- 小面积元素(卡片、Hero、单个色块)用 SVG 版本完全没问题
- background-size 一律用固定 px,不要用 % / cover,否则滤镜被反复重算
- 🔴 不要用 background-attachment: fixed —— 移动端会强制全屏重绘。想要"纸不动"的观感,改用独立固定层(务必 pointer-events: none)。
- 独立覆盖层必须加 pointer-events: none,否则会挡住整页点击。
- 可用 contain: paint 或 content-visibility 把滤镜重算锁在局部。
/* 若确实需要独立固定颗粒层 */
.kv-grain-layer {
position: fixed; inset: 0; pointer-events: none; z-index: 9999;
background-image: var(--kv-grain); background-size: 160px 160px;
mix-blend-mode: multiply; opacity: .5;
}
💡
background-blend-mode只混合元素自己的多层背景,要让文字也"压"进纸里,只能用mix-blend-mode+isolation。这是两者的分工,别混。详见05-前端技术方案.md第 7 节。
2. 斜向网格底纹("可见的构造")
问题:构成主义的网格是露出来的。但用 <table> 或大量 div 画网格很蠢。
解法:两组 repeating-linear-gradient 叠加。
.kv-grid-bg {
background-image:
repeating-linear-gradient(90deg, rgba(18,16,14,.07) 0 1px, transparent 1px 6.25%),
repeating-linear-gradient(0deg, rgba(18,16,14,.07) 0 1px, transparent 1px 48px);
background-size: 100% 100%;
}
斜向网格(更有构成主义味):
.kv-grid-diagonal {
background-image:
repeating-linear-gradient(60deg, rgba(18,16,14,.06) 0 1px, transparent 1px 28px),
repeating-linear-gradient(-60deg, rgba(18,16,14,.06) 0 1px, transparent 1px 28px);
}
坑:透明度超过 0.1 就会和正文抢注意力。0.05–0.08 是上限。
3. 主斜轴(贯穿全页的"主梁")
问题:一条从页面顶部贯穿到底部的斜线,是构成主义最重要的动势来源。
解法:position: fixed + transform: rotate(),或用 clip-path 做一个斜切色带。
.kv-axis {
position: fixed;
inset: -20% -10%;
width: 140%;
height: 14px;
background: var(--kv-ink);
transform: rotate(-15deg); /* 与 --kv-axis-angle 保持一致 */
transform-origin: center;
pointer-events: none;
z-index: 1; /* 在内容之下、背景之上 */
mix-blend-mode: multiply;
}
/* 变体:斜切色带(更宽,带面积) */
.kv-axis--band {
height: 38vh;
background: var(--kv-red);
opacity: .94;
z-index: 0;
}
坑:
- 用 transform: rotate() 而不是 clip-path 画细线——旋转更简单且抗锯齿更好。
- 斜轴必须在 z-index 上低于内容,且 pointer-events: none,否则会拦截点击。
- mix-blend-mode: multiply 会让斜轴与纸张纹理叠印,产生"印上去"的感觉。但它会创建层叠上下文,可能影响内部元素的 z-index(见第 7 节)。
4. 楔形(最具识别度的符号)
问题:红楔子刺入白圆是构成主义的"Logo"。网页里要能反复使用且有动势。
解法:clip-path: polygon()。注意坐标系:百分比是相对元素自身宽高的,所以宽高比变化时尖角会变形。
/* 基础楔形:从左下向右上刺入 */
.kv-wedge {
width: 100%;
aspect-ratio: 16 / 7; /* 锁死宽高比,防止尖角变形 */
background: var(--kv-red);
clip-path: polygon(0% 100%, 100% 8%, 100% 40%, 0% 100%);
}
/* 变体:双楔咬合(红+黑),制造"刺入"关系 */
.kv-wedge-clash { position: relative; aspect-ratio: 16/7; overflow: clip; }
.kv-wedge-clash::before,
.kv-wedge-clash::after {
content: ""; position: absolute; inset: 0;
}
.kv-wedge-clash::before {
background: var(--kv-paper);
clip-path: polygon(0 0, 62% 0, 20% 100%, 0 100%);
}
.kv-wedge-clash::after {
background: var(--kv-red);
clip-path: polygon(24% 0, 78% 0, 40% 100%, 0% 100%);
mix-blend-mode: multiply;
}
坑(前两条踩了会出功能性事故,不是审美问题):
- 🔴 clip-path 会把 outline 一起裁掉 → 键盘焦点环不可见 → WCAG 2.4.7(焦点可见)失败。
凡是可交互元素(a / button / input)都不要在它自己身上用 clip-path。
正确做法:把 clip-path 交给一个 aria-hidden="true" 的装饰子层,交互元素本体不裁剪:
html
<button class="kv-wedge-btn">
<span class="kv-wedge-btn__decor" aria-hidden="true"></span>
<span class="kv-wedge-btn__label">按此</span>
</button>
css
.kv-wedge-btn { position: relative; background: none; border: 0; padding: 0; }
.kv-wedge-btn__decor { position: absolute; inset: 0; background: var(--kv-red);
clip-path: polygon(0% 100%, 100% 6%, 100% 100%); z-index: 0; }
.kv-wedge-btn__label { position: relative; z-index: 1; display: block;
padding: 1rem 2rem; color: var(--kv-paper); }
.kv-wedge-btn:focus-visible { outline: 4px solid var(--kv-blue); outline-offset: 3px; }
- 🔴 非 none 的 clip-path 会新建 stacking context,从而截断 mix-blend-mode 的混合范围。
这就是"明明写了 multiply 却没变暗"的头号原因。同理会新建 stacking context 的属性还有:
opacity < 1、transform、filter、mask、will-change、contain: paint、position: fixed/sticky。
构成主义页面里 transform 和 clip-path 到处都是 → 混合范围很容易被切得七零八落。
对策:用 isolation: isolate 显式划出混合作用域,并把需要混合的层放在同一层里。
- clip-path 会把 box-shadow / border 一起裁掉 → 硬阴影要靠外层包裹元素,不能加在被裁元素上;
楔形也不能靠 border 描边。要描边有三条出路:① 双层(底层当描边色,上层坐标内缩)
② 用 SVG <polygon stroke> + paint-order: stroke fill(让描边压在填充之下,保住尖角)
③ 放弃描边。
- clip-path 元素内的文字也会被裁,长文本可能被切掉 → 文字放到外层,或用 shape-outside。
- 参考盒默认是 border-box(不是 padding-box),百分比相对参考盒解析。
想做随元素缩放的斜切,用 polygon() / shape(),不要用 path() —— path() 里是 px,不会缩放。
- 务必锁 aspect-ratio,否则响应式下楔子的尖角角度会乱变。
更完整的实测证据(BCD 版本号、坐标系陷阱、
fill-rule)见05-前端技术方案.md第 1 节。
5. 套印错位(misregistration)
问题:廉价印刷的红版和黑版对不齐,文字边缘有 1–3px 的红色重影。这正是"印刷感"最强烈的来源。
解法:一个真实的 <span aria-hidden="true"> 承载重影 + 偏移 + mix-blend-mode: multiply。
🔴 重要更正:不要用伪元素
content: attr(data-text)复制文字。 部分屏幕阅读器会朗读伪元素内容 → 标题被念两遍。这是本手册里最容易踩的无障碍坑, 而网上流传的"套印错位"教程几乎全都这么写。默认请用下面的真实 span 写法。
<h2 class="kv-misreg">ПОСТРОИМ БУДУЩЕЕ<span class="kv-misreg__ghost" aria-hidden="true">ПОСТРОИМ БУДУЩЕЕ</span></h2>
.kv-misreg {
position: relative;
display: inline-block; /* 让重影层有确定的包含块 */
isolation: isolate; /* 把 multiply 锁在组件内,避免混到 body 背景 */
color: var(--kv-ink);
}
.kv-misreg__ghost {
position: absolute;
inset: 0;
color: var(--kv-red);
mix-blend-mode: multiply;
transform: translate(3px, -2px); /* 偏移量就是"错版量" */
pointer-events: none;
user-select: none; /* 防止选中重影造成复制出重复文字 */
}
替代方案:text-shadow 三色偏移(唯一零无障碍代价的做法)
.kv-misreg-shadow {
color: var(--kv-ink);
text-shadow:
2.5px 0 0 rgba(214,43,31,.85), /* 红版偏移 */
-2.5px 0 0 rgba(0,81,214,.60), /* 蓝版偏移 */
0 3px 0 rgba(232,179,26,.55); /* 黄版偏移 */
}
- 优点:
text-shadow不被屏幕阅读器朗读,不产生重复文本节点,兼容性最好(Chrome/Safari/FF ≥10) - 缺点:不参与混合(按 normal 合成),做不出 multiply 叠印变暗的效果
坑:
- 偏移量 ≤ 3px。超过 5px 就从"印刷错版"变成"故障艺术(glitch)",那是另一种风格。
- 父元素若有背景色,重影可能被盖住 → 把重影提到本体之上(z-index: 1,本体 position: relative; z-index: 0)。
- Blink(Chrome)与 WebKit(Safari)的 mix-blend-mode 实现有可见差异 → 错位量不要卡在临界值,留 ±1px 冗余。
- 若必须用伪元素版本,请至少加 aria-hidden 的可访问性替身,并接受它只是"装饰性重复"。
更完整的兼容性数据与实测证据见 05-前端技术方案.md 第 6 节。
6. 双色调照片(把照片变成"材料")
问题:原色照片太"数字",且颜色会和红黑白打架。构成主义要的是"两色印刷的照片"。
解法 A:纯 CSS 双色调(推荐,零依赖)
.kv-duotone {
position: relative;
display: block;
filter: grayscale(1) contrast(1.25);
}
.kv-duotone img {
display: block; width: 100%;
filter: grayscale(1) contrast(1.15) brightness(1.02);
mix-blend-mode: luminosity; /* 只取原图的明度 */
}
.kv-duotone::after {
content: ""; position: absolute; inset: 0;
background: var(--kv-red); /* 用红色当"影子色" */
mix-blend-mode: multiply;
pointer-events: none;
}
/* 想要"黑→红"双色而不是"红→红"时,改用渐变映射思路: */
.kv-duotone--ink-red::after {
background: linear-gradient(160deg, var(--kv-ink) 0%, var(--kv-red) 100%);
mix-blend-mode: lighten;
}
解法 B:SVG 滤镜做真正的渐变映射(更准,适合要精确控色的场合)
<svg width="0" height="0" aria-hidden="true" style="position:absolute">
<filter id="kv-duotone" color-interpolation-filters="sRGB">
<feColorMatrix type="matrix"
values="0.299 0.587 0.114 0 0
0.299 0.587 0.114 0 0
0.299 0.587 0.114 0 0
0 0 0 1 0"/>
<feComponentTransfer>
<feFuncR type="table" tableValues="0.071 0.839"/> <!-- 黑 → 红 -->
<feFuncG type="table" tableValues="0.063 0.157"/>
<feFuncB type="table" tableValues="0.055 0.122"/>
</feComponentTransfer>
</filter>
</svg>
<img src="photo.jpg" style="filter: url(#kv-duotone)" alt="...">
可直接引用的现成实现:
- LukyVj/colofilter.css — 619★,MIT,纯 CSS 双色调滤镜库
- rickymetz/duotonegenerator — 89★,MIT,CSS 双色调生成器
坑:
- filter 会创建新的包含块,内部的 position: fixed 会失效。
- mix-blend-mode: luminosity 要求父元素有 isolation: isolate(或明确的层叠上下文),否则会混合到页面其他元素上。这是最常见的翻车点。
7. 半调网点(halftone)
问题:构成主义海报的照片有印刷网点。三种做法各有取舍。
做法 A:repeating-radial-gradient 做网点图案(最轻,纯 CSS)
.kv-halftone {
background-image: radial-gradient(var(--kv-ink) 1.5px, transparent 1.6px);
background-size: 6px 6px; /* 6px 网点间距 = 印刷网点尺度 */
}
/* 用 mask 让网点只出现在图片的暗部(模拟真实半调) */
.kv-halftone-masked {
-webkit-mask-image: radial-gradient(var(--kv-ink) 1.5px, transparent 1.6px);
mask-image: radial-gradient(var(--kv-ink) 1.5px, transparent 1.6px);
mask-size: 6px 6px;
}
做法 B:SVG feTurbulence + feColorMatrix 做有机网点(更有印刷脏感)
<svg width="0" height="0" aria-hidden="true" style="position:absolute">
<filter id="kv-halftone" x="0" y="0" width="100%" height="100%">
<feTurbulence type="fractalNoise" baseFrequency="0.55" numOctaves="1" result="noise"/>
<feColorMatrix in="noise" type="saturate" values="0" result="mono"/>
<feComponentTransfer in="mono" result="dots">
<feFuncA type="discrete" tableValues="0 1"/> <!-- 硬阈值 = 网点 -->
</feComponentTransfer>
<feComposite in="SourceGraphic" in2="dots" operator="in"/>
</filter>
</svg>
做法 C:mask + 位图网点(最可控,但要素材)
.kv-halftone-img {
-webkit-mask-image: url("halftone-tile.png");
mask-image: url("halftone-tile.png");
mask-size: 400px 400px;
mask-repeat: repeat;
}
坑:
- 网点尺寸 3–6px。小于 3px 在高分屏上会摩尔纹闪烁;大于 6px 会变成"圆点装饰画"。
- 网点绝对不能覆盖正文,会严重降低可读性(对比度实测会掉到 3:1 以下)。
- 做法 B 的 SVG 滤镜在大面积元素上性能开销明显,只用于单张图片,不要用于整页背景。
- 用 mask 时记得加 -webkit- 前缀(老 Safari)。
8. 沿路径 / 绕圆排字
问题:构成主义常把文字沿弧线或斜线排布。
解法:SVG <textPath>(这是唯一可靠的做法,CSS 做不到)。
<svg viewBox="0 0 400 400" class="kv-type-circle" role="img" aria-label="ПРОУН">
<defs>
<path id="kv-circle" d="M 200,200 m -150,0 a 150,150 0 1,1 300,0 a 150,150 0 1,1 -300,0"/>
</defs>
<circle cx="200" cy="200" r="150" fill="none" stroke="#12100E" stroke-width="2"/>
<text font-family="Oswald, sans-serif" font-size="26" fill="#12100E" letter-spacing="6">
<textPath href="#kv-circle" startOffset="0">ПРОУН · ПОСТРОЕНИЕ · НОВОГО ·</textPath>
</text>
</svg>
想让它转起来(构成主义海报里圆环常被想象成旋转的):
@keyframes kv-spin { to { transform: rotate(360deg); } }
.kv-type-circle { animation: kv-spin 40s linear infinite; transform-origin: 50% 50%; }
@media (prefers-reduced-motion: reduce) { .kv-type-circle { animation: none; } }
坑:
- <textPath> 的文本无法自动换行,超出路径长度会被直接截断。必须自己控制字数。
- 路径长度要略长于文字长度,startOffset 用来调起点。
- 必须给 SVG 一个可访问名称(role="img" + aria-label),否则读屏用户会听到无意义的字符流,或者完全听不到。这是无障碍红线。
- 用 href 而非 xlink:href(现代浏览器都支持;若需兼容极老浏览器再加 xlink:)。
9. 文字绕几何形(shape-outside)
问题:让文字顺着一个圆形/三角形/斜边流动,而不是规规矩矩排在矩形里。
.kv-shape {
float: left;
width: 240px; height: 240px;
margin: 0 1.5rem 1rem 0;
background: var(--kv-red);
border-radius: 50%; /* 视觉是圆 */
shape-outside: circle(50% at 50% 50%); /* 文字绕圆形流动 */
clip-path: circle(50% at 50% 50%);
}
/* 三角形楔子让文字绕斜边 */
.kv-shape--wedge {
border-radius: 0;
clip-path: polygon(0 0, 100% 0, 0 100%);
shape-outside: polygon(0 0, 100% 0, 0 100%);
}
坑:
- 必须配合 float,shape-outside 对非浮动元素无效。
- shape-outside 只影响外部文字环绕,不裁切元素自身——所以要同时写 clip-path。
- 浮动元素在 Flex/Grid 容器里会失效,必须放在普通块级流里。
10. 竖排文字
.kv-vertical {
writing-mode: vertical-rl;
text-orientation: mixed; /* 中文竖排用 mixed;纯西文可 upright */
letter-spacing: 0.12em;
}
坑:
- 🔴 writing-mode 会交换 inline 与 block 轴。 使用竖排的容器内部,所有 margin / padding / width / height 都必须改用逻辑属性(margin-block / padding-inline / inline-size / block-size),否则左右与上下的语义会整体错位——这是竖排最容易出的布局事故。
- 🔴 text-orientation: upright 会让拉丁字母变成"一堆正立字母",几乎不可读 → 只用于装饰性短标签。中文竖排是可读的,可以用于标题。
- 竖排文本复制出来的纯文本顺序可能异常,PDF 打印也可能乱序 → 主内容请提供水平版本。
- 屏幕阅读器一般能正确处理 vertical-rl,但不要用它承载主要信息层级。
11. 动效三件套
11.1 裁切框扫过(图片揭示)
@keyframes kv-reveal { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
.kv-reveal { animation: kv-reveal var(--kv-dur-slow) var(--kv-ease-mech) both; }
11.2 逐行跳出(标题 staggger)
.kv-stagger > * {
opacity: 0;
transform: translateY(0.6em) ;
animation: kv-jump var(--kv-dur-base) var(--kv-ease-mech) forwards;
}
.kv-stagger > *:nth-child(1) { animation-delay: 0ms; }
.kv-stagger > *:nth-child(2) { animation-delay: 50ms; }
.kv-stagger > *:nth-child(3) { animation-delay: 100ms; }
.kv-stagger > *:nth-child(4) { animation-delay: 150ms; }
@keyframes kv-jump { to { opacity: 1; transform: none; } }
11.3 色块硬切(hover)
.kv-btn {
background: var(--kv-ink); color: var(--kv-paper);
border: 2px solid var(--kv-ink);
border-radius: 0;
min-height: 44px; /* 触控目标红线 */
padding: 0.75rem 1.5rem;
font-family: var(--kv-font-display);
font-weight: var(--kv-weight-label);
letter-spacing: var(--kv-track-label);
text-transform: uppercase;
transition: background-color var(--kv-dur-fast) var(--kv-ease-mech),
color var(--kv-dur-fast) var(--kv-ease-mech);
cursor: pointer;
}
.kv-btn:hover { background: var(--kv-red); border-color: var(--kv-red); color: var(--kv-paper); }
.kv-btn:focus-visible { outline: 4px solid var(--kv-blue); outline-offset: 3px; }
.kv-btn:active { transform: translate(2px, 2px); }
.kv-btn[disabled] { opacity: .45; cursor: not-allowed; }
注意:hover 用 --kv-red 底 + 纸色字 = 4.15:1,按钮文字通常 ≥16px 粗体,刚好卡在 AA 大字阈值。若要严格达标,把 hover 底色改为 --kv-red-ink。
12. 极端字号与溢出防护
.kv-mega {
font-family: var(--kv-font-display);
font-size: var(--kv-size-mega);
line-height: var(--kv-leading-mega);
letter-spacing: var(--kv-track-mega);
text-transform: uppercase;
text-wrap: balance;
overflow-wrap: anywhere; /* 超长单词不得撑破容器 */
hyphens: auto;
}
/* 容器必须允许裁切,否则 vw 字号会在窄屏溢出 */
.kv-hero { overflow: clip; }
坑:clamp() 里的 vw 单位不感知容器宽度,只感知视口。如果这个组件被放在一个窄侧栏里,字号会照样巨大 → 该用 cqw(容器查询单位):
.kv-hero { container-type: inline-size; }
.kv-mega { font-size: clamp(2rem, 14cqw, 12rem); }
13. 对比度自检脚本
改动画板后必须重跑,不要目测。
function Lum([string]$hex) {
$hex = $hex.TrimStart('#')
$v = @(0,2,4) | ForEach-Object {
$c = [Convert]::ToInt32($hex.Substring($_,2),16) / 255.0
if ($c -le 0.03928) { $c / 12.92 } else { [Math]::Pow((($c + 0.055) / 1.055), 2.4) }
}
0.2126*$v[0] + 0.7152*$v[1] + 0.0722*$v[2]
}
function Ratio([string]$a,[string]$b) {
$la = Lum $a; $lb = Lum $b
[Math]::Round(($([Math]::Max($la,$lb)) + 0.05) / ($([Math]::Min($la,$lb)) + 0.05), 2)
}
Ratio '#B31B12' '#EFE9DC' # → 5.63 ✅ AA 正文
Ratio '#D6281F' '#EFE9DC' # → 4.15 ⚠️ 仅大字
判定线:≥4.5:1 = AA 正文合格;≥3:1 = 仅限 ≥24px 或 ≥18.66px 粗体。
14. 印刷级排版细节(最容易被忽略、也最"显专业"的一层)
构成主义海报的文字是排字工手工排的,所以有大量现代浏览器默认不做的细节。这一层做好了,"像印刷品"和"像网页"的差距就出来了。
13.1 悬挂标点
行首的引号、破折号应当溢出到版心之外,让文本左边缘看起来对齐(视觉边距对齐)。
.kv-hang { hanging-punctuation: first last; }
⚠️ hanging-punctuation 的兼容性本环境未取到 MDN Baseline 标识,不做断言。它目前主要在 Safari 生效。可靠替代:
- 用 davidmerfield/Typeset(2,680★, CC0)—— 它在构建期处理悬挂标点,无需客户端 JS,CSS <1KB
- 或对已知的少量标题手动 text-indent: -0.5em 微调
13.2 小型大写(small caps)
用于编号、缩写、"卷/期"这类次级信息,比缩小字号更专业。
.kv-smallcaps { font-variant-caps: all-small-caps; letter-spacing: 0.06em; }
/* 只对真实存在小 caps 字形的字体生效;否则浏览器会伪造成缩小的大写 —— 那很难看 */
⚠️ 前提:字体必须真的有小 caps 字形。Oswald / Unbounded 没有小 caps,所以不要在这两套字体上用 all-small-caps。需要小 caps 就用 PT Serif 或 Inter。
13.3 OpenType 特性
.kv-num-tabular { font-variant-numeric: tabular-nums; } /* 编号列对齐,必用 */
.kv-liga-off { font-variant-ligatures: none; } /* 大标题常关掉连字,字距更均匀 */
.kv-caps-alt { font-feature-settings: "ss01" 1; } /* 某些字体的单层 a / 方点 i 等 */
构成主义偏好:大标题关闭连字(font-variant-ligatures: none)—— 手工排字时代不会为标题启用连字,关掉后字面更"机械"、更均匀。
13.4 断词与断行
.kv-title { text-wrap: balance; } /* 标题:让各行长度均衡 */
.kv-body { text-wrap: pretty; } /* 正文:避免末行孤字 */
.kv-title, .kv-body { hyphens: auto; overflow-wrap: anywhere; }
⚠️ 中文注意:hyphens: auto 需要标注 lang。中文断行依赖 line-break 与 word-break:
:lang(zh) .kv-body { line-break: strict; } /* 严格避头尾 */
:lang(zh) .kv-title { word-break: keep-all; } /* 标题不在词内断开 */
text-spacing-trim 可压缩中文标点间距(让行首行尾的标点不占满字宽),但该特性兼容性本次未验证,建议先不用,或加 @supports 保护:
@supports (text-spacing-trim: trim-start) {
:lang(zh) .kv-body { text-spacing-trim: trim-start; }
}
13.5 竖排中的标点
writing-mode: vertical-rl 下,中文标点会自动旋转到正确位置(浏览器的 text-orientation: mixed 会处理)。但西文括号和引号需要检查,必要时用 text-combine-upright 组合数字/短西文:
.kv-vertical .kv-num { text-combine-upright: digits 2; } /* 两位数字横排在竖排文本里 */
15. 页面骨架清单
必需(缺了就不像构成主义):
- [ ] 全局 border-radius: 0
- [ ] 米黄纸底 + 极轻颗粒
- [ ] 一个统一的斜轴角度(CSS 变量)
- [ ] 极端字号对比(标题:正文 ≥ 6:1)
- [ ] 标题全大写 + 负字距 + 紧行高
- [ ] 大量纸白(红色面积 ≤ 15%)
- [ ] 至少一处几何原语(楔形/圆/粗线)
- [ ] 可见的网格线或编号系统
- [ ] prefers-reduced-motion 支持
加分(决定是"像"还是"精"): - [ ] 照片经双色调/半调加工 - [ ] 一处套印错位 - [ ] 一条贯穿式斜轴 - [ ] 沿圆或沿斜线排的短文字 - [ ] 竖排的短标签 - [ ] 硬边偏移阴影 - [ ] 中文字体与西文字体的视觉重量对齐
禁止(出现即出戏): - [ ] 圆角、柔和投影、渐变 - [ ] 紫/粉/蓝科技渐变 - [ ] Emoji 当图标 - [ ] 居中的对称布局(那是瑞士风格的领域) - [ ] 模糊背景(毛玻璃) - [ ] 弹性/回弹缓动动画