web-design-guidelines
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWeb Platform Design Guidelines
Web平台设计指南
Framework-agnostic rules for accessible, performant, responsive web interfaces. Based on WCAG 2.2, MDN Web Docs, and modern web platform APIs.
一套与框架无关的无障碍、高性能、响应式Web界面设计规则。基于WCAG 2.2、MDN Web Docs及现代Web平台API制定。
1. Accessibility / WCAG [CRITICAL]
1. 无障碍性 / WCAG [CRITICAL]
Accessibility is not optional. Every rule in this section maps to WCAG 2.2 success criteria at Level A or AA.
无障碍性并非可选要求。本节中的每条规则均对应WCAG 2.2的A级或AA级成功标准。
1.1 Use Semantic HTML Elements
1.1 使用语义化HTML元素
Use elements for their intended purpose. Semantic structure provides free accessibility, SEO, and reader-mode support.
| Element | Purpose |
|---|---|
| Primary page content (one per page) |
| Navigation blocks |
| Introductory content or navigational aids |
| Footer for nearest sectioning content |
| Self-contained, independently distributable content |
| Thematic grouping with a heading |
| Tangentially related content (sidebars, callouts) |
| Illustrations, diagrams, code listings |
| Expandable/collapsible disclosure widget |
| Modal or non-modal dialog boxes |
| Machine-readable dates/times |
| Highlighted/referenced text |
| Contact information for nearest article/body |
html
<!-- Good -->
<main>
<article>
<h1>Article Title</h1>
<p>Content...</p>
</article>
<aside>Related links</aside>
</main>
<!-- Bad: div soup -->
<div class="main">
<div class="article">
<div class="title">Article Title</div>
<div class="content">Content...</div>
</div>
</div>Anti-pattern: Using or for interactive elements. Never write when exists.
<div><span><div onclick><button>按元素的预期用途使用它们。语义化结构可免费提供无障碍支持、SEO优化和阅读器模式适配。
| 元素 | 用途 |
|---|---|
| 页面主要内容(每页一个) |
| 导航区块 |
| 介绍性内容或导航辅助区域 |
| 最近的分段内容的页脚 |
| 独立可分发的自包含内容 |
| 带标题的主题分组 |
| 间接相关内容(侧边栏、提示框) |
| 插图、图表、代码清单 |
| 可展开/折叠的披露组件 |
| 模态或非模态对话框 |
| 机器可读的日期/时间 |
| 高亮/引用文本 |
| 最近的文章/页面主体的联系信息 |
html
<!-- 推荐写法 -->
<main>
<article>
<h1>文章标题</h1>
<p>内容...</p>
</article>
<aside>相关链接</aside>
</main>
<!-- 不推荐:div嵌套冗余 -->
<div class="main">
<div class="article">
<div class="title">文章标题</div>
<div class="content">内容...</div>
</div>
</div>反模式:使用或实现交互元素。当可用时,绝不要写。
<div><span><button><div onclick>1.2 ARIA Labels on Interactive Elements
1.2 交互元素的ARIA标签
Every interactive element must have an accessible name. Prefer visible text; use or only when visible text is insufficient (SC 4.1.2).
aria-labelaria-labelledbyhtml
<!-- Icon-only button: needs aria-label -->
<button aria-label="Close dialog">
<svg aria-hidden="true">...</svg>
</button>
<!-- Linked by labelledby -->
<h2 id="section-title">Notifications</h2>
<ul aria-labelledby="section-title">...</ul>
<!-- Redundant: visible text is enough -->
<button>Save Changes</button> <!-- No aria-label needed -->每个交互元素必须有一个可访问的名称。优先使用可见文本;仅当可见文本不足时,才使用或(符合SC 4.1.2标准)。
aria-labelaria-labelledbyhtml
<!-- 仅含图标按钮:需要aria-label -->
<button aria-label="关闭对话框">
<svg aria-hidden="true">...</svg>
</button>
<!-- 通过labelledby关联 -->
<h2 id="section-title">通知</h2>
<ul aria-labelledby="section-title">...</ul>
<!-- 冗余写法:可见文本已足够 -->
<button>保存更改</button> <!-- 无需aria-label -->1.3 Keyboard Navigation
1.3 键盘导航
All interactive elements must be reachable and operable via keyboard (SC 2.1.1).
- Use native interactive elements (,
<button>,<a href>,<input>) which are keyboard-accessible by default.<select> - Custom widgets need to enter tab order and keydown handlers for activation.
tabindex="0" - Never use values greater than 0.
tabindex - Trap focus inside modals; return focus on close.
js
// 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();
}
}
});所有交互元素必须可通过键盘访问并操作(符合SC 2.1.1标准)。
- 使用原生交互元素(、
<button>、<a href>、<input>),它们默认支持键盘访问。<select> - 自定义组件需要以加入Tab顺序,并通过keydown事件处理器实现激活。
tabindex="0" - 绝不要使用大于0的值。
tabindex - 在模态框内捕获焦点;关闭时恢复焦点。
js
// 模态框的焦点捕获
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 Visible Focus Indicators
1.4 可见焦点指示器
Never remove focus outlines without providing a visible replacement (SC 2.4.7, enhanced 2.4.11/2.4.12 in WCAG 2.2).
css
/* 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 requires focus indicators to have a minimum area of the perimeter of the component times 2px, with 3:1 contrast against adjacent colors.
绝不要移除焦点轮廓,除非提供可见的替代样式(符合SC 2.4.7标准,WCAG 2.2中增强为2.4.11/2.4.12)。
css
/* 推荐:自定义焦点指示器 */
:focus-visible {
outline: 3px solid var(--focus-color, #4A90D9);
outline-offset: 2px;
}
/* 仅当:focus-visible受支持时,移除默认样式 */
:focus:not(:focus-visible) {
outline: none;
}
/* 不推荐:移除所有焦点样式 */
/* *:focus { outline: none; } */WCAG 2.2要求焦点指示器的最小面积为组件周长乘以2px,且与相邻颜色的对比度至少为3:1。
1.5 Skip Navigation Links
1.5 跳过导航链接
Provide a mechanism to skip repeated blocks of content (SC 2.4.1).
html
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<nav>...</nav>
<main id="main-content">...</main>
</body>css
.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;
}提供跳过重复内容区块的机制(符合SC 2.4.1标准)。
html
<body>
<a href="#main-content" class="skip-link">跳转到主要内容</a>
<nav>...</nav>
<main id="main-content">...</main>
</body>css
.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 Alt Text for Images
1.6 图片的替代文本
Every must have an attribute (SC 1.1.1).
<img>alt- Informative images: describe the content and function. .
alt="Bar chart showing sales doubled in Q4" - Decorative images: use (empty string) so screen readers skip them.
alt="" - Functional images (inside links/buttons): describe the action. .
alt="Search" - Complex images: use for short description, link to long description or use
alt.<figcaption>
html
<img src="chart.png" alt="Revenue chart: Q1 $2M, Q2 $2.4M, Q3 $3.1M, Q4 $4.5M">
<img src="decorative-wave.svg" alt="">每个必须包含属性(符合SC 1.1.1标准)。
<img>alt- 信息性图片:描述内容和功能。例如。
alt="显示第四季度销售额翻倍的柱状图" - 装饰性图片:使用(空字符串),让屏幕阅读器跳过它们。
alt="" - 功能性图片(链接/按钮内):描述操作。例如。
alt="搜索" - 复杂图片:用提供简短描述,链接到详细说明或使用
alt。<figcaption>
html
<img src="chart.png" alt="收入图表:第一季度200万美元,第二季度240万美元,第三季度310万美元,第四季度450万美元">
<img src="decorative-wave.svg" alt="">1.7 Color Contrast
1.7 颜色对比度
Maintain minimum contrast ratios (SC 1.4.3, 1.4.6, 1.4.11).
| Content | Minimum Ratio |
|---|---|
| Normal text (<24px / <18.66px bold) | 4.5:1 |
| Large text (>=24px / >=18.66px bold) | 3:1 |
| UI components and graphical objects | 3:1 |
Do not rely on color alone to convey information (SC 1.4.1). Pair color with icons, text, or patterns.
css
/* 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 */
}保持最低对比度比值(符合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标准)。将颜色与图标、文本或图案结合使用。
css
/* 检查这些变量的对比度 */
:root {
--text-primary: #1a1a2e; /* 在白色背景上:~16:1 */
--text-secondary: #555770; /* 在白色背景上:~6.5:1 */
--text-disabled: #767693; /* 在白色背景上:~4.5:1,接近临界值 */
}1.8 Form Labels
1.8 表单标签
Every form input must have a programmatically associated label (SC 1.3.1, 3.3.2).
html
<!-- Explicit label (preferred) -->
<label for="email">Email address</label>
<input id="email" type="email" autocomplete="email">
<!-- Implicit label (acceptable) -->
<label>
Email address
<input type="email" autocomplete="email">
</label>
<!-- Never: placeholder as sole label -->
<!-- <input placeholder="Email"> -->每个表单输入必须有一个程序化关联的标签(符合SC 1.3.1、3.3.2标准)。
html
<!-- 显式标签(推荐) -->
<label for="email">电子邮箱</label>
<input id="email" type="email" autocomplete="email">
<!-- 隐式标签(可接受) -->
<label>
电子邮箱
<input type="email" autocomplete="email">
</label>
// 绝不要:仅用占位符作为标签
<!-- <input placeholder="Email"> -->1.9 Error Identification
1.9 错误标识
Identify and describe errors in text (SC 3.3.1). Link error messages to inputs with or .
aria-describedbyaria-errormessagehtml
<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. name@example.com</p>用文本标识并描述错误(符合SC 3.3.1标准)。使用或将错误消息与输入框关联。
aria-describedbyaria-errormessagehtml
<label for="email">电子邮箱</label>
<input id="email" type="email" aria-describedby="email-error" aria-invalid="true">
<p id="email-error" role="alert">请输入有效的电子邮箱地址,例如name@example.com</p>1.10 ARIA Live Regions
1.10 ARIA实时区域
Announce dynamic content changes to screen readers (SC 4.1.3).
html
<!-- Polite: announced when user is idle -->
<div aria-live="polite" aria-atomic="true">
3 results found
</div>
<!-- Assertive: interrupts current speech -->
<div role="alert">
Your session will expire in 2 minutes.
</div>
<!-- Status messages -->
<div role="status">
File uploaded successfully.
</div>Use by default. Reserve / for time-sensitive warnings.
aria-live="polite"role="alert"aria-live="assertive"向屏幕阅读器播报动态内容的变化(符合SC 4.1.3标准)。
html
<!-- 礼貌模式:用户空闲时播报 -->
<div aria-live="polite" aria-atomic="true">
找到3条结果
</div>
<!-- 断言模式:中断当前语音播报 -->
<div role="alert">
您的会话将在2分钟后过期。
</div>
<!-- 状态消息 -->
<div role="status">
文件上传成功。
</div>默认使用。仅将 / 用于时间敏感的警告。
aria-live="polite"role="alert"aria-live="assertive"1.11 ARIA Role Quick Reference
1.11 ARIA角色速查
| Role | Purpose | Native Equivalent |
|---|---|---|
| Clickable action | |
| Navigation | |
| Tab interface | None |
| Modal | |
| Assertive live region | None |
| Polite live region | |
| Nav landmark | |
| Main landmark | |
| Aside landmark | |
| Search landmark | |
| Image | |
| List | |
| Heading (with | |
| Menu widget | None |
| Tree view | None |
| Data grid | |
| Progress | |
| Range input | |
| Toggle | |
Rule: Prefer native HTML over ARIA. Use ARIA only when no native element exists for the pattern.
| 角色 | 用途 | 原生替代元素 |
|---|---|---|
| 可点击操作 | |
| 导航 | |
| 标签页界面 | 无 |
| 模态框 | |
| 断言式实时区域 | 无 |
| 礼貌式实时区域 | |
| 导航地标 | |
| 主要内容地标 | |
| 补充内容地标 | |
| 搜索地标 | |
| 图片 | |
| 列表 | |
| 标题(配合 | |
| 菜单组件 | 无 |
| 树状视图 | 无 |
| 数据网格 | |
| 进度条 | |
| 范围输入 | |
| 开关 | |
规则:优先使用原生HTML元素而非ARIA。仅当没有对应原生元素实现该模式时,才使用ARIA。
2. Responsive Design [CRITICAL]
2. 响应式设计 [CRITICAL]
2.1 Mobile-First Approach
2.1 移动优先方法
Write base styles for the smallest viewport. Layer complexity with media queries.
min-widthcss
/* 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);
}
}为最小视口编写基础样式。通过媒体查询逐步增加复杂度。
min-widthcss
/* 基础样式:移动端 */
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
/* 平板端 */
@media (min-width: 48rem) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* 桌面端 */
@media (min-width: 64rem) {
.grid {
grid-template-columns: repeat(3, 1fr);
}
}2.2 Fluid Layouts with Modern CSS Functions
2.2 使用现代CSS函数实现流式布局
Use , , and for fluid sizing without breakpoints.
clamp()min()max()css
/* 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;
}使用、和实现无需断点的流式尺寸。
clamp()min()max()css
/* 流式排版 */
h1 {
font-size: clamp(1.75rem, 1.2rem + 2vw, 3rem);
}
/* 流式间距 */
.section {
padding: clamp(1.5rem, 4vw, 4rem);
}
/* 流式容器 */
.container {
width: min(90%, 72rem);
margin-inline: auto;
}2.3 Container Queries
2.3 容器查询
Size components based on their container, not the viewport.
css
.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;
}
}根据组件的容器而非视口调整组件尺寸。
css
.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 Breakpoints
2.4 基于内容的断点
Set breakpoints where your content breaks, not at device widths. Common starting points:
css
/* 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 */ }在内容出现布局问题时设置断点,而非基于设备宽度。常见起始点:
css
/* 基于内容,而非“iPhone”或“iPad” */
@media (min-width: 30rem) { /* ~480px:单列布局开始拥挤 */ }
@media (min-width: 48rem) { /* ~768px:可容纳2列 */ }
@media (min-width: 64rem) { /* ~1024px:可容纳侧边栏+内容 */ }
@media (min-width: 80rem) { /* ~1280px:宽屏多列布局 */ }2.5 Touch Targets
2.5 触摸目标
Minimum 44x44 CSS pixels for touch targets (WCAG SC 2.5.8). Provide at least 24px spacing between adjacent targets.
css
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 */
}触摸目标的最小尺寸为44x44 CSS像素(符合WCAG SC 2.5.8标准)。相邻目标之间至少保留24px的间距。
css
button, a, input, select, textarea {
min-height: 44px;
min-width: 44px;
}
/* 不改变视觉尺寸的情况下,扩大点击区域 */
.icon-button {
position: relative;
width: 24px;
height: 24px;
}
.icon-button::after {
content: "";
position: absolute;
inset: -10px; /* 扩大可点击区域 */
}2.6 Viewport Meta Tag
2.6 视口元标签
Always include in the document :
<head>html
<meta name="viewport" content="width=device-width, initial-scale=1">Never use or -- these break pinch-to-zoom accessibility (SC 1.4.4).
maximum-scale=1user-scalable=no务必在文档中包含:
<head>html
<meta name="viewport" content="width=device-width, initial-scale=1">绝不要使用或——这些设置会破坏捏合缩放的无障碍性(符合SC 1.4.4标准)。
maximum-scale=1user-scalable=no2.7 No Horizontal Scrolling
2.7 禁止水平滚动
Content must reflow at 320px width without horizontal scrolling (SC 1.4.10).
css
/* 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;
}内容在320px宽度下必须自动换行,无水平滚动(符合SC 1.4.10标准)。
css
/* 防止溢出 */
img, video, iframe, svg {
max-width: 100%;
height: auto;
}
/* 处理长单词/URL */
.prose {
overflow-wrap: break-word;
}
/* 表格:使用滚动容器而非页面滚动 */
.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}3. Forms [HIGH]
3. 表单设计 [HIGH]
3.1 Label Every Input
3.1 为每个输入添加标签
Every input needs a visible, programmatically associated label. See section 1.8.
每个输入都需要可见且程序化关联的标签。详见1.8节。
3.2 Autocomplete Attributes
3.2 自动完成属性
Use for common fields to enable browser autofill (SC 1.3.5).
autocompletehtml
<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">为常见字段使用属性,启用浏览器自动填充功能(符合SC 1.3.5标准)。
autocompletehtml
<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 Correct Input Types
3.3 正确的输入类型
Use the right to trigger appropriate mobile keyboards and native validation.
type| Type | Use For |
|---|---|
| Email addresses |
| Phone numbers |
| URLs |
| Numeric values with spinners (not for phone, zip, card numbers) |
| Search fields (shows clear button) |
| Temporal values |
| Passwords (triggers password manager) |
| Numeric data without spinners (PINs, zip codes) |
html
<input type="tel" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code">使用合适的属性,触发对应的移动端键盘和原生验证。
type| 类型 | 适用场景 |
|---|---|
| 电子邮箱地址 |
| 电话号码 |
| 网址 |
| 带微调器的数值(不适用于电话、邮编、卡号) |
| 搜索字段(显示清除按钮) |
| 时间相关值 |
| 密码(触发密码管理器) |
| 无微调器的数值(PIN码、邮编) |
html
<input type="tel" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code">3.4 Inline Validation
3.4 内联验证
Validate on (not on every keystroke). Show success and error states.
blurhtml
<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>css
.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; }在事件时验证(而非每次按键)。显示成功和错误状态。
blurhtml
<div class="field" data-state="error">
<label for="username">用户名</label>
<input id="username" type="text" aria-describedby="username-hint username-error" aria-invalid="true">
<p id="username-hint" class="hint">3-20个字符,仅支持字母和数字</p>
<p id="username-error" class="error" role="alert">用户名长度至少为3个字符</p>
</div>css
.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 and Legend for Groups
3.5 使用Fieldset和Legend分组
Group related inputs with and label the group with .
<fieldset><legend>html
<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>使用对相关输入进行分组,并使用为分组添加标签。
<fieldset><legend>html
<fieldset>
<legend>收货地址</legend>
<label for="street">街道</label>
<input id="street" type="text" autocomplete="street-address">
<!-- ... -->
</fieldset>
<fieldset>
<legend>首选联系方式</legend>
<label><input type="radio" name="contact" value="email"> 电子邮箱</label>
<label><input type="radio" name="contact" value="phone"> 电话</label>
</fieldset>3.6 Required Field Indication
3.6 必填字段标识
Indicate required fields visually and programmatically. Use attribute and visible markers.
requiredhtml
<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">If most fields are required, indicate which are optional instead.
通过视觉和程序化方式标识必填字段。使用属性和可见标记。
requiredhtml
<label for="name">
全名 <span aria-hidden="true">*</span>
<span class="sr-only">(必填)</span>
</label>
<input id="name" type="text" required autocomplete="name">如果大多数字段为必填项,则改为标识可选字段。
3.7 Submit Button State
3.7 提交按钮状态
Do not disable the submit button. Instead, validate on submit and show errors.
html
<!-- Good: always enabled, validate on submit -->
<button type="submit">Create Account</button>
<!-- Bad: disabled button with no explanation -->
<!-- <button type="submit" disabled>Create Account</button> -->Disabled buttons fail to communicate why the user cannot proceed. If you must disable, provide a visible explanation.
不要禁用提交按钮。而是在提交时进行验证并显示错误。
html
<!-- 推荐:始终启用,提交时验证 -->
<button type="submit">创建账户</button>
<!-- 不推荐:禁用按钮且无说明 -->
<!-- <button type="submit" disabled>Create Account</button> -->禁用按钮无法向用户说明无法继续的原因。如果必须禁用,请提供可见的解释。
4. Typography [HIGH]
4. 排版设计 [HIGH]
4.1 Font Stacks
4.1 字体栈
Use system font stacks for performance, or web fonts with proper fallbacks.
css
/* 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;
}使用系统字体栈提升性能,或使用带适当回退的Web字体。
css
/* 系统字体栈 */
body {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
/* 等宽字体栈 */
code, pre, kbd {
font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;
}
/* 带回退和size-adjust的Web字体 */
@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 Relative Units
4.2 相对单位
Use for font sizes and spacing. Use for component-relative sizing.
rememcss
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) */使用设置字体大小和间距。使用设置组件相对尺寸。
rememcss
html {
font-size: 100%; /* 默认=16px,尊重用户偏好 */
}
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 */
/* 绝不要:font-size: 16px;(忽略用户缩放设置) */4.3 Line Height and Spacing
4.3 行高与间距
Body text line height of at least 1.5 (SC 1.4.12). Paragraph spacing at least 2x font size.
css
body {
line-height: 1.6;
}
h1, h2, h3 {
line-height: 1.2;
}
p + p {
margin-top: 1em;
}正文字体行高至少为1.5(符合SC 1.4.12标准)。段落间距至少为字体大小的2倍。
css
body {
line-height: 1.6;
}
h1, h2, h3 {
line-height: 1.2;
}
p + p {
margin-top: 1em;
}4.4 Maximum Line Length
4.4 最大行宽
Limit line length to approximately 75 characters for readability.
css
.prose {
max-width: 75ch;
}
/* Or for a content column */
.content {
max-width: 40rem; /* roughly 65-75ch depending on font */
margin-inline: auto;
}将行宽限制在约75个字符以内,提升可读性。
css
.prose {
max-width: 75ch;
}
/* 或针对内容列 */
.content {
max-width: 40rem; /* 根据字体不同,约65-75个字符 */
margin-inline: auto;
}4.5 Typographic Details
4.5 排版细节
Use real quotes, proper dashes, and tabular numbers for data.
css
/* 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; }使用真实引号、正确的破折号和表格数字展示数据。
css
/* 智能引号 */
q { quotes: "\201C" "\201D" "\2018" "\2019"; } /* 先双引号后单引号 */
/* 表格数字用于对齐数据 */
.data-table td {
font-variant-numeric: tabular-nums;
}
/* 正文中的旧式数字(可选) */
.prose {
font-variant-numeric: oldstyle-nums;
}
/* 正确的列表标记 */
ul { list-style-type: disc; }
ol { list-style-type: decimal; }4.6 Heading Hierarchy
4.6 标题层级
Use through in order. Never skip levels. One per page.
h1h6h1html
<!-- Good -->
<h1>Page Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>
<h2>Another Section</h2>
<!-- Bad: skipping h2 -->
<h1>Page Title</h1>
<h3>Subsection</h3> <!-- Where is h2? -->If you need visual styling that differs from the hierarchy, use CSS classes:
html
<h2 class="text-lg">Visually smaller but semantically h2</h2>按顺序使用至。绝不要跳过层级。每页仅使用一个。
h1h6h1html
<!-- 推荐 -->
<h1>页面标题</h1>
<h2>章节</h2>
<h3>子章节</h3>
<h2>另一个章节</h2>
<!-- 不推荐:跳过h2 -->
<h1>页面标题</h1>
<h3>子章节</h3> <!-- h2在哪里? -->如果需要与层级不符的视觉样式,请使用CSS类:
html
<h2 class="text-lg">视觉上更小,但语义上是h2</h2>5. Performance [HIGH]
5. 性能优化 [HIGH]
5.1 Lazy Load Below-Fold Images
5.1 懒加载首屏外图片
Use native lazy loading for images not visible on initial load.
html
<!-- Above fold: load eagerly, add fetchpriority -->
<img src="hero.webp" alt="Hero image" fetchpriority="high" width="1200" height="600">
<!-- Below fold: lazy load -->
<img src="feature.webp" alt="Feature image" loading="lazy" width="600" height="400">对初始加载时不可见的图片使用原生懒加载。
html
<!-- 首屏内图片:立即加载,添加fetchpriority -->
<img src="hero.webp" alt="首屏图片" fetchpriority="high" width="1200" height="600">
<!-- 首屏外图片:懒加载 -->
<img src="feature.webp" alt="功能图片" loading="lazy" width="600" height="400">5.2 Explicit Image Dimensions
5.2 显式图片尺寸
Always specify and to prevent layout shift (CLS).
widthheighthtml
<img src="photo.webp" alt="Description" width="800" height="600">css
/* Responsive images with aspect ratio preservation */
img {
max-width: 100%;
height: auto;
}始终指定和,防止布局偏移(CLS)。
widthheighthtml
<img src="photo.webp" alt="描述" width="800" height="600">css
/* 响应式图片,保持宽高比 */
img {
max-width: 100%;
height: auto;
}5.3 Resource Hints
5.3 资源预提示
Use for third-party origins and for critical resources.
preconnectpreloadhtml
<head>
<!-- Preconnect to critical third-party origins -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<!-- Preload critical resources -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/css/critical.css" as="style">
<!-- DNS prefetch for non-critical origins -->
<link rel="dns-prefetch" href="https://analytics.example.com">
</head>对第三方源使用,对关键资源使用。
preconnectpreloadhtml
<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">
<!-- DNS预解析非关键源 -->
<link rel="dns-prefetch" href="https://analytics.example.com">
</head>5.4 Code Splitting
5.4 代码分割
Load JavaScript only when needed. Use dynamic for route-based and component-based splitting.
import()js
// 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();
});仅在需要时加载JavaScript。使用动态实现基于路由和组件的代码分割。
import()js
// 基于路由的分割
const routes = {
'/dashboard': () => import('./pages/dashboard.js'),
'/settings': () => import('./pages/settings.js'),
};
// 基于交互的分割
button.addEventListener('click', async () => {
const { openEditor } = await import('./editor.js');
openEditor();
});5.5 Virtualize Long Lists
5.5 长列表虚拟化
For lists exceeding a few hundred items, render only visible rows.
js
// 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);对于超过几百条数据的列表,仅渲染可见行。
js
// 概念:虚拟滚动
// 仅渲染视口内的项+缓冲项
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 Avoid Layout Thrashing
5.6 避免布局抖动
Batch DOM reads and writes. Never interleave them.
js
// 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
});批量处理DOM读取和写入操作。绝不要交替进行。
js
// 不推荐:读-写-读-写(强制同步布局)
elements.forEach(el => {
const height = el.offsetHeight; // 读取
el.style.height = height + 10 + 'px'; // 写入
});
// 推荐:批量读取,然后批量写入
const heights = elements.map(el => el.offsetHeight); // 全部读取
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px'; // 全部写入
});5.7 Use will-change
Sparingly
will-change5.7 谨慎使用will-change
will-changeOnly apply to elements that will animate, and remove it after animation completes.
will-changecss
/* Good: scoped and temporary */
.card:hover {
will-change: transform;
}
.card.animating {
will-change: transform, opacity;
}
/* Bad: blanket will-change */
/* * { will-change: transform; } */仅对即将动画的元素应用,并在动画结束后移除。
will-changecss
/* 推荐:作用域有限且临时使用 */
.card:hover {
will-change: transform;
}
.card.animating {
will-change: transform, opacity;
}
/* 不推荐:全局使用will-change */
/* * { will-change: transform; } */6. Animation and Motion [MEDIUM]
6. 动画与动效设计 [MEDIUM]
6.1 Respect prefers-reduced-motion
6.1 尊重prefers-reduced-motion
设置
prefers-reduced-motionAlways provide a reduced-motion alternative (SC 2.3.3).
css
/* 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;
}
}js
// Check in JavaScript
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;始终提供减少动效的替代方案(符合SC 2.3.3标准)。
css
/* 正常定义动画 */
.fade-in {
animation: fadeIn 300ms ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* 为偏好减少动效的用户移除或简化动效 */
@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;
}
}js
// 在JavaScript中检测
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;6.2 Compositor-Friendly Animations
6.2 compositor友好的动画
Animate only and for smooth 60fps animation. These run on the GPU compositor thread.
transformopacitycss
/* 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;
}仅对和设置动画,实现60fps的流畅动画。这些属性在GPU compositor线程运行。
transformopacitycss
// 推荐:仅使用 compositor 属性
.slide-in {
transition: transform 200ms ease-out, opacity 200ms ease-out;
}
// 不推荐:触发布局/绘制
.slide-in-bad {
transition: left 200ms, width 200ms, height 200ms;
}6.3 No Flashing Content
6.3 避免闪烁内容
Never flash content more than 3 times per second (SC 2.3.1). This can trigger seizures.
内容闪烁频率绝不要超过每秒3次(符合SC 2.3.1标准)。这可能引发癫痫发作。
6.4 Transitions for State Changes
6.4 状态变化的过渡效果
Use transitions for hover, focus, open/close, and other state changes to provide continuity.
css
.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;
}为悬停、聚焦、展开/关闭等状态变化添加过渡效果,提供视觉连续性。
css
.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 Meaningful Motion Only
6.5 仅添加有意义的动效
Animation should communicate state, guide attention, or show spatial relationships. Never animate for decoration alone.
动画应传达状态变化、引导注意力或展示空间关系。绝不要仅为装饰而添加动画。
7. Dark Mode and Theming [MEDIUM]
7. 深色模式与主题设计 [MEDIUM]
7.1 System Preference Detection
7.1 系统偏好检测
css
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f0f17;
--text: #e4e4ef;
--surface: #1c1c2e;
--border: #2e2e44;
}
}css
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f0f17;
--text: #e4e4ef;
--surface: #1c1c2e;
--border: #2e2e44;
}
}7.2 CSS Custom Properties for Theming
7.2 使用CSS自定义属性实现主题
Define all theme values as custom properties. Toggle themes by changing property values.
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;
}
}将所有主题值定义为自定义属性。通过修改属性值切换主题。
css
:root {
color-scheme: light dark;
/* 浅色主题(默认) */
--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 Meta Tag
7.3 Color-Scheme元标签
Tell the browser about supported color schemes for native UI elements (scrollbars, form controls).
html
<meta name="color-scheme" content="light dark">告知浏览器支持的颜色方案,适配原生UI元素(滚动条、表单控件)。
html
<meta name="color-scheme" content="light dark">7.4 Maintain Contrast in Both Modes
7.4 两种模式下均保持对比度
Verify contrast ratios in both light and dark modes. Dark mode often suffers from low-contrast text on dark surfaces.
在浅色和深色模式下均验证对比度。深色模式下,深色背景上的文本常出现对比度不足的问题。
7.5 Adaptive Images
7.5 自适应图片
Provide appropriate images for light and dark contexts.
html
<picture>
<source srcset="logo-dark.svg" media="(prefers-color-scheme: dark)">
<img src="logo-light.svg" alt="Company logo">
</picture>css
/* Or use CSS filter for simple cases */
@media (prefers-color-scheme: dark) {
.decorative-img {
filter: brightness(0.9) contrast(1.1);
}
}为浅色和深色场景提供合适的图片。
html
<picture>
<source srcset="logo-dark.svg" media="(prefers-color-scheme: dark)">
<img src="logo-light.svg" alt="公司标志">
</picture>css
// 简单场景下使用CSS滤镜
@media (prefers-color-scheme: dark) {
.decorative-img {
filter: brightness(0.9) contrast(1.1);
}
}8. Navigation and State [MEDIUM]
8. 导航与状态管理 [MEDIUM]
8.1 URL Reflects State
8.1 URL反映状态
Every meaningful view should have a unique URL. Users should be able to bookmark, share, and reload any state.
js
// 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);每个有意义的视图都应有唯一的URL。用户应能收藏、分享和重新加载任意状态。
js
// 不刷新页面更新URL
function updateFilters(filters) {
const params = new URLSearchParams(filters);
history.pushState(null, '', `?${params}`);
renderResults(filters);
}
// 页面加载时从URL恢复状态
const params = new URLSearchParams(location.search);
const initialFilters = Object.fromEntries(params);8.2 Browser Back/Forward
8.2 浏览器前进/后退
Handle to support browser navigation.
popstatejs
window.addEventListener('popstate', () => {
const params = new URLSearchParams(location.search);
renderResults(Object.fromEntries(params));
});处理事件,支持浏览器导航。
popstatejs
window.addEventListener('popstate', () => {
const params = new URLSearchParams(location.search);
renderResults(Object.fromEntries(params));
});8.3 Active Navigation States
8.3 激活的导航状态
Indicate the current page or section in navigation. Use for the active link.
aria-current="page"html
<nav aria-label="Main">
<a href="/" aria-current="page">Home</a>
<a href="/products">Products</a>
<a href="/about">About</a>
</nav>css
[aria-current="page"] {
font-weight: 700;
border-bottom: 2px solid var(--color-primary);
}在导航中标识当前页面或章节。对激活的链接使用。
aria-current="page"html
<nav aria-label="主导航">
<a href="/" aria-current="page">首页</a>
<a href="/products">产品</a>
<a href="/about">关于我们</a>
</nav>css
[aria-current="page"] {
font-weight: 700;
border-bottom: 2px solid var(--color-primary);
}8.4 Breadcrumbs
8.4 面包屑导航
Provide breadcrumbs for sites with deep hierarchies.
html
<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>为层级较深的网站提供面包屑导航。
html
<nav aria-label="面包屑">
<ol>
<li><a href="/">首页</a></li>
<li><a href="/products">产品</a></li>
<li><a href="/products/widgets" aria-current="page">小部件</a></li>
</ol>
</nav>8.5 Scroll Restoration
8.5 滚动位置恢复
Manage scroll position for SPA navigation.
js
// 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)));
}
});管理SPA导航时的滚动位置。
js
// 禁用浏览器自动恢复,手动控制
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
// 导航前保存滚动位置
function saveScrollPosition() {
sessionStorage.setItem(`scroll-${location.pathname}`, window.scrollY);
}
// 前进/后退时恢复
window.addEventListener('popstate', () => {
const saved = sessionStorage.getItem(`scroll-${location.pathname}`);
if (saved) {
requestAnimationFrame(() => window.scrollTo(0, parseInt(saved)));
}
});9. Touch and Interaction [MEDIUM]
9. 触摸与交互设计 [MEDIUM]
9.1 Touch-Action for Scroll Control
9.1 使用Touch-Action控制滚动
Use to control gesture behavior on interactive elements.
touch-actioncss
/* 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;
}使用控制交互元素的手势行为。
touch-actioncss
// 仅允许垂直滚动(禁用水平拖动和捏合缩放)
.vertical-scroll {
touch-action: pan-y;
}
// 轮播:仅允许水平滚动
.carousel {
touch-action: pan-x;
}
// 画布/地图:禁用所有浏览器手势
.canvas {
touch-action: none;
}9.2 Tap Highlight
9.2 点击高亮
Control the tap highlight on mobile WebKit browsers.
css
button, a {
-webkit-tap-highlight-color: transparent;
}控制移动WebKit浏览器上的点击高亮效果。
css
button, a {
-webkit-tap-highlight-color: transparent;
}9.3 Hover and Focus Parity
9.3 悬停与聚焦的一致性
Every hover interaction must also work with keyboard focus.
css
/* 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);
}每个悬停交互也必须支持键盘聚焦。
css
// 始终将:hover与:focus-visible配对使用
.card:hover,
.card:focus-visible {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}9.4 No Hover-Only Interactions
9.4 避免仅悬停触发的交互
Never hide essential functionality behind hover. Touch devices have no hover state.
css
/* 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;
}绝不要将核心功能隐藏在悬停操作后。触摸设备没有悬停状态。
css
// 不推荐:仅悬停时可访问内容
/* .tooltip { display: none; }
.trigger:hover .tooltip { display: block; } */
// 推荐:支持聚焦和点击
.trigger:hover .tooltip,
.trigger:focus-within .tooltip,
.tooltip:focus-within {
display: block;
}9.5 Scroll Snap for Carousels
9.5 轮播的滚动吸附
Use CSS scroll snap for card carousels and horizontal lists.
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);
}使用CSS滚动吸附实现卡片轮播和水平列表。
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. Internationalization [MEDIUM]
10. 国际化设计 [MEDIUM]
10.1 dir and lang Attributes
10.1 dir与lang属性
Set on the element. Use for user-generated content.
lang<html>dir="auto"html
<html lang="en" dir="ltr">
<!-- User-generated content: let browser detect direction -->
<p dir="auto">User-submitted text here</p>
<!-- Explicit override for known RTL content -->
<blockquote lang="ar" dir="rtl">...</blockquote>在元素上设置。对用户生成的内容使用。
<html>langdir="auto"html
<html lang="en" dir="ltr">
<!-- 用户生成内容:让浏览器自动检测方向 -->
<p dir="auto">用户提交的文本</p>
<!-- 显式覆盖已知的RTL内容 -->
<blockquote lang="ar" dir="rtl">...</blockquote>10.2 Intl APIs for Formatting
10.2 使用Intl API进行格式化
Use the API for locale-aware formatting. Never hard-code date or number formats.
Intljs
// 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)]}`; }使用 API实现区域感知的格式化。绝不要硬编码日期或数字格式。
Intljs
// 日期
new Intl.DateTimeFormat('en-US', { dateStyle: 'long' }).format(date);
// "January 15, 2026"
// 数字
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(1234.56);
// "1.234,56 EUR"
// 相对时间
new Intl.RelativeTimeFormat('en', { numeric: 'auto' }).format(-1, 'day');
// "yesterday"
// 列表
new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }).format(['a', 'b', 'c']);
// "a, b, and c"
// 复数
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 Avoid Text in Images
10.3 避免图片中包含文本
Text in images cannot be translated, resized, or read by screen readers. Use HTML/CSS text with background images when a styled text overlay is needed.
图片中的文本无法翻译、调整大小或被屏幕阅读器读取。当需要带样式的文本覆盖层时,使用HTML/CSS文本配合背景图片。
10.4 CSS Logical Properties
10.4 CSS逻辑属性
Use logical properties instead of physical ones to support both LTR and RTL layouts.
css
/* 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 */
}| Physical | Logical |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
使用逻辑属性而非物理属性,同时支持LTR和RTL布局。
css
// 物理属性(在RTL布局中失效)
/* margin-left: 1rem; padding-right: 2rem; border-left: 1px solid; */
// 逻辑属性(在LTR和RTL中均有效)
.sidebar {
margin-inline-start: 1rem;
padding-inline-end: 2rem;
border-inline-start: 1px solid var(--color-border);
}
.stack > * + * {
margin-block-start: 1rem;
}
// 逻辑属性简写
.box {
margin-inline: auto; /* 左+右 */
padding-block: 2rem; /* 上+下 */
inset-inline-start: 0; /* LTR中是左,RTL中是右 */
border-start-start-radius: 8px; /* LTR中是左上,RTL中是右上 */
}| 物理属性 | 逻辑属性 |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
10.5 RTL Layout Support
10.5 RTL布局支持
Test layouts in RTL mode. Flexbox and Grid handle RTL automatically with logical properties.
css
/* 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);
}在RTL模式下测试布局。配合逻辑属性,Flexbox和Grid可自动处理RTL。
css
// 该布局在LTR和RTL中均无需修改即可工作
.layout {
display: flex;
gap: 1rem;
}
// 指示方向的图标需要翻转
[dir="rtl"] .arrow-icon {
transform: scaleX(-1);
}Evaluation Checklist
评估检查清单
Use this checklist when building or reviewing web interfaces.
构建或评审Web界面时使用此检查清单。
Accessibility
无障碍性
- All images have appropriate text
alt - Color contrast meets 4.5:1 (text) and 3:1 (UI components)
- All interactive elements are keyboard accessible
- Focus indicators are visible (3:1 contrast, 2px minimum perimeter)
- Skip navigation link is present
- Form inputs have associated labels
- Error messages are linked to their inputs
- Dynamic content updates use ARIA live regions
- No content flashes more than 3 times per second
- Page has proper heading hierarchy (h1-h6, no skips)
- Landmarks are used correctly (main, nav, header, footer)
- 所有图片均有合适的文本
alt - 颜色对比度符合4.5:1(文本)和3:1(UI组件)要求
- 所有交互元素均可通过键盘访问
- 焦点指示器可见(对比度3:1,最小周长2px)
- 存在跳过导航链接
- 表单输入均有关联标签
- 错误消息与对应输入框关联
- 动态内容更新使用ARIA实时区域
- 内容闪烁频率不超过每秒3次
- 页面有正确的标题层级(h1-h6,无跳过)
- 地标元素使用正确(main、nav、header、footer)
Responsive
响应式
- No horizontal scrolling at 320px width
- Touch targets are at least 44x44px
- Viewport meta tag is present (no user-scalable=no)
- Layout works on mobile, tablet, and desktop
- Text is readable without zooming on mobile
- 在320px宽度下无水平滚动
- 触摸目标尺寸至少为44x44px
- 存在视口元标签(无user-scalable=no)
- 布局在移动端、平板和桌面端均正常工作
- 移动端无需缩放即可阅读文本
Forms
表单
- All inputs have visible labels
- Autocomplete attributes are set for common fields
- Correct input types trigger correct mobile keyboards
- Error messages are clear and specific
- Required fields are indicated
- Submit button is not disabled
- 所有输入均有可见标签
- 常见字段设置了自动完成属性
- 正确的输入类型触发对应的移动端键盘
- 错误消息清晰具体
- 必填字段已标识
- 提交按钮未被禁用
Performance
性能
- Below-fold images use
loading="lazy" - Images have explicit and
widthheight - Critical fonts are preloaded
- Third-party origins use
preconnect - Large JS bundles are code-split
- 首屏外图片使用
loading="lazy" - 图片设置了显式的和
widthheight - 关键字体已预加载
- 第三方源使用了
preconnect - 大型JS包已进行代码分割
Motion and Theming
动效与主题
- is respected
prefers-reduced-motion - Animations use only and
transformopacity - Dark mode maintains contrast ratios
- meta tag is present
color-scheme - Theme uses CSS custom properties
- 已尊重设置
prefers-reduced-motion - 仅对和
transform设置动画opacity - 深色模式下保持对比度
- 存在元标签
color-scheme - 主题使用CSS自定义属性
Internationalization
国际化
- attribute on
lang<html> - CSS logical properties used (not physical)
- Dates/numbers formatted with Intl APIs
- No text embedded in images
- Layout tested in RTL mode
- 元素设置了
<html>属性lang - 使用CSS逻辑属性(而非物理属性)
- 日期/数字使用Intl API格式化
- 图片中无文本
- 已在RTL模式下测试布局
Common Anti-Patterns
常见反模式
| Anti-Pattern | Fix |
|---|---|
| Use |
| Use |
| Add a |
| Use |
| Remove it |
| Use |
Animating | Animate |
| Disabling submit button | Validate on submit, show errors |
| Color alone for status | Add icon, text, or pattern |
| Use |
| Add |
| Hover-only disclosure | Add |
| 反模式 | 修复方案 |
|---|---|
| 使用 |
| 使用 |
仅用 | 添加 |
| 使用 |
| 移除该设置 |
| 使用 |
对 | 对 |
| 禁用提交按钮 | 提交时验证并显示错误 |
| 仅依赖颜色传达状态 | 配合图标、文本或图案 |
| 使用 |
| 添加 |
| 仅悬停可触发的交互 | 添加 |