关于 web-design-guidelines
面向 Web 设计与开发者的跨框架指南,汇总无障碍、响应式布局、表单和排版规则,并以 WCAG 2.2、MDN Web Docs 和现代 Web API 为依据。
主要功能
- 01WCAG 2.2 无障碍实践
- 02语义 HTML 与 ARIA 用法
- 03移动优先响应式布局
- 04表单标签与校验建议
- 05字体与可读性规范
适用场景
- 01检查交互控件的键盘可用性
- 02规划适配不同视口的网页
- 03改进表单标签和错误提示
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ ehmo/ platform-design-skills --skill web-design-guidelines安装后在 Claude Code 中输入/web-design-guidelines确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
AGENTS.mdmetadata.jsonrules/_sections.md详细介绍
Web 平台设计指南
适用于各种框架的无障碍、高性能、响应式 Web 界面规则。基于 WCAG 2.2、MDN Web Docs 和现代 Web 平台 API。
1. 无障碍 / WCAG [CRITICAL]
无障碍并非可选项。本节的大多数规则对应 WCAG 2.2 的 A 级或 AA 级成功准则。少数最佳实践规则(已在正文中注明)以 AAA 级为目标或超出 WCAG 要求。
1.1 使用语义化 HTML 元素
按元素的预期用途使用元素。语义结构可免费提供无障碍、SEO 和阅读模式支持。
| 元素 | 用途 |
|---|---|
<main> | 页面主要内容(每页一个) |
<nav> | 导航区块 |
<header> | 介绍性内容或导航辅助内容 |
<footer> | 最近的分节内容的页脚 |
<article> | 独立完整、可单独分发的内容 |
<section> | 带标题的主题分组 |
<aside> | 相关性较弱的内容(侧边栏、提示框) |
<figure> / <figcaption> | 插图、图表、代码清单 |
<details> / <summary> | 可展开/折叠的披露控件 |
<dialog> | 模态或非模态对话框 |
<time> | 机器可读的日期/时间 |
<mark> | 高亮或引用的文本 |
<address> | 最近的文章/正文的联系信息 |
<main>
<article>
<h1>Article Title</h1>
<p>Content...</p>
</article>
<aside>Related links</aside>
</main>
<div class="main">
<div class="article">
<div class="title">Article Title</div>
<div class="content">Content...</div>
</div>
</div>
反模式:使用 <div> 或 <span> 作为交互元素。存在 <button> 时,绝不要编写 <div onclick>。
1.2 交互元素上的 ARIA 标签
每个交互元素都必须有无障碍名称。优先使用可见文本;仅在可见文本不足时才使用 aria-label 或 aria-labelledby(SC 4.1.2)。
<button aria-label="Close dialog">
<svg aria-hidden="true">...</svg>
</button>
<h2 id="section-title">Notifications</h2>
<ul aria-labelledby="section-title">...</ul>
<button>Save Changes</button>
1.3 键盘导航
所有交互元素都必须可通过键盘访问和操作(SC 2.1.1)。
- 使用原生交互元素(
<button>、<a href>、<input>、<select>),它们默认支持键盘操作。 - 自定义控件需要设置
tabindex="0"以进入 Tab 顺序,并添加用于激活控件的 keydown 处理程序。 - 绝不要使用大于 0 的
tabindex值。 - 将焦点限制在模态框内;关闭时恢复焦点。
// Focus trap for modal
dialog.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
const focusable = dialog.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
});
1.4 可见的焦点指示器
移除焦点轮廓时,必须提供可见的替代样式(SC 2.4.7;WCAG 2.2 中增强的 SC 2.4.11(AA)和 SC 2.4.12(AAA))。
/* Good: custom focus indicator */
:focus-visible {
outline: 3px solid var(--focus-color, #4A90D9);
outline-offset: 2px;
}
/* Remove default only when :focus-visible is supported */
:focus:not(:focus-visible) {
outline: none;
}
/* Bad: removing all focus styles */
/* *:focus { outline: none; } */
WCAG 2.2 要求焦点指示器的最小面积为组件周长乘以 2px,且与相邻颜色的对比度为 3:1。
1.5 跳过导航链接
提供跳过重复内容区块的机制(SC 2.4.1)。
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<nav>...</nav>
<main id="main-content">...</main>
</body>
.skip-link {
position: absolute;
top: -100%;
left: 0;
z-index: 1000;
padding: 0.75rem 1.5rem;
background: var(--color-primary);
color: var(--color-on-primary);
}
.skip-link:focus {
top: 0;
}
1.6 图像的替代文本
每个 <img> 都必须有 alt 属性(SC 1.1.1)。
- 信息性图像:描述其内容和功能。
alt="Bar chart showing sales doubled in Q4"。 - 装饰性图像:使用
alt=""(空字符串),以便屏幕阅读器跳过它们。 - 功能性图像(位于链接/按钮内):描述其操作。
alt="Search"。 - 复杂图像:使用
alt提供简短描述,链接到详细描述,或使用<figcaption>。
<img src="chart.png" alt="Revenue chart: Q1 $2M, Q2 $2.4M, Q3 $3.1M, Q4 $4.5M">
<img src="decorative-wave.svg" alt="">
1.7 色彩对比度
保持最低对比度(SC 1.4.3、1.4.6、1.4.11)。
| 内容 | 最低对比度 |
|---|---|
| 正文文本(<24px / 粗体 <18.66px) | 4.5:1 |
| 大号文本(>=24px / 粗体 >=18.66px) | 3:1 |
| UI 组件和图形对象 | 3:1 |
不要仅依靠颜色传达信息(SC 1.4.1)。将颜色与图标、文本或图案搭配使用。
/* Check contrast of these tokens */
:root {
--text-primary: #1a1a2e; /* on white: ~16:1 */
--text-secondary: #555770; /* on white: ~6.5:1 */
--text-disabled: #767693; /* on white: ~4.5:1, borderline */
}
1.8 表单标签
每个表单输入项都必须有以编程方式关联的标签(SC 1.3.1、3.3.2)。
<label for="email">Email address</label>
<input id="email" type="email" autocomplete="email">
<label>
Email address
<input type="email" autocomplete="email">
</label>
1.9 错误识别
以文本形式指出并描述错误(SC 3.3.1)。使用 aria-describedby 或 aria-errormessage 将错误消息与输入项关联。
<label for="email">Email</label>
<input id="email" type="email" aria-describedby="email-error" aria-invalid="true">
<p id="email-error" role="alert">Enter a valid email address, e.g. [email protected]</p>
1.10 ARIA 实时区域
向屏幕阅读器播报动态内容变化(SC 4.1.3)。
<div aria-live="polite" aria-atomic="true">
3 results found
</div>
<div role="alert">
Your session will expire in 2 minutes.
</div>
<div role="status">
File uploaded successfully.
</div>
默认使用 aria-live="polite"。仅将 role="alert" / aria-live="assertive" 用于有时效性的警告。
1.11 ARIA 角色速查表
| 角色 | 用途 | 原生等效项 |
|---|---|---|
button | 可点击的操作 | <button> |
link | 导航 | <a href> |
tab / tablist / tabpanel | 标签页界面 | 无 |
dialog | 模态框 | <dialog> |
alert | 紧急实时区域 | 无 |
status | 礼貌实时区域 | <output> |
navigation | 导航地标 | <nav> |
main | 主内容地标 | <main> |
complementary | 辅助内容地标 | <aside> |
search | 搜索地标 | <search> (HTML5) |
img | 图像 | <img> |
list / listitem | 列表 | <ul>/<li> |
heading | 标题(使用 aria-level) | <h1>-<h6> |
menu / menuitem | 菜单组件 | 无 |
tree / treeitem | 树形视图 | 无 |
grid / row / gridcell | 数据网格 | <table> |
progressbar | 进度 | <progress> |
slider | 范围输入 | <input type="range"> |
switch | 切换开关 | <input type="checkbox"> |
规则:优先使用原生 HTML,而不是 ARIA。仅当该模式没有对应的原生元素时才使用 ARIA。
1.12 名称中的标签(WCAG 2.5.3 A 级)
当交互元素具有可见文本时,其可访问名称必须包含该可见文本作为子字符串(SC 2.5.3)。语音控制用户(Dragon NaturallySpeaking、macOS Voice Control)会说出可见标签来激活控件。如果 aria-label 替代或与可见文本相矛盾,语音命令就会失效。
<button aria-label="Delete item from cart">Delete</button>
<button>Save Changes</button>
<button aria-label="Close dialog">
<svg aria-hidden="true">...</svg>
</button>
<button aria-label="Remove">Delete</button>
<button aria-label="Proceed to next step">Submit</button>
规则:存在可见文本时,aria-label 必须包含该可见文本(原样保留,不区分大小写)。如果可见文本已足够清晰,最好完全不使用 aria-label。
2. 响应式设计 [CRITICAL]
2.1 移动优先方法
为最小视口编写基础样式。使用 min-width 媒体查询逐步增加复杂样式。
/* Base: mobile */
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
/* Tablet */
@media (min-width: 48rem) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Desktop */
@media (min-width: 64rem) {
.grid {
grid-template-columns: repeat(3, 1fr);
}
}
2.2 使用现代 CSS 函数创建流式布局
使用 clamp()、min() 和 max() 实现流式尺寸调整,无需断点。
/* Fluid typography */
h1 {
font-size: clamp(1.75rem, 1.2rem + 2vw, 3rem);
}
/* Fluid spacing */
.section {
padding: clamp(1.5rem, 4vw, 4rem);
}
/* Fluid container */
.container {
width: min(90%, 72rem);
margin-inline: auto;
}
2.3 容器查询
根据组件所在的容器而非视口调整其尺寸。
.card-container {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
}
}
@container card (min-width: 700px) {
.card {
grid-template-columns: 300px 1fr;
gap: 2rem;
}
}
2.4 基于内容设置断点
在内容需要调整的位置设置断点,而不是根据设备宽度。常见的起始点:
/* Content-based, not "iPhone" or "iPad" */
@media (min-width: 30rem) { /* ~480px: single column gets cramped */ }
@media (min-width: 48rem) { /* ~768px: room for 2 columns */ }
@media (min-width: 64rem) { /* ~1024px: room for sidebar + content */ }
@media (min-width: 80rem) { /* ~1280px: wide multi-column */ }
2.5 触控目标
触控目标至少为 44x44 CSS 像素(WCAG SC 2.5.5 AAA;SC 2.5.8 在 AA 级仅要求 24x24px)。相邻目标之间至少留出 24px 间距。
button, a, input, select, textarea {
min-height: 44px;
min-width: 44px;
}
/* Enlarge tap area without changing visual size */
.icon-button {
position: relative;
width: 24px;
height: 24px;
}
.icon-button::after {
content: "";
position: absolute;
inset: -10px; /* expands clickable area */
}
2.6 视口 meta 标签
始终在文档的 <head> 中包含:
<meta name="viewport" content="width=device-width, initial-scale=1">
切勿使用 maximum-scale=1 或 user-scalable=no——这会破坏缩放手势的无障碍体验(SC 1.4.4)。
2.7 禁止水平滚动
内容必须能在宽度为 320px 时重排,不出现水平滚动(SC 1.4.10)。
/* Prevent overflow */
img, video, iframe, svg {
max-width: 100%;
height: auto;
}
/* Contain long words/URLs */
.prose {
overflow-wrap: break-word;
}
/* Tables: scroll container, not page */
.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
3. 表单 [HIGH]
3.1 为每个输入项添加标签
每个输入项都需要一个可见且以编程方式关联的标签。参见第 1.8 节。
3.2 自动填充属性
为常见字段使用 autocomplete,以启用浏览器自动填充(SC 1.3.5)。
<input type="text" autocomplete="name" name="full-name">
<input type="email" autocomplete="email" name="email">
<input type="tel" autocomplete="tel" name="phone">
<input type="text" autocomplete="street-address" name="address">
<input type="text" autocomplete="postal-code" name="zip">
<input type="text" autocomplete="cc-name" name="card-name">
<input type="text" autocomplete="cc-number" name="card-number">
<input type="password" autocomplete="new-password" name="password">
<input type="password" autocomplete="current-password" name="current-pw">
3.3 使用正确的输入类型
使用合适的 type,以调用相应的移动键盘并启用原生验证。
| 类型 | 用途 |
|---|---|
email | 电子邮件地址 |
tel | 电话号码 |
url | URL |
number | 带微调控件的数值(不用于电话号码、邮政编码或银行卡号) |
search | 搜索字段(显示清除按钮) |
date / time / datetime-local | 时间值 |
password | 密码(会触发密码管理器) |
text,搭配 inputmode="numeric" | 不带微调控件的数值数据(PIN 码、邮政编码) |
<input type="tel" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code">
3.4 行内验证
在 blur 时验证(不要在每次按键时都验证)。显示成功和错误状态。
<div class="field" data-state="error">
<label for="username">Username</label>
<input id="username" type="text" aria-describedby="username-hint username-error" aria-invalid="true">
<p id="username-hint" class="hint">3-20 characters, letters and numbers only</p>
<p id="username-error" class="error" role="alert">Username must be at least 3 characters</p>
</div>
.field[data-state="error"] input {
border-color: var(--color-error);
box-shadow: 0 0 0 1px var(--color-error);
}
.field[data-state="error"] .error { display: block; }
.field:not([data-state="error"]) .error { display: none; }
3.5 使用 Fieldset 和 Legend 分组
使用 <fieldset> 对相关输入项分组,并使用 <legend> 为该组添加标签。
<fieldset>
<legend>Shipping Address</legend>
<label for="street">Street</label>
<input id="street" type="text" autocomplete="street-address">
</fieldset>
<fieldset>
<legend>Preferred contact method</legend>
<label><input type="radio" name="contact" value="email"> Email</label>
<label><input type="radio" name="contact" value="phone"> Phone</label>
</fieldset>
3.6 标明必填字段
以视觉和编程方式标明必填字段。使用 required 属性和可见标记。
<label for="name">
Full name <span aria-hidden="true">*</span>
<span class="sr-only">(required)</span>
</label>
<input id="name" type="text" required autocomplete="name">
如果大多数字段都是必填项,则改为标明哪些字段是选填的。
3.7 提交按钮状态
不要禁用提交按钮,而应在提交时进行验证并显示错误。
<button type="submit">Create Account</button>
禁用按钮无法说明用户为什么无法继续。如果必须禁用,请提供清晰可见的解释。
3.8 将说明放在字段旁边
通过提示和错误文本,将格式示例、限制条件和问题解决说明放在相关字段旁边。切勿只在介绍性文字中说明一次要求,就指望用户之后还记得。
4. 排版 [HIGH]
4.1 字体栈
为提升性能,请使用系统字体栈;或者使用带有适当回退字体的 Web 字体。
/* System font stack */
body {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
/* Monospace stack */
code, pre, kbd {
font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;
}
/* Web font with fallbacks and size-adjust */
@font-face {
font-family: "CustomFont";
src: url("/fonts/custom.woff2") format("woff2");
font-display: swap;
font-weight: 100 900;
}
body {
font-family: "CustomFont", system-ui, sans-serif;
}
4.2 相对单位
字体大小和间距使用 rem。组件相对尺寸使用 em。
html {
font-size: 100%; /* = 16px default, respects user preference */
}
body {
font-size: 1rem; /* 16px */
}
h1 { font-size: 2.5rem; } /* 40px */
h2 { font-size: 2rem; } /* 32px */
h3 { font-size: 1.5rem; } /* 24px */
small { font-size: 0.875rem; } /* 14px */
/* Never: font-size: 16px; (ignores user zoom settings) */
4.3 行高和间距
正文行高至少为 1.5(SC 1.4.12)。段落间距至少为字体大小的 2 倍。
body {
line-height: 1.6;
}
h1, h2, h3 {
line-height: 1.2;
}
p + p {
margin-top: 1em;
}
4.4 最大行长
将行长限制在约 75 个字符,以提高可读性。
.prose {
max-width: 75ch;
}
/* Or for a content column */
.content {
max-width: 40rem; /* roughly 65-75ch depending on font */
margin-inline: auto;
}
4.5 排版细节
使用正确的引号、破折号和用于数据的等宽数字。
/* Smart quotes */
q { quotes: "\201C" "\201D" "\2018" "\2019"; } /* curly double then single */
/* Tabular numbers for aligned data */
.data-table td {
font-variant-numeric: tabular-nums;
}
/* Oldstyle numbers for running prose (optional) */
.prose {
font-variant-numeric: oldstyle-nums;
}
/* Proper list markers */
ul { list-style-type: disc; }
ol { list-style-type: decimal; }
4.6 标题层级
按顺序使用 h1 到 h6。切勿跳过层级。每页仅使用一个 h1。
<h1>Page Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>
<h2>Another Section</h2>
<h1>Page Title</h1>
<h3>Subsection</h3>
如果需要与层级不同的视觉样式,请使用 CSS 类:
<h2 class="text-lg">Visually smaller but semantically h2</h2>
5. 性能 [HIGH]
5.1 延迟加载首屏以下的图片
对于初始加载时不可见的图片,请使用原生延迟加载。
<img src="hero.webp" alt="Hero image" fetchpriority="high" width="1200" height="600">
<img src="feature.webp" alt="Feature image" loading="lazy" width="600" height="400">
5.2 明确指定图片尺寸
始终指定 width 和 height,以防止布局偏移(CLS)。
<img src="photo.webp" alt="Description" width="800" height="600">
/* Responsive images with aspect ratio preservation */
img {
max-width: 100%;
height: auto;
}
5.3 资源提示
对第三方来源使用 preconnect,对关键资源使用 preload。
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/css/critical.css" as="style">
<link rel="dns-prefetch" href="https://analytics.example.com">
</head>
5.4 代码拆分
仅在需要时加载 JavaScript。使用动态 import() 按路由和组件拆分代码。
// Route-based splitting
const routes = {
'/dashboard': () => import('./pages/dashboard.js'),
'/settings': () => import('./pages/settings.js'),
};
// Interaction-based splitting
button.addEventListener('click', async () => {
const { openEditor } = await import('./editor.js');
openEditor();
});
5.5 虚拟化长列表
对于超过数百项的列表,只渲染可见行。
// Concept: virtual scrolling
// Render only items in viewport + buffer
const visibleStart = Math.floor(scrollTop / itemHeight);
const visibleEnd = visibleStart + Math.ceil(containerHeight / itemHeight);
const buffer = 5;
const renderStart = Math.max(0, visibleStart - buffer);
const renderEnd = Math.min(totalItems, visibleEnd + buffer);
5.6 避免布局抖动
批量执行 DOM 读取和写入操作。切勿交替执行。
// Bad: read-write-read-write (forces synchronous layout)
elements.forEach(el => {
const height = el.offsetHeight; // read
el.style.height = height + 10 + 'px'; // write
});
// Good: batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight); // all reads
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px'; // all writes
});
5.7 谨慎使用 will-change
仅对将要执行动画的元素应用 will-change,并在动画完成后移除。
/* Good: scoped and temporary */
.card:hover {
will-change: transform;
}
.card.animating {
will-change: transform, opacity;
}
/* Bad: blanket will-change */
/* * { will-change: transform; } */
5.8 及时显示等待状态
用户执行操作后,立即反馈新的状态。如果工作无法在短时间内完成,请显示进度、骨架屏、乐观 UI 或 aria-busy 反馈,而不要让界面保持不变。
6. 动画与动态效果 [MEDIUM]
6.1 遵循 prefers-reduced-motion
始终提供减少动态效果的替代方案(SC 2.3.3,AAA 级)。
/* Define animations normally */
.fade-in {
animation: fadeIn 300ms ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* Remove or reduce for users who prefer it */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
// Check in JavaScript
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
6.2 使用合成器友好的动画
为实现流畅的 60fps 动画,仅对 transform 和 opacity 设置动画。这些属性由 GPU 合成器线程处理。
/* Good: compositor-only properties */
.slide-in {
transition: transform 200ms ease-out, opacity 200ms ease-out;
}
/* Bad: triggers layout/paint */
.slide-in-bad {
transition: left 200ms, width 200ms, height 200ms;
}
6.3 不要使用闪烁内容
内容闪烁频率切勿超过每秒 3 次(SC 2.3.1),否则可能引发癫痫发作。
6.4 为状态变化添加过渡效果
为悬停、焦点、打开/关闭和其他状态变化添加过渡效果,以保持连贯性。
.dropdown {
opacity: 0;
transform: translateY(-4px);
transition: opacity 150ms ease-out, transform 150ms ease-out;
pointer-events: none;
}
.dropdown.open {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
6.5 仅使用有意义的动态效果
动画应传达状态、引导注意力或展示空间关系。切勿仅为装饰而添加动画。
7. 暗色模式与主题 [MEDIUM]
7.1 检测系统偏好设置
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f0f17;
--text: #e4e4ef;
--surface: #1c1c2e;
--border: #2e2e44;
}
}
7.2 使用 CSS 自定义属性设置主题
将所有主题值定义为自定义属性。通过更改属性值来切换主题。
:root {
color-scheme: light dark;
/* Light theme (default) */
--color-bg: #ffffff;
--color-surface: #f5f5f7;
--color-text-primary: #1a1a2e;
--color-text-secondary: #555770;
--color-border: #d1d1e0;
--color-primary: #2563eb;
--color-primary-text: #ffffff;
--color-error: #dc2626;
--color-success: #16a34a;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0f0f17;
--color-surface: #1c1c2e;
--color-text-primary: #e4e4ef;
--color-text-secondary: #a0a0b8;
--color-border: #2e2e44;
--color-primary: #60a5fa;
--color-primary-text: #0f0f17;
--color-error: #f87171;
--color-success: #4ade80;
}
}
7.3 color-scheme 元标签
告知浏览器支持的配色方案,以便设置原生 UI 元素(滚动条、表单控件)。
<meta name="color-scheme" content="light dark">
7.4 确保两种模式下的对比度
验证浅色和暗色模式下的对比度。暗色模式中的深色表面常出现文本对比度不足的问题。
7.5 自适应图片
为浅色和暗色环境提供合适的图片。
<picture>
<source srcset="logo-dark.svg" media="(prefers-color-scheme: dark)">
<img src="logo-light.svg" alt="Company logo">
</picture>
/* Or use CSS filter for simple cases */
@media (prefers-color-scheme: dark) {
.decorative-img {
filter: brightness(0.9) contrast(1.1);
}
}
7.6 遵循 prefers-contrast
使用 @media (prefers-contrast: more) 和 @media (prefers-contrast: forced) 遵循用户的对比度偏好。prefers-contrast: more 会响应 macOS/iOS「增加对比度」系统设置;prefers-contrast: forced 会响应 Windows 高对比度模式——这是一项独立的操作系统功能,会完全覆盖颜色。
/* Default theme */
:root {
--color-text: #555770;
--color-border: #d1d1e0;
--color-bg: #ffffff;
}
/* High contrast mode: stronger text and border colors */
@media (prefers-contrast: more) {
:root {
--color-text: #1a1a2e; /* Darker text for higher ratio */
--color-border: #1a1a2e; /* Stronger borders */
--color-bg: #ffffff;
}
/* Ensure interactive elements are clearly delineated */
button, input, select, textarea {
border: 2px solid currentColor;
}
}
/* Forced colors (Windows High Contrast mode) */
@media (prefers-contrast: forced) {
/* Use system color keywords to respect OS color palette */
:root {
--color-text: ButtonText;
--color-bg: ButtonFace;
--color-border: ButtonBorder;
}
}
8. 导航与状态 [MEDIUM]
8.1 URL 反映状态
每个有意义的视图都应有唯一的 URL。用户应能为任何状态添加书签、分享并重新加载。
// Update URL without full page reload
function updateFilters(filters) {
const params = new URLSearchParams(filters);
history.pushState(null, '', `?${params}`);
renderResults(filters);
}
// Restore state from URL on load
const params = new URLSearchParams(location.search);
const initialFilters = Object.fromEntries(params);
8.2 浏览器前进/后退
处理 popstate,以支持浏览器导航。
window.addEventListener('popstate', () => {
const params = new URLSearchParams(location.search);
renderResults(Object.fromEntries(params));
});
8.3 当前导航状态
在导航中标示当前页面或分区。为活动链接使用 aria-current="page"。
<nav aria-label="Main">
<a href="/" aria-current="page">Home</a>
<a href="/products">Products</a>
<a href="/about">About</a>
</nav>
[aria-current="page"] {
font-weight: 700;
border-bottom: 2px solid var(--color-primary);
}
8.4 面包屑导航
对于层级较深的网站,提供面包屑导航。
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/products">Products</a></li>
<li><a href="/products/widgets" aria-current="page">Widgets</a></li>
</ol>
</nav>
8.5 滚动位置恢复
管理 SPA 导航时的滚动位置。
// Disable browser auto-restoration for manual control
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
// Save scroll position before navigation
function saveScrollPosition() {
sessionStorage.setItem(`scroll-${location.pathname}`, window.scrollY);
}
// Restore on back/forward
window.addEventListener('popstate', () => {
const saved = sessionStorage.getItem(`scroll-${location.pathname}`);
if (saved) {
requestAnimationFrame(() => window.scrollTo(0, parseInt(saved)));
}
});
9. 触摸与交互 [MEDIUM]
9.1 使用 Touch-Action 控制滚动
使用 touch-action 控制交互元素的手势行为。
/* Allow only vertical scrolling (disable horizontal pan and pinch-zoom) */
.vertical-scroll {
touch-action: pan-y;
}
/* Carousel: horizontal scroll only */
.carousel {
touch-action: pan-x;
}
/* Canvas/map: disable all browser gestures */
.canvas {
touch-action: none;
}
9.2 点击高亮
控制移动端 WebKit 浏览器中的点击高亮。
button, a {
-webkit-tap-highlight-color: transparent;
}
9.3 悬停与焦点保持一致
每种悬停交互都必须支持键盘焦点。
/* Always pair :hover with :focus-visible */
.card:hover,
.card:focus-visible {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
9.4 不要只支持悬停的交互
切勿将必要功能隐藏在悬停交互后。触摸设备没有悬停状态。
/* Bad: content only accessible on hover */
/* .tooltip { display: none; }
.trigger:hover .tooltip { display: block; } */
/* Good: works with focus and click too */
.trigger:hover .tooltip,
.trigger:focus-within .tooltip,
.tooltip:focus-within {
display: block;
}
9.5 为轮播使用滚动捕捉
为卡片轮播和水平列表使用 CSS 滚动捕捉。
.carousel {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
gap: 1rem;
scroll-padding: 1rem;
}
.carousel > .slide {
scroll-snap-align: start;
flex: 0 0 min(85%, 400px);
}
10. 国际化 [MEDIUM]
10.1 dir 和 lang 属性
在 <html> 元素上设置 lang。对用户生成的内容使用 dir="auto"。
<html lang="en" dir="ltr">
<p dir="auto">User-submitted text here</p>
<blockquote lang="ar" dir="rtl">...</blockquote>
10.2 使用 Intl API 进行格式化
使用 Intl API 根据区域设置进行格式化。切勿硬编码日期或数字格式。
// Dates
new Intl.DateTimeFormat('en-US', { dateStyle: 'long' }).format(date);
// "January 15, 2026"
// Numbers
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(1234.56);
// "1.234,56 EUR"
// Relative time
new Intl.RelativeTimeFormat('en', { numeric: 'auto' }).format(-1, 'day');
// "yesterday"
// Lists
new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }).format(['a', 'b', 'c']);
// "a, b, and c"
// Plurals
const pr = new Intl.PluralRules('en');
const suffixes = { one: 'st', two: 'nd', few: 'rd', other: 'th' };
function ordinal(n) { return `${n}${suffixes[pr.select(n)]}`; }
10.3 避免在图像中放置文本
图像中的文本无法翻译、调整大小,也无法被屏幕阅读器读取。需要带样式的文本叠加时,使用 HTML/CSS 文本和背景图像。
10.4 CSS 逻辑属性
使用逻辑属性而非物理属性,以同时支持 LTR 和 RTL 布局。
/* Physical (breaks in RTL) */
/* margin-left: 1rem; padding-right: 2rem; border-left: 1px solid; */
/* Logical (works in LTR and RTL) */
.sidebar {
margin-inline-start: 1rem;
padding-inline-end: 2rem;
border-inline-start: 1px solid var(--color-border);
}
.stack > * + * {
margin-block-start: 1rem;
}
/* Logical shorthands */
.box {
margin-inline: auto; /* left + right */
padding-block: 2rem; /* top + bottom */
inset-inline-start: 0; /* left in LTR, right in RTL */
border-start-start-radius: 8px; /* top-left in LTR, top-right in RTL */
}
| 物理属性 | 逻辑属性 |
|---|---|
left / right | inline-start / inline-end |
top / bottom | block-start / block-end |
margin-left | margin-inline-start |
padding-right | padding-inline-end |
border-top-left-radius | border-start-start-radius |
width | inline-size |
height | block-size |
text-align: left | text-align: start |
10.5 RTL 布局支持
在 RTL 模式下测试布局。搭配逻辑属性时,Flexbox 和 Grid 会自动处理 RTL。
/* This layout works in both LTR and RTL without changes */
.layout {
display: flex;
gap: 1rem;
}
/* Icons that indicate direction need flipping */
[dir="rtl"] .arrow-icon {
transform: scaleX(-1);
}
11. 渐进式 Web 应用 [MEDIUM]
PWA 可安装并离线运行。构建可安装的 Web 应用时,以下规则可确保体验一致且可靠。
11.1 提供完整的 Web App Manifest
在 <head> 中引入 manifest.json,并填写可安装所需的所有字段。缺少字段会导致安装提示无法显示,且不会给出提示。
<link rel="manifest" href="/manifest.json">
{
"name": "My App",
"short_name": "App",
"start_url": "/",
"display": "standalone",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
不正确:
{
"name": "My App"
// Missing start_url, display, and icons — app is not installable
}
11.2 设置 theme_color 和 background_color
theme_color 会为浏览器界面和操作系统任务切换器着色。background_color 会在应用加载前填充启动画面背景。两者都必须与品牌颜色一致。
{
"theme_color": "#1a73e8",
"background_color": "#ffffff"
}
11.3 注册 Service Worker 以支持离线
安装和离线功能都需要 Service Worker。安装时缓存关键资源;离线时从缓存响应。
// In your main entry point
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
// sw.js — cache on install, serve from cache when offline
const CACHE = 'v1';
const PRECACHE = ['/', '/index.html', '/app.js', '/app.css'];
self.addEventListener('install', e =>
e.waitUntil(caches.open(CACHE).then(c => c.addAll(PRECACHE)))
);
self.addEventListener('fetch', e =>
e.respondWith(
caches.match(e.request).then(hit => hit ?? fetch(e.request))
)
);
11.4 满足可安装条件
要显示浏览器安装提示:应用必须通过 HTTPS 提供,已注册带有 fetch 处理器的 Service Worker,并包含具有 name、icons、start_url 和 display: standalone(或 fullscreen/minimal-ui)的 manifest。
11.5 适当使用 display 模式
| 值 | 适用场景 |
|---|---|
standalone | 应用替代浏览器 UI;最常见的选择 |
fullscreen | 需要占据整个屏幕的游戏或媒体应用 |
minimal-ui | 保留最少的浏览器控件(后退、重新加载) |
browser | 不涉及安装行为;在浏览器标签页中打开 |
评估清单
构建或审查 Web 界面时,请使用此清单。
无障碍
- 所有图像都有合适的
alt文本 - 颜色对比度达到 4.5:1(文本)和 3:1(UI 组件)
- 所有交互元素都可通过键盘访问
- 焦点指示器清晰可见(对比度为 3:1,周长至少为 2px)
- 提供跳过导航链接
- 表单输入框都有关联的标签
- 错误消息与对应的输入框相关联
- 动态内容更新使用 ARIA live 区域
- 内容每秒闪烁不超过 3 次
- 页面具有恰当的标题层级(h1-h6,不跳级)
- 正确使用地标(main、nav、header、footer)
响应式
- 在宽度为 320px 时不会出现水平滚动
- 触控目标至少为 44x44px
- 存在视口 meta 标签(不含 user-scalable=no)
- 布局适用于手机、平板和桌面设备
- 手机上无需缩放即可清晰阅读文本
表单
- 所有输入框都有可见标签
- 为常用字段设置 autocomplete 属性
- 正确的输入类型会触发正确的移动键盘
- 错误消息清晰且具体
- 标明必填字段
- 提交按钮未禁用
性能
- 首屏以下的图像使用
loading="lazy" - 图像明确设置
width和height - 预加载关键字体
- 第三方源使用
preconnect - 将大型 JS 包拆分为多个代码块
动效与主题
- 遵循
prefers-reduced-motion设置 - 动画仅使用
transform和opacity - 暗色模式保持对比度
- 存在
color-schememeta 标签 - 主题使用 CSS 自定义属性
-
prefers-contrast: more会提高文本和边框的对比度 -
prefers-contrast: forced使用系统颜色关键字
国际化
-
<html>上设置lang属性 - 使用 CSS 逻辑属性(而非物理方向属性)
- 使用 Intl API 格式化日期和数字
- 图像中不嵌入文本
- 在 RTL 模式下测试布局
渐进式 Web 应用
- 在
<head>中链接 Web App Manifest,并包含name、icons、start_url和display -
theme_color和background_color与品牌配色一致 - 注册带有
fetch处理程序的 Service Worker,以支持离线使用 - 应用通过 HTTPS 提供服务
常见反模式
| 反模式 | 修正方法 |
|---|---|
<div onclick="..."> | 使用 <button> |
没有替代样式的 outline: none | 使用 :focus-visible 并自定义轮廓 |
将 placeholder 用作标签 | 添加 <label> 元素 |
tabindex="5" | 使用 tabindex="0" 或自然顺序 |
user-scalable=no | 移除它 |
font-size: 12px | 使用 font-size: 0.75rem |
为 width/height/top/left 添加动画 | 为 transform 和 opacity 添加动画 |
| 禁用提交按钮 | 在提交时验证并显示错误 |
| 仅用颜色表示状态 | 添加图标、文本或图案 |
margin-left / padding-right | 使用 margin-inline-start / padding-inline-end |
<img> 未设置尺寸 | 添加 width 和 height 属性 |
| 仅通过悬停显示内容 | 添加 :focus-within 和点击处理程序 |