web-design-guidelines

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Web 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.
ElementPurpose
<main>
Primary page content (one per page)
<nav>
Navigation blocks
<header>
Introductory content or navigational aids
<footer>
Footer for nearest sectioning content
<article>
Self-contained, independently distributable content
<section>
Thematic grouping with a heading
<aside>
Tangentially related content (sidebars, callouts)
<figure>
/
<figcaption>
Illustrations, diagrams, code listings
<details>
/
<summary>
Expandable/collapsible disclosure widget
<dialog>
Modal or non-modal dialog boxes
<time>
Machine-readable dates/times
<mark>
Highlighted/referenced text
<address>
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
<div>
or
<span>
for interactive elements. Never write
<div onclick>
when
<button>
exists.
按元素的预期用途使用它们。语义化结构可免费提供无障碍支持、SEO优化和阅读器模式适配。
元素用途
<main>
页面主要内容(每页一个)
<nav>
导航区块
<header>
介绍性内容或导航辅助区域
<footer>
最近的分段内容的页脚
<article>
独立可分发的自包含内容
<section>
带标题的主题分组
<aside>
间接相关内容(侧边栏、提示框)
<figure>
/
<figcaption>
插图、图表、代码清单
<details>
/
<summary>
可展开/折叠的披露组件
<dialog>
模态或非模态对话框
<time>
机器可读的日期/时间
<mark>
高亮/引用文本
<address>
最近的文章/页面主体的联系信息
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
aria-label
or
aria-labelledby
only when visible text is insufficient (SC 4.1.2).
html
<!-- 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 -->
每个交互元素必须有一个可访问的名称。优先使用可见文本;仅当可见文本不足时,才使用
aria-label
aria-labelledby
(符合SC 4.1.2标准)。
html
<!-- 仅含图标按钮:需要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>
    ,
    <select>
    ) which are keyboard-accessible by default.
  • Custom widgets need
    tabindex="0"
    to enter tab order and keydown handlers for activation.
  • Never use
    tabindex
    values greater than 0.
  • 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>
    ),它们默认支持键盘访问。
  • 自定义组件需要
    tabindex="0"
    以加入Tab顺序,并通过keydown事件处理器实现激活。
  • 绝不要使用大于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
<img>
must have an
alt
attribute (SC 1.1.1).
  • Informative images: describe the content and function.
    alt="Bar chart showing sales doubled in Q4"
    .
  • Decorative images: use
    alt=""
    (empty string) so screen readers skip them.
  • Functional images (inside links/buttons): describe the action.
    alt="Search"
    .
  • Complex images: use
    alt
    for short description, link to long description or use
    <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="">
每个
<img>
必须包含
alt
属性(符合SC 1.1.1标准)。
  • 信息性图片:描述内容和功能。例如
    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).
ContentMinimum Ratio
Normal text (<24px / <18.66px bold)4.5:1
Large text (>=24px / >=18.66px bold)3:1
UI components and graphical objects3: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
aria-describedby
or
aria-errormessage
.
html
<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-describedby
aria-errormessage
将错误消息与输入框关联。
html
<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
aria-live="polite"
by default. Reserve
role="alert"
/
aria-live="assertive"
for time-sensitive warnings.
向屏幕阅读器播报动态内容的变化(符合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角色速查

RolePurposeNative Equivalent
button
Clickable action
<button>
link
Navigation
<a href>
tab
/
tablist
/
tabpanel
Tab interfaceNone
dialog
Modal
<dialog>
alert
Assertive live regionNone
status
Polite live region
<output>
navigation
Nav landmark
<nav>
main
Main landmark
<main>
complementary
Aside landmark
<aside>
search
Search landmark
<search>
(HTML5)
img
Image
<img>
list
/
listitem
List
<ul>/<li>
heading
Heading (with
aria-level
)
<h1>
-
<h6>
menu
/
menuitem
Menu widgetNone
tree
/
treeitem
Tree viewNone
grid
/
row
/
gridcell
Data grid
<table>
progressbar
Progress
<progress>
slider
Range input
<input type="range">
switch
Toggle
<input type="checkbox">
Rule: Prefer native HTML over ARIA. Use ARIA only when no native element exists for the pattern.

角色用途原生替代元素
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。

2. Responsive Design [CRITICAL]

2. 响应式设计 [CRITICAL]

2.1 Mobile-First Approach

2.1 移动优先方法

Write base styles for the smallest viewport. Layer complexity with
min-width
media queries.
css
/* 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-width
媒体查询逐步增加复杂度。
css
/* 基础样式:移动端 */
.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
clamp()
,
min()
, and
max()
for fluid sizing without breakpoints.
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
maximum-scale=1
or
user-scalable=no
-- these break pinch-to-zoom accessibility (SC 1.4.4).
务必在文档
<head>
中包含:
html
<meta name="viewport" content="width=device-width, initial-scale=1">
绝不要使用
maximum-scale=1
user-scalable=no
——这些设置会破坏捏合缩放的无障碍性(符合SC 1.4.4标准)。

2.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
autocomplete
for common fields to enable browser autofill (SC 1.3.5).
html
<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">
为常见字段使用
autocomplete
属性,启用浏览器自动填充功能(符合SC 1.3.5标准)。
html
<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
type
to trigger appropriate mobile keyboards and native validation.
TypeUse For
email
Email addresses
tel
Phone numbers
url
URLs
number
Numeric values with spinners (not for phone, zip, card numbers)
search
Search fields (shows clear button)
date
/
time
/
datetime-local
Temporal values
password
Passwords (triggers password manager)
text
with
inputmode="numeric"
Numeric data without spinners (PINs, zip codes)
html
<input type="tel" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code">
使用合适的
type
属性,触发对应的移动端键盘和原生验证。
类型适用场景
email
电子邮箱地址
tel
电话号码
url
网址
number
带微调器的数值(不适用于电话、邮编、卡号)
search
搜索字段(显示清除按钮)
date
/
time
/
datetime-local
时间相关值
password
密码(触发密码管理器)
text
配合
inputmode="numeric"
无微调器的数值(PIN码、邮编)
html
<input type="tel" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code">

3.4 Inline Validation

3.4 内联验证

Validate on
blur
(not on every keystroke). Show success and error states.
html
<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; }
blur
事件时验证(而非每次按键)。显示成功和错误状态。
html
<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
<fieldset>
and label the group with
<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
required
attribute and visible markers.
html
<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.
通过视觉和程序化方式标识必填字段。使用
required
属性和可见标记。
html
<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
rem
for font sizes and spacing. Use
em
for component-relative sizing.
css
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) */
使用
rem
设置字体大小和间距。使用
em
设置组件相对尺寸。
css
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
h1
through
h6
in order. Never skip levels. One
h1
per page.
html
<!-- 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>

按顺序使用
h1
h6
。绝不要跳过层级。每页仅使用一个
h1
html
<!-- 推荐 -->
<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
width
and
height
to prevent layout shift (CLS).
html
<img src="photo.webp" alt="Description" width="800" height="600">
css
/* Responsive images with aspect ratio preservation */
img {
  max-width: 100%;
  height: auto;
}
始终指定
width
height
,防止布局偏移(CLS)。
html
<img src="photo.webp" alt="描述" width="800" height="600">
css
/* 响应式图片,保持宽高比 */
img {
  max-width: 100%;
  height: auto;
}

5.3 Resource Hints

5.3 资源预提示

Use
preconnect
for third-party origins and
preload
for critical resources.
html
<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>
对第三方源使用
preconnect
,对关键资源使用
preload
html
<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
import()
for route-based and component-based splitting.
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

5.7 谨慎使用
will-change

Only apply
will-change
to elements that will animate, and remove it after animation completes.
css
/* Good: scoped and temporary */
.card:hover {
  will-change: transform;
}
.card.animating {
  will-change: transform, opacity;
}

/* Bad: blanket will-change */
/* * { will-change: transform; } */

仅对即将动画的元素应用
will-change
,并在动画结束后移除。
css
/* 推荐:作用域有限且临时使用 */
.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
设置

Always 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
transform
and
opacity
for smooth 60fps animation. These run on the GPU compositor thread.
css
/* 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;
}
仅对
transform
opacity
设置动画,实现60fps的流畅动画。这些属性在GPU compositor线程运行。
css
// 推荐:仅使用 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
popstate
to support browser navigation.
js
window.addEventListener('popstate', () => {
  const params = new URLSearchParams(location.search);
  renderResults(Object.fromEntries(params));
});
处理
popstate
事件,支持浏览器导航。
js
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
aria-current="page"
for the active link.
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
touch-action
to control gesture behavior on interactive elements.
css
/* 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-action
控制交互元素的手势行为。
css
// 仅允许垂直滚动(禁用水平拖动和捏合缩放)
.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
lang
on the
<html>
element. Use
dir="auto"
for user-generated content.
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>
元素上设置
lang
。对用户生成的内容使用
dir="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
Intl
API for locale-aware formatting. Never hard-code date or number formats.
js
// 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)]}`; }
使用
Intl
API实现区域感知的格式化。绝不要硬编码日期或数字格式。
js
// 日期
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 */
}
PhysicalLogical
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
使用逻辑属性而非物理属性,同时支持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中是右上 */
}
物理属性逻辑属性
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 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
    alt
    text
  • 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
    width
    and
    height
  • Critical fonts are preloaded
  • Third-party origins use
    preconnect
  • Large JS bundles are code-split
  • 首屏外图片使用
    loading="lazy"
  • 图片设置了显式的
    width
    height
  • 关键字体已预加载
  • 第三方源使用了
    preconnect
  • 大型JS包已进行代码分割

Motion and Theming

动效与主题

  • prefers-reduced-motion
    is respected
  • Animations use only
    transform
    and
    opacity
  • Dark mode maintains contrast ratios
  • color-scheme
    meta tag is present
  • Theme uses CSS custom properties
  • 已尊重
    prefers-reduced-motion
    设置
  • 仅对
    transform
    opacity
    设置动画
  • 深色模式下保持对比度
  • 存在
    color-scheme
    元标签
  • 主题使用CSS自定义属性

Internationalization

国际化

  • lang
    attribute on
    <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-PatternFix
<div onclick="...">
Use
<button>
outline: none
without replacement
Use
:focus-visible
with custom outline
placeholder
as label
Add a
<label>
element
tabindex="5"
Use
tabindex="0"
or natural order
user-scalable=no
Remove it
font-size: 12px
Use
font-size: 0.75rem
Animating
width
/
height
/
top
/
left
Animate
transform
and
opacity
Disabling submit buttonValidate on submit, show errors
Color alone for statusAdd icon, text, or pattern
margin-left
/
padding-right
Use
margin-inline-start
/
padding-inline-end
<img>
without dimensions
Add
width
and
height
attributes
Hover-only disclosureAdd
:focus-within
and click handler
反模式修复方案
<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
和点击处理器