mcpField
W
web-design-guidelines

web-design-guidelines

github.com/ehmo设计与 UI6331.5k 次安装32 周前更新
代码仓库

关于 web-design-guidelines

面向 Web 设计与开发者的跨框架指南,汇总无障碍、响应式布局、表单和排版规则,并以 WCAG 2.2、MDN Web Docs 和现代 Web API 为依据。

主要功能

  1. 01WCAG 2.2 无障碍实践
  2. 02语义 HTML 与 ARIA 用法
  3. 03移动优先响应式布局
  4. 04表单标签与校验建议
  5. 05字体与可读性规范

适用场景

  1. 01检查交互控件的键盘可用性
  2. 02规划适配不同视口的网页
  3. 03改进表单标签和错误提示

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/ehmo/platform-design-skills --skill web-design-guidelines

安装后在 Claude Code 中输入/web-design-guidelines确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

AGENTS.md
metadata.json
rules/
_sections.md

详细介绍

Web 平台设计指南

适用于各种框架的无障碍、高性能、响应式 Web 界面规则。基于 WCAG 2.2、MDN Web Docs 和现代 Web 平台 API。


1. 无障碍 / WCAG [CRITICAL]

无障碍并非可选项。本节的大多数规则对应 WCAG 2.2 的 A 级或 AA 级成功准则。少数最佳实践规则(已在正文中注明)以 AAA 级为目标或超出 WCAG 要求。

1.1 使用语义化 HTML 元素

按元素的预期用途使用元素。语义结构可免费提供无障碍、SEO 和阅读模式支持。

元素用途
<main>页面主要内容(每页一个)
<nav>导航区块
<header>介绍性内容或导航辅助内容
<footer>最近的分节内容的页脚
<article>独立完整、可单独分发的内容
<section>带标题的主题分组
<aside>相关性较弱的内容(侧边栏、提示框)
<figure> / <figcaption>插图、图表、代码清单
<details> / <summary>可展开/折叠的披露控件
<dialog>模态或非模态对话框
<time>机器可读的日期/时间
<mark>高亮或引用的文本
<address>最近的文章/正文的联系信息

<main>
  <article>
    <h1>Article Title</h1>
    <p>Content...</p>
  </article>
  <aside>Related links</aside>
</main>


<div class="main">
  <div class="article">
    <div class="title">Article Title</div>
    <div class="content">Content...</div>
  </div>
</div>

反模式:使用 <div> 或 <span> 作为交互元素。存在 <button> 时,绝不要编写 <div onclick>。

1.2 交互元素上的 ARIA 标签

每个交互元素都必须有无障碍名称。优先使用可见文本;仅在可见文本不足时才使用 aria-label 或 aria-labelledby(SC 4.1.2)。


<button aria-label="Close dialog">
  <svg aria-hidden="true">...</svg>
</button>


<h2 id="section-title">Notifications</h2>
<ul aria-labelledby="section-title">...</ul>


<button>Save Changes</button> 
1.3 键盘导航

所有交互元素都必须可通过键盘访问和操作(SC 2.1.1)。

  • 使用原生交互元素(<button>、<a href>、<input>、<select>),它们默认支持键盘操作。
  • 自定义控件需要设置 tabindex="0" 以进入 Tab 顺序,并添加用于激活控件的 keydown 处理程序。
  • 绝不要使用大于 0 的 tabindex 值。
  • 将焦点限制在模态框内;关闭时恢复焦点。
// Focus trap for modal
dialog.addEventListener('keydown', (e) => {
  if (e.key === 'Tab') {
    const focusable = dialog.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    const first = focusable[0];
    const last = focusable[focusable.length - 1];
    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  }
});
1.4 可见的焦点指示器

移除焦点轮廓时,必须提供可见的替代样式(SC 2.4.7;WCAG 2.2 中增强的 SC 2.4.11(AA)和 SC 2.4.12(AAA))。

/* Good: custom focus indicator */
:focus-visible {
  outline: 3px solid var(--focus-color, #4A90D9);
  outline-offset: 2px;
}

/* Remove default only when :focus-visible is supported */
:focus:not(:focus-visible) {
  outline: none;
}

/* Bad: removing all focus styles */
/* *:focus { outline: none; } */

WCAG 2.2 要求焦点指示器的最小面积为组件周长乘以 2px,且与相邻颜色的对比度为 3:1。

1.5 跳过导航链接

提供跳过重复内容区块的机制(SC 2.4.1)。

<body>
  <a href="#main-content" class="skip-link">Skip to main content</a>
  <nav>...</nav>
  <main id="main-content">...</main>
</body>
.skip-link {
  position: absolute;
  top: -100%;
  left: 0;
  z-index: 1000;
  padding: 0.75rem 1.5rem;
  background: var(--color-primary);
  color: var(--color-on-primary);
}
.skip-link:focus {
  top: 0;
}
1.6 图像的替代文本

每个 <img> 都必须有 alt 属性(SC 1.1.1)。

  • 信息性图像:描述其内容和功能。alt="Bar chart showing sales doubled in Q4"。
  • 装饰性图像:使用 alt=""(空字符串),以便屏幕阅读器跳过它们。
  • 功能性图像(位于链接/按钮内):描述其操作。alt="Search"。
  • 复杂图像:使用 alt 提供简短描述,链接到详细描述,或使用 <figcaption>。
<img src="chart.png" alt="Revenue chart: Q1 $2M, Q2 $2.4M, Q3 $3.1M, Q4 $4.5M">
<img src="decorative-wave.svg" alt="">
1.7 色彩对比度

保持最低对比度(SC 1.4.3、1.4.6、1.4.11)。

内容最低对比度
正文文本(<24px / 粗体 <18.66px)4.5:1
大号文本(>=24px / 粗体 >=18.66px)3:1
UI 组件和图形对象3:1

不要仅依靠颜色传达信息(SC 1.4.1)。将颜色与图标、文本或图案搭配使用。

/* Check contrast of these tokens */
:root {
  --text-primary: #1a1a2e;    /* on white: ~16:1 */
  --text-secondary: #555770;  /* on white: ~6.5:1 */
  --text-disabled: #767693;   /* on white: ~4.5:1, borderline */
}
1.8 表单标签

每个表单输入项都必须有以编程方式关联的标签(SC 1.3.1、3.3.2)。


<label for="email">Email address</label>
<input id="email" type="email" autocomplete="email">


<label>
  Email address
  <input type="email" autocomplete="email">
</label>



1.9 错误识别

以文本形式指出并描述错误(SC 3.3.1)。使用 aria-describedby 或 aria-errormessage 将错误消息与输入项关联。

<label for="email">Email</label>
<input id="email" type="email" aria-describedby="email-error" aria-invalid="true">
<p id="email-error" role="alert">Enter a valid email address, e.g. [email protected]</p>
1.10 ARIA 实时区域

向屏幕阅读器播报动态内容变化(SC 4.1.3)。


<div aria-live="polite" aria-atomic="true">
  3 results found
</div>


<div role="alert">
  Your session will expire in 2 minutes.
</div>


<div role="status">
  File uploaded successfully.
</div>

默认使用 aria-live="polite"。仅将 role="alert" / aria-live="assertive" 用于有时效性的警告。

1.11 ARIA 角色速查表
角色用途原生等效项
button可点击的操作<button>
link导航<a href>
tab / tablist / tabpanel标签页界面无
dialog模态框<dialog>
alert紧急实时区域无
status礼貌实时区域<output>
navigation导航地标<nav>
main主内容地标<main>
complementary辅助内容地标<aside>
search搜索地标<search> (HTML5)
img图像<img>
list / listitem列表<ul>/<li>
heading标题(使用 aria-level)<h1>-<h6>
menu / menuitem菜单组件无
tree / treeitem树形视图无
grid / row / gridcell数据网格<table>
progressbar进度<progress>
slider范围输入<input type="range">
switch切换开关<input type="checkbox">

规则:优先使用原生 HTML,而不是 ARIA。仅当该模式没有对应的原生元素时才使用 ARIA。

1.12 名称中的标签(WCAG 2.5.3 A 级)

当交互元素具有可见文本时,其可访问名称必须包含该可见文本作为子字符串(SC 2.5.3)。语音控制用户(Dragon NaturallySpeaking、macOS Voice Control)会说出可见标签来激活控件。如果 aria-label 替代或与可见文本相矛盾,语音命令就会失效。


<button aria-label="Delete item from cart">Delete</button>


<button>Save Changes</button>


<button aria-label="Close dialog">
  <svg aria-hidden="true">...</svg>
</button>

<button aria-label="Remove">Delete</button>


<button aria-label="Proceed to next step">Submit</button>

规则:存在可见文本时,aria-label 必须包含该可见文本(原样保留,不区分大小写)。如果可见文本已足够清晰,最好完全不使用 aria-label。


2. 响应式设计 [CRITICAL]

2.1 移动优先方法

为最小视口编写基础样式。使用 min-width 媒体查询逐步增加复杂样式。

/* Base: mobile */
.grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
}

/* Tablet */
@media (min-width: 48rem) {
  .grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Desktop */
@media (min-width: 64rem) {
  .grid {
    grid-template-columns: repeat(3, 1fr);
  }
}
2.2 使用现代 CSS 函数创建流式布局

使用 clamp()、min() 和 max() 实现流式尺寸调整,无需断点。

/* Fluid typography */
h1 {
  font-size: clamp(1.75rem, 1.2rem + 2vw, 3rem);
}

/* Fluid spacing */
.section {
  padding: clamp(1.5rem, 4vw, 4rem);
}

/* Fluid container */
.container {
  width: min(90%, 72rem);
  margin-inline: auto;
}
2.3 容器查询

根据组件所在的容器而非视口调整其尺寸。

.card-container {
  container-type: inline-size;
  container-name: card;
}

@container card (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 200px 1fr;
  }
}

@container card (min-width: 700px) {
  .card {
    grid-template-columns: 300px 1fr;
    gap: 2rem;
  }
}
2.4 基于内容设置断点

在内容需要调整的位置设置断点,而不是根据设备宽度。常见的起始点:

/* Content-based, not "iPhone" or "iPad" */
@media (min-width: 30rem)  { /* ~480px: single column gets cramped */ }
@media (min-width: 48rem)  { /* ~768px: room for 2 columns */ }
@media (min-width: 64rem)  { /* ~1024px: room for sidebar + content */ }
@media (min-width: 80rem)  { /* ~1280px: wide multi-column */ }
2.5 触控目标

触控目标至少为 44x44 CSS 像素(WCAG SC 2.5.5 AAA;SC 2.5.8 在 AA 级仅要求 24x24px)。相邻目标之间至少留出 24px 间距。

button, a, input, select, textarea {
  min-height: 44px;
  min-width: 44px;
}

/* Enlarge tap area without changing visual size */
.icon-button {
  position: relative;
  width: 24px;
  height: 24px;
}
.icon-button::after {
  content: "";
  position: absolute;
  inset: -10px; /* expands clickable area */
}
2.6 视口 meta 标签

始终在文档的 <head> 中包含:

<meta name="viewport" content="width=device-width, initial-scale=1">

切勿使用 maximum-scale=1 或 user-scalable=no——这会破坏缩放手势的无障碍体验(SC 1.4.4)。

2.7 禁止水平滚动

内容必须能在宽度为 320px 时重排,不出现水平滚动(SC 1.4.10)。

/* Prevent overflow */
img, video, iframe, svg {
  max-width: 100%;
  height: auto;
}

/* Contain long words/URLs */
.prose {
  overflow-wrap: break-word;
}

/* Tables: scroll container, not page */
.table-wrapper {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

3. 表单 [HIGH]

3.1 为每个输入项添加标签

每个输入项都需要一个可见且以编程方式关联的标签。参见第 1.8 节。

3.2 自动填充属性

为常见字段使用 autocomplete,以启用浏览器自动填充(SC 1.3.5)。

<input type="text" autocomplete="name" name="full-name">
<input type="email" autocomplete="email" name="email">
<input type="tel" autocomplete="tel" name="phone">
<input type="text" autocomplete="street-address" name="address">
<input type="text" autocomplete="postal-code" name="zip">
<input type="text" autocomplete="cc-name" name="card-name">
<input type="text" autocomplete="cc-number" name="card-number">
<input type="password" autocomplete="new-password" name="password">
<input type="password" autocomplete="current-password" name="current-pw">
3.3 使用正确的输入类型

使用合适的 type,以调用相应的移动键盘并启用原生验证。

类型用途
email电子邮件地址
tel电话号码
urlURL
number带微调控件的数值(不用于电话号码、邮政编码或银行卡号)
search搜索字段(显示清除按钮)
date / time / datetime-local时间值
password密码(会触发密码管理器)
text,搭配 inputmode="numeric"不带微调控件的数值数据(PIN 码、邮政编码)
<input type="tel" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code">
3.4 行内验证

在 blur 时验证(不要在每次按键时都验证)。显示成功和错误状态。

<div class="field" data-state="error">
  <label for="username">Username</label>
  <input id="username" type="text" aria-describedby="username-hint username-error" aria-invalid="true">
  <p id="username-hint" class="hint">3-20 characters, letters and numbers only</p>
  <p id="username-error" class="error" role="alert">Username must be at least 3 characters</p>
</div>
.field[data-state="error"] input {
  border-color: var(--color-error);
  box-shadow: 0 0 0 1px var(--color-error);
}
.field[data-state="error"] .error { display: block; }
.field:not([data-state="error"]) .error { display: none; }
3.5 使用 Fieldset 和 Legend 分组

使用 <fieldset> 对相关输入项分组,并使用 <legend> 为该组添加标签。

<fieldset>
  <legend>Shipping Address</legend>
  <label for="street">Street</label>
  <input id="street" type="text" autocomplete="street-address">
  
</fieldset>

<fieldset>
  <legend>Preferred contact method</legend>
  <label><input type="radio" name="contact" value="email"> Email</label>
  <label><input type="radio" name="contact" value="phone"> Phone</label>
</fieldset>
3.6 标明必填字段

以视觉和编程方式标明必填字段。使用 required 属性和可见标记。

<label for="name">
  Full name <span aria-hidden="true">*</span>
  <span class="sr-only">(required)</span>
</label>
<input id="name" type="text" required autocomplete="name">

如果大多数字段都是必填项,则改为标明哪些字段是选填的。

3.7 提交按钮状态

不要禁用提交按钮,而应在提交时进行验证并显示错误。


<button type="submit">Create Account</button>



禁用按钮无法说明用户为什么无法继续。如果必须禁用,请提供清晰可见的解释。

3.8 将说明放在字段旁边

通过提示和错误文本,将格式示例、限制条件和问题解决说明放在相关字段旁边。切勿只在介绍性文字中说明一次要求,就指望用户之后还记得。


4. 排版 [HIGH]

4.1 字体栈

为提升性能,请使用系统字体栈;或者使用带有适当回退字体的 Web 字体。

/* System font stack */
body {
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}

/* Monospace stack */
code, pre, kbd {
  font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;
}

/* Web font with fallbacks and size-adjust */
@font-face {
  font-family: "CustomFont";
  src: url("/fonts/custom.woff2") format("woff2");
  font-display: swap;
  font-weight: 100 900;
}
body {
  font-family: "CustomFont", system-ui, sans-serif;
}
4.2 相对单位

字体大小和间距使用 rem。组件相对尺寸使用 em。

html {
  font-size: 100%; /* = 16px default, respects user preference */
}

body {
  font-size: 1rem;       /* 16px */
}

h1 { font-size: 2.5rem; }  /* 40px */
h2 { font-size: 2rem; }    /* 32px */
h3 { font-size: 1.5rem; }  /* 24px */
small { font-size: 0.875rem; } /* 14px */

/* Never: font-size: 16px; (ignores user zoom settings) */
4.3 行高和间距

正文行高至少为 1.5(SC 1.4.12)。段落间距至少为字体大小的 2 倍。

body {
  line-height: 1.6;
}

h1, h2, h3 {
  line-height: 1.2;
}

p + p {
  margin-top: 1em;
}
4.4 最大行长

将行长限制在约 75 个字符,以提高可读性。

.prose {
  max-width: 75ch;
}

/* Or for a content column */
.content {
  max-width: 40rem; /* roughly 65-75ch depending on font */
  margin-inline: auto;
}
4.5 排版细节

使用正确的引号、破折号和用于数据的等宽数字。

/* Smart quotes */
q { quotes: "\201C" "\201D" "\2018" "\2019"; } /* curly double then single */

/* Tabular numbers for aligned data */
.data-table td {
  font-variant-numeric: tabular-nums;
}

/* Oldstyle numbers for running prose (optional) */
.prose {
  font-variant-numeric: oldstyle-nums;
}

/* Proper list markers */
ul { list-style-type: disc; }
ol { list-style-type: decimal; }
4.6 标题层级

按顺序使用 h1 到 h6。切勿跳过层级。每页仅使用一个 h1。


<h1>Page Title</h1>
  <h2>Section</h2>
    <h3>Subsection</h3>
  <h2>Another Section</h2>


<h1>Page Title</h1>
  <h3>Subsection</h3> 

如果需要与层级不同的视觉样式,请使用 CSS 类:

<h2 class="text-lg">Visually smaller but semantically h2</h2>

5. 性能 [HIGH]

5.1 延迟加载首屏以下的图片

对于初始加载时不可见的图片,请使用原生延迟加载。


<img src="hero.webp" alt="Hero image" fetchpriority="high" width="1200" height="600">


<img src="feature.webp" alt="Feature image" loading="lazy" width="600" height="400">
5.2 明确指定图片尺寸

始终指定 width 和 height,以防止布局偏移(CLS)。

<img src="photo.webp" alt="Description" width="800" height="600">
/* Responsive images with aspect ratio preservation */
img {
  max-width: 100%;
  height: auto;
}
5.3 资源提示

对第三方来源使用 preconnect,对关键资源使用 preload。

<head>
  
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://cdn.example.com" crossorigin>

  
  <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
  <link rel="preload" href="/css/critical.css" as="style">

  
  <link rel="dns-prefetch" href="https://analytics.example.com">
</head>
5.4 代码拆分

仅在需要时加载 JavaScript。使用动态 import() 按路由和组件拆分代码。

// Route-based splitting
const routes = {
  '/dashboard': () => import('./pages/dashboard.js'),
  '/settings':  () => import('./pages/settings.js'),
};

// Interaction-based splitting
button.addEventListener('click', async () => {
  const { openEditor } = await import('./editor.js');
  openEditor();
});
5.5 虚拟化长列表

对于超过数百项的列表,只渲染可见行。

// Concept: virtual scrolling
// Render only items in viewport + buffer
const visibleStart = Math.floor(scrollTop / itemHeight);
const visibleEnd = visibleStart + Math.ceil(containerHeight / itemHeight);
const buffer = 5;
const renderStart = Math.max(0, visibleStart - buffer);
const renderEnd = Math.min(totalItems, visibleEnd + buffer);
5.6 避免布局抖动

批量执行 DOM 读取和写入操作。切勿交替执行。

// Bad: read-write-read-write (forces synchronous layout)
elements.forEach(el => {
  const height = el.offsetHeight;     // read
  el.style.height = height + 10 + 'px'; // write
});

// Good: batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight); // all reads
elements.forEach((el, i) => {
  el.style.height = heights[i] + 10 + 'px'; // all writes
});
5.7 谨慎使用 will-change

仅对将要执行动画的元素应用 will-change,并在动画完成后移除。

/* Good: scoped and temporary */
.card:hover {
  will-change: transform;
}
.card.animating {
  will-change: transform, opacity;
}

/* Bad: blanket will-change */
/* * { will-change: transform; } */
5.8 及时显示等待状态

用户执行操作后,立即反馈新的状态。如果工作无法在短时间内完成,请显示进度、骨架屏、乐观 UI 或 aria-busy 反馈,而不要让界面保持不变。


6. 动画与动态效果 [MEDIUM]

6.1 遵循 prefers-reduced-motion

始终提供减少动态效果的替代方案(SC 2.3.3,AAA 级)。

/* Define animations normally */
.fade-in {
  animation: fadeIn 300ms ease-out;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Remove or reduce for users who prefer it */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
// Check in JavaScript
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
6.2 使用合成器友好的动画

为实现流畅的 60fps 动画,仅对 transform 和 opacity 设置动画。这些属性由 GPU 合成器线程处理。

/* Good: compositor-only properties */
.slide-in {
  transition: transform 200ms ease-out, opacity 200ms ease-out;
}

/* Bad: triggers layout/paint */
.slide-in-bad {
  transition: left 200ms, width 200ms, height 200ms;
}
6.3 不要使用闪烁内容

内容闪烁频率切勿超过每秒 3 次(SC 2.3.1),否则可能引发癫痫发作。

6.4 为状态变化添加过渡效果

为悬停、焦点、打开/关闭和其他状态变化添加过渡效果,以保持连贯性。

.dropdown {
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 150ms ease-out, transform 150ms ease-out;
  pointer-events: none;
}
.dropdown.open {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}
6.5 仅使用有意义的动态效果

动画应传达状态、引导注意力或展示空间关系。切勿仅为装饰而添加动画。


7. 暗色模式与主题 [MEDIUM]

7.1 检测系统偏好设置
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f0f17;
    --text: #e4e4ef;
    --surface: #1c1c2e;
    --border: #2e2e44;
  }
}
7.2 使用 CSS 自定义属性设置主题

将所有主题值定义为自定义属性。通过更改属性值来切换主题。

:root {
  color-scheme: light dark;

  /* Light theme (default) */
  --color-bg: #ffffff;
  --color-surface: #f5f5f7;
  --color-text-primary: #1a1a2e;
  --color-text-secondary: #555770;
  --color-border: #d1d1e0;
  --color-primary: #2563eb;
  --color-primary-text: #ffffff;
  --color-error: #dc2626;
  --color-success: #16a34a;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #0f0f17;
    --color-surface: #1c1c2e;
    --color-text-primary: #e4e4ef;
    --color-text-secondary: #a0a0b8;
    --color-border: #2e2e44;
    --color-primary: #60a5fa;
    --color-primary-text: #0f0f17;
    --color-error: #f87171;
    --color-success: #4ade80;
  }
}
7.3 color-scheme 元标签

告知浏览器支持的配色方案,以便设置原生 UI 元素(滚动条、表单控件)。

<meta name="color-scheme" content="light dark">
7.4 确保两种模式下的对比度

验证浅色和暗色模式下的对比度。暗色模式中的深色表面常出现文本对比度不足的问题。

7.5 自适应图片

为浅色和暗色环境提供合适的图片。

<picture>
  <source srcset="logo-dark.svg" media="(prefers-color-scheme: dark)">
  <img src="logo-light.svg" alt="Company logo">
</picture>
/* Or use CSS filter for simple cases */
@media (prefers-color-scheme: dark) {
  .decorative-img {
    filter: brightness(0.9) contrast(1.1);
  }
}
7.6 遵循 prefers-contrast

使用 @media (prefers-contrast: more) 和 @media (prefers-contrast: forced) 遵循用户的对比度偏好。prefers-contrast: more 会响应 macOS/iOS「增加对比度」系统设置;prefers-contrast: forced 会响应 Windows 高对比度模式——这是一项独立的操作系统功能,会完全覆盖颜色。

/* Default theme */
:root {
  --color-text: #555770;
  --color-border: #d1d1e0;
  --color-bg: #ffffff;
}

/* High contrast mode: stronger text and border colors */
@media (prefers-contrast: more) {
  :root {
    --color-text: #1a1a2e;       /* Darker text for higher ratio */
    --color-border: #1a1a2e;     /* Stronger borders */
    --color-bg: #ffffff;
  }

  /* Ensure interactive elements are clearly delineated */
  button, input, select, textarea {
    border: 2px solid currentColor;
  }
}

/* Forced colors (Windows High Contrast mode) */
@media (prefers-contrast: forced) {
  /* Use system color keywords to respect OS color palette */
  :root {
    --color-text: ButtonText;
    --color-bg: ButtonFace;
    --color-border: ButtonBorder;
  }
}

8. 导航与状态 [MEDIUM]

8.1 URL 反映状态

每个有意义的视图都应有唯一的 URL。用户应能为任何状态添加书签、分享并重新加载。

// Update URL without full page reload
function updateFilters(filters) {
  const params = new URLSearchParams(filters);
  history.pushState(null, '', `?${params}`);
  renderResults(filters);
}

// Restore state from URL on load
const params = new URLSearchParams(location.search);
const initialFilters = Object.fromEntries(params);
8.2 浏览器前进/后退

处理 popstate,以支持浏览器导航。

window.addEventListener('popstate', () => {
  const params = new URLSearchParams(location.search);
  renderResults(Object.fromEntries(params));
});
8.3 当前导航状态

在导航中标示当前页面或分区。为活动链接使用 aria-current="page"。

<nav aria-label="Main">
  <a href="/" aria-current="page">Home</a>
  <a href="/products">Products</a>
  <a href="/about">About</a>
</nav>
[aria-current="page"] {
  font-weight: 700;
  border-bottom: 2px solid var(--color-primary);
}
8.4 面包屑导航

对于层级较深的网站,提供面包屑导航。

<nav aria-label="Breadcrumb">
  <ol>
    <li><a href="/">Home</a></li>
    <li><a href="/products">Products</a></li>
    <li><a href="/products/widgets" aria-current="page">Widgets</a></li>
  </ol>
</nav>
8.5 滚动位置恢复

管理 SPA 导航时的滚动位置。

// Disable browser auto-restoration for manual control
if ('scrollRestoration' in history) {
  history.scrollRestoration = 'manual';
}

// Save scroll position before navigation
function saveScrollPosition() {
  sessionStorage.setItem(`scroll-${location.pathname}`, window.scrollY);
}

// Restore on back/forward
window.addEventListener('popstate', () => {
  const saved = sessionStorage.getItem(`scroll-${location.pathname}`);
  if (saved) {
    requestAnimationFrame(() => window.scrollTo(0, parseInt(saved)));
  }
});

9. 触摸与交互 [MEDIUM]

9.1 使用 Touch-Action 控制滚动

使用 touch-action 控制交互元素的手势行为。

/* Allow only vertical scrolling (disable horizontal pan and pinch-zoom) */
.vertical-scroll {
  touch-action: pan-y;
}

/* Carousel: horizontal scroll only */
.carousel {
  touch-action: pan-x;
}

/* Canvas/map: disable all browser gestures */
.canvas {
  touch-action: none;
}
9.2 点击高亮

控制移动端 WebKit 浏览器中的点击高亮。

button, a {
  -webkit-tap-highlight-color: transparent;
}
9.3 悬停与焦点保持一致

每种悬停交互都必须支持键盘焦点。

/* Always pair :hover with :focus-visible */
.card:hover,
.card:focus-visible {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  transform: translateY(-2px);
}
9.4 不要只支持悬停的交互

切勿将必要功能隐藏在悬停交互后。触摸设备没有悬停状态。

/* Bad: content only accessible on hover */
/* .tooltip { display: none; }
   .trigger:hover .tooltip { display: block; } */

/* Good: works with focus and click too */
.trigger:hover .tooltip,
.trigger:focus-within .tooltip,
.tooltip:focus-within {
  display: block;
}
9.5 为轮播使用滚动捕捉

为卡片轮播和水平列表使用 CSS 滚动捕捉。

.carousel {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  gap: 1rem;
  scroll-padding: 1rem;
}

.carousel > .slide {
  scroll-snap-align: start;
  flex: 0 0 min(85%, 400px);
}

10. 国际化 [MEDIUM]

10.1 dir 和 lang 属性

在 <html> 元素上设置 lang。对用户生成的内容使用 dir="auto"。

<html lang="en" dir="ltr">


<p dir="auto">User-submitted text here</p>


<blockquote lang="ar" dir="rtl">...</blockquote>
10.2 使用 Intl API 进行格式化

使用 Intl API 根据区域设置进行格式化。切勿硬编码日期或数字格式。

// Dates
new Intl.DateTimeFormat('en-US', { dateStyle: 'long' }).format(date);
// "January 15, 2026"

// Numbers
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(1234.56);
// "1.234,56 EUR"

// Relative time
new Intl.RelativeTimeFormat('en', { numeric: 'auto' }).format(-1, 'day');
// "yesterday"

// Lists
new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }).format(['a', 'b', 'c']);
// "a, b, and c"

// Plurals
const pr = new Intl.PluralRules('en');
const suffixes = { one: 'st', two: 'nd', few: 'rd', other: 'th' };
function ordinal(n) { return `${n}${suffixes[pr.select(n)]}`; }
10.3 避免在图像中放置文本

图像中的文本无法翻译、调整大小,也无法被屏幕阅读器读取。需要带样式的文本叠加时,使用 HTML/CSS 文本和背景图像。

10.4 CSS 逻辑属性

使用逻辑属性而非物理属性,以同时支持 LTR 和 RTL 布局。

/* Physical (breaks in RTL) */
/* margin-left: 1rem; padding-right: 2rem; border-left: 1px solid; */

/* Logical (works in LTR and RTL) */
.sidebar {
  margin-inline-start: 1rem;
  padding-inline-end: 2rem;
  border-inline-start: 1px solid var(--color-border);
}

.stack > * + * {
  margin-block-start: 1rem;
}

/* Logical shorthands */
.box {
  margin-inline: auto;     /* left + right */
  padding-block: 2rem;     /* top + bottom */
  inset-inline-start: 0;   /* left in LTR, right in RTL */
  border-start-start-radius: 8px; /* top-left in LTR, top-right in RTL */
}
物理属性逻辑属性
left / rightinline-start / inline-end
top / bottomblock-start / block-end
margin-leftmargin-inline-start
padding-rightpadding-inline-end
border-top-left-radiusborder-start-start-radius
widthinline-size
heightblock-size
text-align: lefttext-align: start
10.5 RTL 布局支持

在 RTL 模式下测试布局。搭配逻辑属性时,Flexbox 和 Grid 会自动处理 RTL。

/* This layout works in both LTR and RTL without changes */
.layout {
  display: flex;
  gap: 1rem;
}

/* Icons that indicate direction need flipping */
[dir="rtl"] .arrow-icon {
  transform: scaleX(-1);
}

11. 渐进式 Web 应用 [MEDIUM]

PWA 可安装并离线运行。构建可安装的 Web 应用时,以下规则可确保体验一致且可靠。

11.1 提供完整的 Web App Manifest

在 <head> 中引入 manifest.json,并填写可安装所需的所有字段。缺少字段会导致安装提示无法显示,且不会给出提示。

<link rel="manifest" href="/manifest.json">
{
  "name": "My App",
  "short_name": "App",
  "start_url": "/",
  "display": "standalone",
  "icons": [
    { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
  ]
}

不正确:

{
  "name": "My App"
  // Missing start_url, display, and icons — app is not installable
}
11.2 设置 theme_color 和 background_color

theme_color 会为浏览器界面和操作系统任务切换器着色。background_color 会在应用加载前填充启动画面背景。两者都必须与品牌颜色一致。

{
  "theme_color": "#1a73e8",
  "background_color": "#ffffff"
}
11.3 注册 Service Worker 以支持离线

安装和离线功能都需要 Service Worker。安装时缓存关键资源;离线时从缓存响应。

// In your main entry point
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js');
}
// sw.js — cache on install, serve from cache when offline
const CACHE = 'v1';
const PRECACHE = ['/', '/index.html', '/app.js', '/app.css'];

self.addEventListener('install', e =>
  e.waitUntil(caches.open(CACHE).then(c => c.addAll(PRECACHE)))
);

self.addEventListener('fetch', e =>
  e.respondWith(
    caches.match(e.request).then(hit => hit ?? fetch(e.request))
  )
);
11.4 满足可安装条件

要显示浏览器安装提示:应用必须通过 HTTPS 提供,已注册带有 fetch 处理器的 Service Worker,并包含具有 name、icons、start_url 和 display: standalone(或 fullscreen/minimal-ui)的 manifest。

11.5 适当使用 display 模式
值适用场景
standalone应用替代浏览器 UI;最常见的选择
fullscreen需要占据整个屏幕的游戏或媒体应用
minimal-ui保留最少的浏览器控件(后退、重新加载)
browser不涉及安装行为;在浏览器标签页中打开

评估清单

构建或审查 Web 界面时,请使用此清单。

无障碍
  • 所有图像都有合适的 alt 文本
  • 颜色对比度达到 4.5:1(文本)和 3:1(UI 组件)
  • 所有交互元素都可通过键盘访问
  • 焦点指示器清晰可见(对比度为 3:1,周长至少为 2px)
  • 提供跳过导航链接
  • 表单输入框都有关联的标签
  • 错误消息与对应的输入框相关联
  • 动态内容更新使用 ARIA live 区域
  • 内容每秒闪烁不超过 3 次
  • 页面具有恰当的标题层级(h1-h6,不跳级)
  • 正确使用地标(main、nav、header、footer)
响应式
  • 在宽度为 320px 时不会出现水平滚动
  • 触控目标至少为 44x44px
  • 存在视口 meta 标签(不含 user-scalable=no)
  • 布局适用于手机、平板和桌面设备
  • 手机上无需缩放即可清晰阅读文本
表单
  • 所有输入框都有可见标签
  • 为常用字段设置 autocomplete 属性
  • 正确的输入类型会触发正确的移动键盘
  • 错误消息清晰且具体
  • 标明必填字段
  • 提交按钮未禁用
性能
  • 首屏以下的图像使用 loading="lazy"
  • 图像明确设置 width 和 height
  • 预加载关键字体
  • 第三方源使用 preconnect
  • 将大型 JS 包拆分为多个代码块
动效与主题
  • 遵循 prefers-reduced-motion 设置
  • 动画仅使用 transform 和 opacity
  • 暗色模式保持对比度
  • 存在 color-scheme meta 标签
  • 主题使用 CSS 自定义属性
  • prefers-contrast: more 会提高文本和边框的对比度
  • prefers-contrast: forced 使用系统颜色关键字
国际化
  • <html> 上设置 lang 属性
  • 使用 CSS 逻辑属性(而非物理方向属性)
  • 使用 Intl API 格式化日期和数字
  • 图像中不嵌入文本
  • 在 RTL 模式下测试布局
渐进式 Web 应用
  • 在 <head> 中链接 Web App Manifest,并包含 name、icons、start_url 和 display
  • theme_color 和 background_color 与品牌配色一致
  • 注册带有 fetch 处理程序的 Service Worker,以支持离线使用
  • 应用通过 HTTPS 提供服务

常见反模式

反模式修正方法
<div onclick="...">使用 <button>
没有替代样式的 outline: none使用 :focus-visible 并自定义轮廓
将 placeholder 用作标签添加 <label> 元素
tabindex="5"使用 tabindex="0" 或自然顺序
user-scalable=no移除它
font-size: 12px使用 font-size: 0.75rem
为 width/height/top/left 添加动画为 transform 和 opacity 添加动画
禁用提交按钮在提交时验证并显示错误
仅用颜色表示状态添加图标、文本或图案
margin-left / padding-right使用 margin-inline-start / padding-inline-end
<img> 未设置尺寸添加 width 和 height 属性
仅通过悬停显示内容添加 :focus-within 和点击处理程序

评论

支持 Markdown