light-dark-mode
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseLight/Dark Mode Accessibility Skill
明暗模式无障碍技能
Canonical source:inexamples/LIGHT_DARK_MODE_ACCESSIBILITY_BEST_PRACTICES.mdThis skill is derived from that file. When in doubt, the example is authoritative.mgifford/ACCESSIBILITY.md
Apply these rules whenever implementing or reviewing colour theme support in
HTML, CSS, or JavaScript.
Only load this skill if the project supports light/dark mode or user theme switching.
标准来源:中的mgifford/ACCESSIBILITY.md本技能源自该文件。如有疑问,以示例内容为准。examples/LIGHT_DARK_MODE_ACCESSIBILITY_BEST_PRACTICES.md
在HTML、CSS或JavaScript中实现或审核色彩主题支持时,请遵循以下规则。
仅当项目支持明暗模式或用户主题切换时,才启用此技能。
Critical Rule: Focus Is Navigation, Not Selection
关键规则:焦点用于导航,而非选择
A theme must not change until the user explicitly activates an option.
Moving keyboard focus or pointer hover across theme choices MUST NOT preview,
select, or apply a theme. Tabbing across System, Light, and Dark MUST leave the
page colours unchanged until the user presses Enter or Space.
This is a Critical requirement. Violations cause colour flashing during
keyboard navigation and disorient users with vestibular disorders, cognitive
disabilities, and low vision.
主题必须在用户明确激活选项后才会更改。
通过键盘移动焦点或指针悬停在主题选项上时,绝对不能预览、选择或应用主题。在“系统”“浅色”“深色”选项间切换Tab键时,页面颜色必须保持不变,直到用户按下Enter或Space键。
这是一项关键要求。违反此要求会导致键盘导航时出现颜色闪烁,使患有前庭障碍、认知障碍和低视力的用户感到困惑。
Core Mandate
核心要求
All colour themes must meet WCAG 2.2 Level AA contrast in both light and
dark modes, including forced-colours / high contrast modes. Test all three —
not just the default.
所有色彩主题在明暗两种模式下,包括强制色彩/高对比度模式,都必须满足WCAG 2.2 AA级对比度要求。请测试所有三种模式——而不仅仅是默认模式。
Required: Three-Option Theme Selector Pattern
必备:三选项主题选择器模式
Use a visible three-option selector that shows all three options at the same time:
- System
- Light
- Dark
使用可见的三选项选择器,同时显示所有三个选项:
- 系统
- 浅色
- 深色
Interaction pattern requirements
交互模式要求
- MUST use a labelled group containing three native elements
<button type="button"> - MUST use for the selected option and
aria-pressed="true"for othersaria-pressed="false" - MUST NOT use a single cycling button
- MUST NOT use a two-state light/dark toggle
- MUST NOT use a menu that hides the available options
- MUST NOT use custom elements
role="radio" - MUST NOT use a radiogroup that changes theme when arrow-key focus moves
- MUST NOT use hover or focus previews
- MUST NOT automatically select when a user merely tabs to an option
- 必须使用包含三个原生元素的带标签组
<button type="button"> - 选中的选项必须使用,其他选项使用
aria-pressed="true"aria-pressed="false" - 不得使用单一循环按钮
- 不得使用两态明暗切换开关
- 不得使用隐藏可用选项的菜单
- 不得使用自定义元素
role="radio" - 不得使用箭头键移动焦点时就更改主题的单选组
- 不得使用悬停或焦点预览
- 用户仅切换到某个选项时,不得自动选择该选项
Keyboard requirements
键盘操作要求
The selector MUST work with standard native button behaviour. Users MUST be able to:
- Tab to each option
- Use Shift+Tab to move backwards
- Activate an option with Enter
- Activate an option with Space
- Activate an option with mouse, touch, switch device, voice control, or other pointer
Moving focus between the options MUST NOT change the theme. The theme MUST
change only after deliberate activation.
Do NOT add unnecessary custom keyboard handlers where native button behaviour
already provides the required functionality.
选择器必须支持标准原生按钮行为。用户必须能够:
- 通过Tab键切换到每个选项
- 使用Shift+Tab键向后移动
- 使用Enter键激活选项
- 使用Space键激活选项
- 使用鼠标、触摸、切换设备、语音控制或其他指针设备激活选项
在选项间移动焦点时不得更改主题。主题必须仅在用户刻意激活后才更改。
在原生按钮行为已提供所需功能的情况下,请勿添加不必要的自定义键盘处理程序。
Prevent colour flashing during keyboard navigation
防止键盘导航时出现颜色闪烁
Theme changes MUST occur only after explicit activation. Moving keyboard focus
or pointer hover across theme choices MUST NOT preview, select, or apply a theme.
Tabbing across System, Light, and Dark MUST leave the page colours unchanged
until the user presses Enter or Space.
主题必须仅在用户明确激活后才更改。通过键盘移动焦点或指针悬停在主题选项上时,绝对不能预览、选择或应用主题。
在“系统”“浅色”“深色”选项间切换Tab键时,页面颜色必须保持不变,直到用户按下Enter或Space键。
Required: HTML Structure
必备:HTML结构
Use a group with an accessible label containing three native buttons:
html
<div role="group" aria-label="Colour theme">
<button
type="button"
class="theme-mode-btn"
aria-pressed="false"
data-theme-value="system">
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="20" height="20">
<path fill="none" stroke="currentColor" stroke-width="2" d="M3 4h18v12H3zM8 20h8"/>
</svg>
<span>System</span>
</button>
<button
type="button"
class="theme-mode-btn"
aria-pressed="false"
data-theme-value="light">
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="20" height="20">
<circle cx="12" cy="12" r="5" fill="currentColor"/>
</svg>
<span>Light</span>
</button>
<button
type="button"
class="theme-mode-btn"
aria-pressed="true"
data-theme-value="dark">
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="20" height="20">
<path fill="currentColor" d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
</svg>
<span>Dark</span>
</button>
</div>使用带有无障碍标签的组,包含三个原生按钮:
html
<div role="group" aria-label="Colour theme">
<button
type="button"
class="theme-mode-btn"
aria-pressed="false"
data-theme-value="system">
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="20" height="20">
<path fill="none" stroke="currentColor" stroke-width="2" d="M3 4h18v12H3zM8 20h8"/>
</svg>
<span>System</span>
</button>
<button
type="button"
class="theme-mode-btn"
aria-pressed="false"
data-theme-value="light">
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="20" height="20">
<circle cx="12" cy="12" r="5" fill="currentColor"/>
</svg>
<span>Light</span>
</button>
<button
type="button"
class="theme-mode-btn"
aria-pressed="true"
data-theme-value="dark">
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="20" height="20">
<path fill="currentColor" d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
</svg>
<span>Dark</span>
</button>
</div>Required: Default Behaviour
必备:默认行为
Use as the default preference when the user has not made an explicit choice.
systemDo NOT convert the system preference into a stored light or dark preference.
For example, when the user selects system while their operating system is
currently dark, store:
systemDo NOT store:
darkThis distinction is required so that later operating-system changes continue to
affect the page.
当用户未做出明确选择时,将作为默认偏好设置。
system请勿将系统偏好转换为存储的浅色或深色偏好。
例如,当用户选择系统模式,而其操作系统当前为深色模式时,存储的值应为:
system请勿存储:
dark此区分是必要的,以便后续操作系统的更改仍能影响页面显示。
Required: Storage Requirements
必备:存储要求
Use a clearly named storage key such as . Valid stored values are
only: , , .
theme-modesystemlightdark- Validate stored values before using them
- Wrap all reads and writes in
localStoragetry/catch - The theme control MUST continue to work for the current page when storage is unavailable
- Do NOT allow a storage failure to stop script execution
Storage can be unavailable in:
- Privacy-restricted browsers
- Sandboxed contexts
- Private browsing modes
- Environments where storage access throws an exception
使用命名清晰的存储键,例如。有效的存储值仅为:、、。
theme-modesystemlightdark- 使用存储值前先验证其有效性
- 将所有的读写操作包裹在
localStorage中try/catch - 当存储不可用时,主题控件仍必须能在当前页面正常工作
- 请勿因存储失败而停止脚本执行
存储不可用的场景包括:
- 隐私受限的浏览器
- 沙箱环境
- 隐私浏览模式
- 存储访问会抛出异常的环境
Required: Theme Application Model
必备:主题应用模型
Maintain separate values for the selected mode and resolved theme:
javascript
selectedMode = "system" | "light" | "dark";
resolvedTheme = "light" | "dark";Apply both to the root element using explicit attributes:
html
<html data-theme-mode="system" data-theme="dark">- represents the user's actual selection
data-theme-mode - represents the currently rendered light or dark scheme
data-theme
When system is active, listen for changes to:
javascript
window.matchMedia("(prefers-color-scheme: dark)")Update the resolved theme when that preference changes. Do NOT overwrite the
stored mode when the system preference changes.
分别维护所选模式和解析后的主题两个值:
javascript
selectedMode = "system" | "light" | "dark";
resolvedTheme = "light" | "dark";通过显式属性将两者应用到根元素:
html
<html data-theme-mode="system" data-theme="dark">- 表示用户的实际选择
data-theme-mode - 表示当前渲染的浅色或深色方案
data-theme
当系统模式激活时,监听以下对象的变化:
javascript
window.matchMedia("(prefers-color-scheme: dark)")当该偏好设置更改时,更新解析后的主题。系统偏好更改时,请勿覆盖存储的模式。
Required: Initial Rendering and Flash Prevention
必备:初始渲染与防闪烁
Provide an early initialization pattern that runs before the primary stylesheet
or before the page is visibly rendered. The pattern MUST:
- Safely read the stored preference
- Validate the stored value
- Default to system
- Resolve system using
prefers-color-scheme - Set the root theme attributes before the first significant paint
- Set an appropriate value
color-scheme
Include early in :
<head>html
<meta name="color-scheme" content="light dark">Explain that this early script is intended to reduce a flash of the incorrect
colour scheme during page load. Do NOT claim that a script can eliminate every
possible visual transition in every browser. Describe it as reducing or
preventing the common flash caused by applying the preference too late.
提供一种提前初始化的模式,使其在主样式表加载前或页面可见渲染前运行。该模式必须:
- 安全读取存储的偏好设置
- 验证存储值
- 默认使用系统模式
- 使用解析系统模式
prefers-color-scheme - 在首次重要绘制前设置根主题属性
- 设置合适的值
color-scheme
在中尽早添加:
<head>html
<meta name="color-scheme" content="light dark">说明此早期脚本旨在减少页面加载时出现的错误配色方案闪烁。请勿声称脚本可以在所有浏览器中消除所有可能的视觉过渡。应将其描述为减少或防止因偏好设置应用过晚而导致的常见闪烁问题。
Required: CSS Custom Properties Pattern
必备:CSS自定义属性模式
Use semantic theme tokens through CSS custom properties. Do NOT hard-code
component colours independently throughout the example.
css
:root {
color-scheme: light dark;
--color-text: #1a1a1a;
--color-background: #ffffff;
--color-surface: #f7f7f7;
--color-link: #0066cc;
--color-focus: #004499;
--color-border: #cccccc;
--color-hover: #f5f5f5;
}
@media (prefers-color-scheme: dark) {
:root {
--color-text: #e8e8e8;
--color-background: #121212;
--color-surface: #1b1b1b;
--color-link: #66aaff;
--color-focus: #9bd1ff;
--color-border: #444444;
--color-hover: #262626;
}
}
@supports (color: light-dark(#000, #fff)) {
:root {
--color-text: light-dark(#1a1a1a, #e8e8e8);
--color-background: light-dark(#ffffff, #121212);
--color-surface: light-dark(#f7f7f7, #1b1b1b);
--color-link: light-dark(#0066cc, #66aaff);
--color-focus: light-dark(#004499, #9bd1ff);
--color-border: light-dark(#cccccc, #444444);
--color-hover: light-dark(#f5f5f5, #262626);
}
}
/* Manual overrides narrow the active scheme when a user chooses one. */
[data-theme="light"] {
color-scheme: light;
}
[data-theme="dark"] {
color-scheme: dark;
}Component styles MUST read from the tokens above rather than hardcode colour values.
通过CSS自定义属性使用语义化主题令牌。请勿在示例中独立硬编码组件颜色。
css
:root {
color-scheme: light dark;
--color-text: #1a1a1a;
--color-background: #ffffff;
--color-surface: #f7f7f7;
--color-link: #0066cc;
--color-focus: #004499;
--color-border: #cccccc;
--color-hover: #f5f5f5;
}
@media (prefers-color-scheme: dark) {
:root {
--color-text: #e8e8e8;
--color-background: #121212;
--color-surface: #1b1b1b;
--color-link: #66aaff;
--color-focus: #9bd1ff;
--color-border: #444444;
--color-hover: #262626;
}
}
@supports (color: light-dark(#000, #fff)) {
:root {
--color-text: light-dark(#1a1a1a, #e8e8e8);
--color-background: light-dark(#ffffff, #121212);
--color-surface: light-dark(#f7f7f7, #1b1b1b);
--color-link: light-dark(#0066cc, #66aaff);
--color-focus: light-dark(#004499, #9bd1ff);
--color-border: light-dark(#cccccc, #444444);
--color-hover: light-dark(#f5f5f5, #262626);
}
}
/* 手动覆盖会在用户选择特定方案时缩小生效范围。 */
[data-theme="light"] {
color-scheme: light;
}
[data-theme="dark"] {
color-scheme: dark;
}组件样式必须从上述令牌中读取颜色值,而非硬编码颜色。
Required: Selected and Focus State Styling
必备:选中与焦点状态样式
The selected state MUST NOT be communicated through colour alone. Use multiple
cues such as:
aria-pressed- A visible checkmark or icon
- A thicker or otherwise distinct border
- Font-weight
- Text labels
Focus and selected state MUST be visually distinguishable from one another. A
focused but unselected option MUST NOT look identical to the selected option.
css
.theme-mode-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 2px solid var(--color-border);
border-radius: 0.375rem;
background: var(--color-surface);
color: var(--color-text);
font-size: 1rem;
cursor: pointer;
}
/* Focus indicator — visible outline, not box-shadow */
.theme-mode-btn:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
/* Selected state — distinct from focus */
.theme-mode-btn[aria-pressed="true"] {
border-color: var(--color-link);
font-weight: 600;
background: var(--color-background);
}Use with a real outline. Do NOT remove the outline unless it
is replaced by an equal or stronger visible focus indicator.
:focus-visible选中状态不得仅通过颜色来传达。请使用多种提示,例如:
- 属性
aria-pressed - 可见的勾选标记或图标
- 更粗或其他独特的边框
- 字体粗细
- 文本标签
焦点状态和选中状态必须在视觉上可区分。已聚焦但未选中的选项不得与选中选项外观相同。
css
.theme-mode-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 2px solid var(--color-border);
border-radius: 0.375rem;
background: var(--color-surface);
color: var(--color-text);
font-size: 1rem;
cursor: pointer;
}
/* 焦点指示器 — 使用可见轮廓,而非box-shadow */
.theme-mode-btn:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
/* 选中状态 — 与焦点状态区分开 */
.theme-mode-btn[aria-pressed="true"] {
border-color: var(--color-link);
font-weight: 600;
background: var(--color-background);
}使用并搭配真实的轮廓。除非用同等或更明显的可见焦点指示器替代,否则请勿移除轮廓。
:focus-visibleRequired: Forced-Colours Support
必备:强制色彩模式支持
The selector MUST remain usable in Windows High Contrast Mode and other
forced-colours environments. Do NOT rely only on:
- Background colours
- Gradients
- Box shadows
- Opacity
- SVG fill colours
- Colour differences
Use system colours where appropriate within .
@media (forced-colors: active)Ensure that:
- Button boundaries remain visible
- The selected option remains identifiable
- The focused option remains identifiable
- Selected and focused states remain distinct
- Icons using remain visible
currentColor
Do NOT use unless there is a specific, documented
reason and the resulting colours are tested.
forced-color-adjust: nonecss
@media (forced-colors: active) {
.theme-mode-btn {
border: 2px solid ButtonText;
color: ButtonText;
background: ButtonFace;
}
.theme-mode-btn:focus-visible {
outline: 2px solid Highlight;
outline-offset: 2px;
}
.theme-mode-btn[aria-pressed="true"] {
border-color: Highlight;
forced-color-adjust: none;
background: Highlight;
color: HighlightText;
}
}选择器必须在Windows高对比度模式和其他强制色彩环境中保持可用。请勿仅依赖:
- 背景色
- 渐变
- 盒阴影
- 透明度
- SVG填充色
- 颜色差异
在中,酌情使用系统颜色。
@media (forced-colors: active)确保:
- 按钮边界保持可见
- 选中选项可识别
- 聚焦选项可识别
- 选中状态和聚焦状态保持区分
- 使用的图标保持可见
currentColor
除非有特定的文档化理由,且结果颜色经过测试,否则请勿使用。
forced-color-adjust: nonecss
@media (forced-colors: active) {
.theme-mode-btn {
border: 2px solid ButtonText;
color: ButtonText;
background: ButtonFace;
}
.theme-mode-btn:focus-visible {
outline: 2px solid Highlight;
outline-offset: 2px;
}
.theme-mode-btn[aria-pressed="true"] {
border-color: Highlight;
forced-color-adjust: none;
background: Highlight;
color: HighlightText;
}
}Required: Accessible Naming and Status
必备:无障碍命名与状态
Use visible text labels: System, Light, Dark. Icons MAY supplement the labels
but MUST NOT replace them.
Decorative icons MUST use:
html
<svg aria-hidden="true" focusable="false">...</svg>Do NOT dynamically replace stable button labels with phrases such as:
- Switch to dark
- Current dark mode
- Use system dark
The button label SHOULD describe the preference the button selects. The
selected state is communicated through .
aria-pressedIt is acceptable to include a separate status message explaining the resolved
system theme, for example:
html
<p class="visually-hidden" aria-live="polite" id="theme-status">
System preference currently uses dark mode.
</p>Do NOT make this announcement excessively verbose. Avoid announcing the same
state repeatedly when nothing has changed.
使用可见文本标签:系统、浅色、深色。图标可作为标签的补充,但不得替代标签。
装饰性图标必须使用:
html
<svg aria-hidden="true" focusable="false">...</svg>请勿将稳定的按钮标签动态替换为以下短语:
- 切换到深色模式
- 当前为深色模式
- 使用系统深色模式
按钮标签应描述该按钮选择的偏好设置。选中状态通过属性传达。
aria-pressed可以添加单独的状态消息来解释解析后的系统主题,例如:
html
<p class="visually-hidden" aria-live="polite" id="theme-status">
系统偏好当前使用深色模式。
</p>请勿使此提示过于冗长。当状态未发生变化时,避免重复提示相同内容。
Required: Reduced Motion
必备:减少动画
Do NOT use animated colour transitions by default. Theme changes can affect
most of the viewport and may be uncomfortable or disorienting.
If a transition example is included, it MUST:
- Be subtle
- Avoid transitioning every CSS property
- Respect
prefers-reduced-motion - Not delay the actual state change
- Not create intermediate low-contrast states
Prefer no theme-transition animation in the canonical example.
css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}默认情况下请勿使用颜色过渡动画。主题更改会影响大部分视口,可能会让用户感到不适或困惑。
如果包含过渡示例,必须:
- 效果柔和
- 避免过渡所有CSS属性
- 尊重设置
prefers-reduced-motion - 不延迟实际状态更改
- 不创建中间低对比度状态
在标准示例中,建议不使用主题过渡动画。
css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}Required: Contrast Requirements
必备:对比度要求
The skill MUST continue requiring WCAG 2.2 Level AA contrast testing in every
relevant state. Test at least:
- System resolving to light
- System resolving to dark
- Explicit Light
- Explicit Dark
- Forced colours
- Keyboard focus
- Selected option
- Hover
- Disabled controls (if any)
- Links
- Form fields
- Borders and graphical objects
Do NOT assume that colours which pass in light mode will pass in dark mode. Do
NOT present experimental CSS functions as substitutes for actual contrast testing.
本技能必须继续要求在所有相关状态下进行WCAG 2.2 AA级对比度测试。至少测试:
- 系统模式解析为浅色
- 系统模式解析为深色
- 显式浅色模式
- 显式深色模式
- 强制色彩模式
- 键盘焦点状态
- 选中选项
- 悬停状态
- 禁用控件(如有)
- 链接
- 表单字段
- 边框和图形对象
请勿假设在浅色模式下通过对比度测试的颜色在深色模式下也能通过。请勿将实验性CSS函数作为实际对比度测试的替代方案。
Required: Complete Example Implementation
必备:完整示例实现
Include a complete example containing:
- Accessible HTML
- CSS
- Early anti-flash initialization
- Main JavaScript
- Safe storage handling
- System-preference change handling
- Forced-colours styles
- Visible focus styles
- Selected-state styles
The example MUST be internally consistent. Use the same storage key, data
attributes, class names, mode names, and selected-state mechanism throughout
all code samples. Do NOT provide fragments that contradict one another.
包含完整示例,包括:
- 无障碍HTML
- CSS样式
- 提前防闪烁初始化
- 核心JavaScript逻辑
- 安全存储处理
- 系统偏好更改处理
- 强制色彩模式样式
- 可见焦点样式
- 选中状态样式
示例必须内部一致。在所有代码示例中使用相同的存储键、数据属性、类名、模式名称和选中状态机制。请勿提供相互矛盾的代码片段。
Complete HTML
完整HTML
html
<!DOCTYPE html>
<html lang="en" data-theme-mode="system" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<title>Theme Selector Example</title>
<style>
/* Anti-flash: set theme as early as possible */
html[data-theme="dark"] {
color-scheme: dark;
}
html[data-theme="light"] {
color-scheme: light;
}
</style>
<script>
/* Early initialization — runs before first paint */
(function() {
var stored = null;
try { stored = localStorage.getItem('theme-mode'); } catch (e) {}
var mode = (stored === 'light' || stored === 'dark') ? stored : 'system';
var resolved = mode === 'system'
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: mode;
document.documentElement.setAttribute('data-theme-mode', mode);
document.documentElement.setAttribute('data-theme', resolved);
})();
</script>
</head>
<body>
<header>
<nav aria-label="Main navigation">
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<div role="group" aria-label="Colour theme" id="theme-selector">
<button type="button" class="theme-mode-btn" aria-pressed="false" data-theme-value="system">
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="20" height="20">
<path fill="none" stroke="currentColor" stroke-width="2" d="M3 4h18v12H3zM8 20h8"/>
</svg>
<span>System</span>
</button>
<button type="button" class="theme-mode-btn" aria-pressed="false" data-theme-value="light">
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="20" height="20">
<circle cx="12" cy="12" r="5" fill="currentColor"/>
</svg>
<span>Light</span>
</button>
<button type="button" class="theme-mode-btn" aria-pressed="true" data-theme-value="dark">
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="20" height="20">
<path fill="currentColor" d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
</svg>
<span>Dark</span>
</button>
</div>
</header>
<main>
<h1>Theme Selector Example</h1>
<p>This page demonstrates a three-option theme selector with System, Light, and Dark modes.</p>
</main>
<p class="visually-hidden" aria-live="polite" id="theme-status"></p>
<script>
/* Main theme logic — runs after DOM ready */
(function() {
var STORAGE_KEY = 'theme-mode';
var VALID_MODES = ['system', 'light', 'dark'];
var buttons = document.querySelectorAll('.theme-mode-btn');
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
var statusEl = document.getElementById('theme-status');
var currentMode = 'system';
function getStoredMode() {
try {
var stored = localStorage.getItem(STORAGE_KEY);
return VALID_MODES.indexOf(stored) !== -1 ? stored : 'system';
} catch (e) {
return 'system';
}
}
function setStoredMode(mode) {
try {
localStorage.setItem(STORAGE_KEY, mode);
} catch (e) {
/* Storage unavailable; preference still works for current session. */
}
}
function resolveTheme(mode) {
if (mode === 'system') {
return prefersDark.matches ? 'dark' : 'light';
}
return mode;
}
function updateButtons(activeMode) {
for (var i = 0; i < buttons.length; i++) {
var btn = buttons[i];
var isActive = btn.getAttribute('data-theme-value') === activeMode;
btn.setAttribute('aria-pressed', isActive ? 'true' : 'false');
}
}
function applyMode(mode) {
currentMode = mode;
var resolved = resolveTheme(mode);
document.documentElement.setAttribute('data-theme-mode', mode);
document.documentElement.setAttribute('data-theme', resolved);
updateButtons(mode);
}
function handleActivation(event) {
var btn = event.currentTarget;
var mode = btn.getAttribute('data-theme-value');
setStoredMode(mode);
applyMode(mode);
}
/* Attach click handlers — no keyboard handlers needed */
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', handleActivation);
}
/* Listen for OS preference changes */
prefersDark.addEventListener('change', function() {
if (currentMode === 'system') {
applyMode('system');
}
});
/* Initialize */
applyMode(getStoredMode());
})();
</script>
</body>
</html>html
<!DOCTYPE html>
<html lang="en" data-theme-mode="system" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<title>Theme Selector Example</title>
<style>
/* 防闪烁:尽早设置主题 */
html[data-theme="dark"] {
color-scheme: dark;
}
html[data-theme="light"] {
color-scheme: light;
}
</style>
<script>
/* 提前初始化 — 在首次绘制前运行 */
(function() {
var stored = null;
try { stored = localStorage.getItem('theme-mode'); } catch (e) {}
var mode = (stored === 'light' || stored === 'dark') ? stored : 'system';
var resolved = mode === 'system'
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: mode;
document.documentElement.setAttribute('data-theme-mode', mode);
document.documentElement.setAttribute('data-theme', resolved);
})();
</script>
</head>
<body>
<header>
<nav aria-label="Main navigation">
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<div role="group" aria-label="Colour theme" id="theme-selector">
<button type="button" class="theme-mode-btn" aria-pressed="false" data-theme-value="system">
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="20" height="20">
<path fill="none" stroke="currentColor" stroke-width="2" d="M3 4h18v12H3zM8 20h8"/>
</svg>
<span>System</span>
</button>
<button type="button" class="theme-mode-btn" aria-pressed="false" data-theme-value="light">
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="20" height="20">
<circle cx="12" cy="12" r="5" fill="currentColor"/>
</svg>
<span>Light</span>
</button>
<button type="button" class="theme-mode-btn" aria-pressed="true" data-theme-value="dark">
<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="20" height="20">
<path fill="currentColor" d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
</svg>
<span>Dark</span>
</button>
</div>
</header>
<main>
<h1>Theme Selector Example</h1>
<p>This page demonstrates a three-option theme selector with System, Light, and Dark modes.</p>
</main>
<p class="visually-hidden" aria-live="polite" id="theme-status"></p>
<script>
/* 核心主题逻辑 — DOM加载完成后运行 */
(function() {
var STORAGE_KEY = 'theme-mode';
var VALID_MODES = ['system', 'light', 'dark'];
var buttons = document.querySelectorAll('.theme-mode-btn');
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
var statusEl = document.getElementById('theme-status');
var currentMode = 'system';
function getStoredMode() {
try {
var stored = localStorage.getItem(STORAGE_KEY);
return VALID_MODES.indexOf(stored) !== -1 ? stored : 'system';
} catch (e) {
return 'system';
}
}
function setStoredMode(mode) {
try {
localStorage.setItem(STORAGE_KEY, mode);
} catch (e) {
/* 存储不可用;偏好设置仍在当前会话生效。 */
}
}
function resolveTheme(mode) {
if (mode === 'system') {
return prefersDark.matches ? 'dark' : 'light';
}
return mode;
}
function updateButtons(activeMode) {
for (var i = 0; i < buttons.length; i++) {
var btn = buttons[i];
var isActive = btn.getAttribute('data-theme-value') === activeMode;
btn.setAttribute('aria-pressed', isActive ? 'true' : 'false');
}
}
function applyMode(mode) {
currentMode = mode;
var resolved = resolveTheme(mode);
document.documentElement.setAttribute('data-theme-mode', mode);
document.documentElement.setAttribute('data-theme', resolved);
updateButtons(mode);
}
function handleActivation(event) {
var btn = event.currentTarget;
var mode = btn.getAttribute('data-theme-value');
setStoredMode(mode);
applyMode(mode);
}
/* 添加点击处理程序 — 无需键盘处理程序 */
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', handleActivation);
}
/* 监听操作系统偏好更改 */
prefersDark.addEventListener('change', function() {
if (currentMode === 'system') {
applyMode('system');
}
});
/* 初始化 */
applyMode(getStoredMode());
})();
</script>
</body>
</html>Required: Failure Patterns
必备:禁用模式
The following patterns MUST be rejected:
以下模式必须被拒绝:
Binary toggle
二元切换开关
A single button that alternates only between light and dark and provides no way
to return to System.
仅在浅色和深色之间切换的单一按钮,无法返回系统模式。
Focus-triggered preview
焦点触发预览
JavaScript that changes the theme in a focus or arrow-key handler.
在焦点或箭头键处理程序中更改主题的JavaScript代码。
Incorrect radio implementation
错误的单选框实现
A radiogroup where arrow keys both move focus and immediately change the entire
page theme.
箭头键移动焦点时立即更改整个页面主题的单选组。
Incorrect persistence
错误的持久化方式
Storing when the user actually selected and the system happened
to be dark.
darksystem当用户实际选择了系统模式,而系统恰好为深色时,存储而非。
darksystemColour-only selection
仅通过颜色区分选中状态
Showing the selected theme only through a different background colour.
仅通过不同背景色显示选中主题。
Unsafe storage
不安全的存储方式
Using or without exception handling.
localStorage.getItem()setItem()未添加异常处理的或调用。
localStorage.getItem()setItem()Late initialization
延迟初始化
Waiting for before applying the stored theme, causing an
avoidable flash of the wrong theme.
DOMContentLoaded等待后再应用存储的主题,导致不必要的错误主题闪烁。
DOMContentLoadedDynamic label confusion
动态标签混淆
Changing the selected option's accessible name instead of keeping its label
stable and exposing selection with .
aria-pressed更改选中选项的无障碍名称,而非保持标签稳定并通过暴露选中状态。
aria-pressedSeverity Scale (this skill)
严重程度等级(本技能)
| Level | Meaning |
|---|---|
| Critical | Colour theme makes content or interaction completely inaccessible |
| Serious | Contrast or mode failure significantly impairs access for a disability group |
| Moderate | Theme degrades usability but content remains partially accessible |
| Minor | Best-practice gap; marginal impact |
| 等级 | 含义 |
|---|---|
| 关键 | 色彩主题导致内容或交互完全无法访问 |
| 严重 | 对比度或模式故障严重影响残障群体的访问 |
| 中等 | 主题降低可用性,但内容仍部分可访问 |
| 轻微 | 最佳实践缺失;影响极小 |
Validation and Testing
验证与测试
Before shipping, verify all of the following:
发布前,请验证以下所有内容:
Keyboard
键盘操作
- Load the page.
- Tab to the theme selector.
- Continue tabbing through System, Light, and Dark.
- Confirm that the page theme does NOT change as focus moves.
- Press Enter on an unselected option.
- Confirm that the theme changes once.
- Press Space on another option.
- Confirm that the theme changes once.
- Confirm that focus remains visible.
- Confirm that the selected state and focus state are distinguishable.
- 加载页面。
- 通过Tab键切换到主题选择器。
- 继续通过Tab键在系统、浅色、深色选项间切换。
- 确认页面主题不会随焦点移动而更改。
- 在未选中的选项上按Enter键。
- 确认主题仅更改一次。
- 在另一个选项上按Space键。
- 确认主题仅更改一次。
- 确认焦点保持可见。
- 确认选中状态和焦点状态可区分。
System mode
系统模式
- Select System.
- Confirm that system is stored.
- Change the operating-system colour preference.
- Confirm that the page updates without reloading.
- Confirm that the stored value remains system.
- 选择系统模式。
- 确认系统模式已存储。
- 更改操作系统的颜色偏好。
- 确认页面无需刷新即可更新。
- 确认存储的值仍为系统模式。
Persistence
持久化
- Select Light.
- Reload.
- Confirm that Light remains selected.
- Select Dark.
- Reload.
- Confirm that Dark remains selected.
- Select System.
- Reload.
- Confirm that System remains selected and resolves correctly.
- 选择浅色模式。
- 刷新页面。
- 确认浅色模式仍保持选中。
- 选择深色模式。
- 刷新页面。
- 确认深色模式仍保持选中。
- 选择系统模式。
- 刷新页面。
- 确认系统模式仍保持选中并正确解析。
Storage failure
存储失败
Test or simulate throwing an exception. Confirm that:
localStorage- The page still renders
- The selector still works
- The selected preference works for the current page
- No uncaught JavaScript error stops execution
测试或模拟抛出异常。确认:
localStorage- 页面仍能正常渲染
- 选择器仍能正常工作
- 选中的偏好设置在当前页面仍生效
- 未捕获的JavaScript错误不会停止执行
Assistive technology
辅助技术
Test with at least:
- One desktop screen reader
- Keyboard only
- Browser zoom at 200% and 400%
- Forced-colours or Windows High Contrast Mode
- Touch or mobile interaction where practical
Confirm that assistive technology announces:
- The group label
- Each button label
- Pressed or not pressed state
至少使用以下工具测试:
- 一款桌面屏幕阅读器
- 仅键盘操作
- 浏览器缩放至200%和400%
- 强制色彩模式或Windows高对比度模式
- 触摸或移动设备交互(如适用)
确认辅助技术会提示:
- 组标签
- 每个按钮的标签
- 已按下或未按下状态
Visual stability
视觉稳定性
Record or observe keyboard traversal through the selector. Confirm that no
colour scheme is previewed while tabbing.
录制或观察通过选择器的键盘遍历过程。确认切换Tab键时不会预览配色方案。
Automated tests
自动化测试
Inspect the repository's existing testing conventions and add tests where the
project supports them. Tests SHOULD verify, where practical:
- All three options exist
- All options are native buttons
- Only one option has
aria-pressed="true" - Focus alone does not change the theme
- Click activation changes the theme
- Keyboard activation changes the theme
- System tracks
prefers-color-scheme - An explicit Light or Dark selection ignores later system changes
- Invalid stored values fall back to System
- Storage exceptions do not crash the control
- Selected mode and resolved theme use separate root attributes
Do NOT introduce a new test framework unless necessary. Follow the repository's
existing approach.
检查仓库现有的测试约定,并在项目支持的情况下添加测试。测试应尽可能验证:
- 三个选项均存在
- 所有选项均为原生按钮
- 仅有一个选项的
aria-pressed="true" - 仅焦点移动不会更改主题
- 点击激活会更改主题
- 键盘激活会更改主题
- 系统模式跟踪
prefers-color-scheme - 显式选择浅色或深色模式后,后续系统更改不会生效
- 无效存储值会回退到系统模式
- 存储异常不会导致控件崩溃
- 所选模式和解析后的主题使用单独的根属性
除非必要,否则请勿引入新的测试框架。遵循仓库现有的测试方法。
Definition of Done Checklist
完成标准检查清单
- All text/UI elements meet WCAG 2.2 AA contrast in light mode
- All text/UI elements meet WCAG 2.2 AA contrast in dark mode
- declared on
color-scheme: light dark;:root - Theme tokens use with
light-dark()only as fallbackprefers-color-scheme - Forced-colours mode: content comprehensible; no meaning conveyed by shadow, gradient, or background alone
- Focus rings use , not
outlinealonebox-shadow - Information not conveyed by colour alone — icon + text + colour
- Focus indicators visible and meeting 3:1 in all modes
- Three-option selector with System, Light, and Dark visible simultaneously
- Selector uses native with
<button type="button">aria-pressed - Theme changes only on explicit activation (Enter, Space, click)
- Focus alone does NOT change the theme
- In mode, theme resolves from
systemand auto-updatesprefers-color-scheme - access wrapped in
localStoragetry/catch - Invalid stored values fall back to system
- User preference persists across sessions where is available
localStorage - respected for theme transitions
prefers-reduced-motion - SVGs use
currentColor - and
color-mix()gated behindcontrast-color()with explicit fallback values@supports - Browser support, forced-colours, high contrast, light mode, dark mode, and manual WCAG contrast are verified
- Keyboard user can tab across all options without changing page colours
- 所有文本/UI元素在浅色模式下满足WCAG 2.2 AA级对比度
- 所有文本/UI元素在深色模式下满足WCAG 2.2 AA级对比度
- 在上声明
:rootcolor-scheme: light dark; - 主题令牌使用,仅将
light-dark()作为回退prefers-color-scheme - 强制色彩模式:内容可理解;不会仅通过阴影、渐变或背景传达含义
- 焦点环使用,而非仅使用
outlinebox-shadow - 信息不会仅通过颜色传达 — 图标 + 文本 + 颜色
- 焦点指示器在所有模式下可见且满足3:1对比度
- 同时显示系统、浅色、深色三个选项的选择器
- 选择器使用带有的原生
aria-pressed<button type="button"> - 仅在明确激活(Enter、Space、点击)时更改主题
- 仅焦点移动不会更改主题
- 在系统模式下,主题从解析并自动更新
prefers-color-scheme - 访问包裹在
localStorage中try/catch - 无效存储值回退到系统模式
- 在可用时,用户偏好可跨会话持久化
localStorage - 主题过渡尊重设置
prefers-reduced-motion - SVG使用
currentColor - 和
color-mix()在contrast-color()中使用,并带有显式回退值@supports - 已验证浏览器支持、强制色彩模式、高对比度、浅色模式、深色模式和手动WCAG对比度
- 键盘用户可在所有选项间切换Tab键而不更改页面颜色
Key WCAG Criteria
关键WCAG标准
- 1.4.1 Use of Color (A)
- 1.4.3 Contrast Minimum (AA) — Serious if failing in either mode
- 1.4.11 Non-text Contrast (AA)
- 2.4.11 Focus Appearance (AA, WCAG 2.2)
- 1.4.1 颜色使用(A级)
- 1.4.3 最低对比度(AA级)—— 任一模式不满足则为严重问题
- 1.4.11 非文本对比度(AA级)
- 2.4.11 焦点外观(AA级,WCAG 2.2)
References
参考资料
- Full best practices guide
- Modern CSS Theme Architecture — complementary guidance on separating design decisions from implementation via semantic design tokens
- WCAG 2.2 Understanding 1.4.3 Contrast Minimum
- WCAG 2.2 Understanding 2.4.11 Focus Appearance
- MDN: light-dark()
- MDN: contrast-color()
- CSS prefers-color-scheme (Media Queries Level 5)
- CSS forced-colors (Media Queries Level 5)
- CSS System Colors (CSS Color Level 4)
- MDN: forced-color-adjust
- Baseline 2023: color-mix() browser support
Standards horizon: WCAG 3.0's proposed APCA (Advanced Perceptual Contrast Algorithm) replaces the current 4.5:1 / 3:1 luminance-ratio model with a perceptual contrast model that treats light-on-dark and dark-on-light differently. This will directly affect light/dark mode contrast targets. Do not apply APCA to production work until WCAG 3.0 is a published standard. Monitor: https://www.w3.org/TR/wcag-3.0/ and https://git.apcacontrast.com/
- 完整最佳实践指南
- 现代CSS主题架构 — 补充指导,通过语义化设计令牌分离设计决策与实现
- WCAG 2.2 理解1.4.3最低对比度
- WCAG 2.2 理解2.4.11焦点外观
- MDN: light-dark()
- MDN: contrast-color()
- CSS prefers-color-scheme(媒体查询第5级)
- CSS forced-colors(媒体查询第5级)
- CSS系统颜色(CSS颜色第4级)
- MDN: forced-color-adjust
- 基线2023: color-mix()浏览器支持
标准前沿:WCAG 3.0提议的APCA(高级感知对比度算法)将取代当前的4.5:1 / 3:1亮度比率模型,采用感知对比度模型,区分浅色背景深色文本和深色背景浅色文本的情况。这将直接影响明暗模式的对比度目标。 在WCAG 3.0成为发布标准前,请勿将APCA应用于生产工作。 关注:https://www.w3.org/TR/wcag-3.0/ 和 https://git.apcacontrast.com/