wave-contrast-audit

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

WAVE Contrast Audit

WAVE 对比度检测

Systematically find and fix every color contrast violation on a live page using axe-core for precise data, then trace failures back to CSS source rules.
使用axe-core获取精准数据,系统性地查找并修复在线页面上的所有颜色对比度不符合项,然后追溯问题至CSS源规则。

When to Use This Skill

何时使用该技能

  • A WAVE report shows contrast errors on a deployed page
  • User shares a WAVE report URL (
    wave.webaim.org/report#/...
    )
  • Asked to "fix contrast issues" or "make the site WCAG AA compliant"
  • After a design system token change that may have introduced failures
  • 已部署页面的WAVE报告显示对比度错误
  • 用户分享WAVE报告链接(
    wave.webaim.org/report#/...
  • 被要求“修复对比度问题”或“使网站符合WCAG AA标准”
  • 设计系统令牌变更后可能引入不符合项时

Core Workflow

核心工作流程

Step 1 — Navigate to the live page (not the WAVE report)

步骤1 — 导航至在线页面(而非WAVE报告页)

Always run axe-core on the actual page, not the WAVE report wrapper.
Navigate to: https://example.com/page.html
始终在实际页面上运行axe-core,而非WAVE报告的包装页面。
Navigate to: https://example.com/page.html

Step 2 — Inject and run axe-core for exact color pairs

步骤2 — 注入并运行axe-core以获取精准颜色配对

This gives you precise foreground/background hex values, contrast ratios, CSS selectors, and font sizes — far more actionable than clicking WAVE icons one by one.
javascript
async () => {
  const script = document.createElement('script');
  script.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.9.1/axe.min.js';
  document.head.appendChild(script);
  await new Promise(r => script.onload = r);

  const results = await axe.run({ runOnly: ['color-contrast'] });

  // Deduplicate by unique fg+bg pair
  const pairs = {};
  for (const v of results.violations) {
    for (const node of v.nodes) {
      for (const rel of node.any) {
        if (rel.data?.fgColor) {
          const key = `${rel.data.fgColor}__on__${rel.data.bgColor}`;
          if (!pairs[key]) {
            pairs[key] = {
              fg: rel.data.fgColor,
              bg: rel.data.bgColor,
              ratio: rel.data.contrastRatio,
              fontSize: rel.data.fontSize,
              fontWeight: rel.data.fontWeight,
              required: rel.data.expectedContrastRatio,
              count: 0,
              selector: node.target[0]
            };
          }
          pairs[key].count++;
        }
      }
    }
  }
  return {
    total: results.violations.reduce((a, v) => a + v.nodes.length, 0),
    pairs: Object.values(pairs).sort((a, b) => a.ratio - b.ratio)
  };
}
这能为你提供精准的前景/背景十六进制值、对比度比值、CSS选择器和字体大小——比逐个点击WAVE图标更具可操作性。
javascript
async () => {
  const script = document.createElement('script');
  script.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.9.1/axe.min.js';
  document.head.appendChild(script);
  await new Promise(r => script.onload = r);

  const results = await axe.run({ runOnly: ['color-contrast'] });

  // Deduplicate by unique fg+bg pair
  const pairs = {};
  for (const v of results.violations) {
    for (const node of v.nodes) {
      for (const rel of node.any) {
        if (rel.data?.fgColor) {
          const key = `${rel.data.fgColor}__on__${rel.data.bgColor}`;
          if (!pairs[key]) {
            pairs[key] = {
              fg: rel.data.fgColor,
              bg: rel.data.bgColor,
              ratio: rel.data.contrastRatio,
              fontSize: rel.data.fontSize,
              fontWeight: rel.data.fontWeight,
              required: rel.data.expectedContrastRatio,
              count: 0,
              selector: node.target[0]
            };
          }
          pairs[key].count++;
        }
      }
    }
  }
  return {
    total: results.violations.reduce((a, v) => a + v.nodes.length, 0),
    pairs: Object.values(pairs).sort((a, b) => a.ratio - b.ratio)
  };
}

Step 3 — Interpret the results

步骤3 — 解读结果

For each unique pair, note:
  • fg + bg — the actual rendered hex values (rgba already resolved to hex)
  • ratio — current contrast (e.g. 3.1:1)
  • required — 4.5 (normal text) or 3.0 (large text >= 18px / >= 14px bold)
  • selector — CSS selector pointing to the failing element
  • count — how many elements share this exact pair
对于每一组唯一的颜色配对,记录:
  • 前景色 + 背景色 — 实际渲染的十六进制值(rgba已转换为十六进制)
  • 对比度比值 — 当前对比度(如3.1:1)
  • 要求比值 — 4.5(常规文本)或3.0(大文本 >=18px / >=14px粗体)
  • 选择器 — 指向不符合元素的CSS选择器
  • 计数 — 有多少元素使用该颜色配对

Step 4 — Find the CSS source rules

步骤4 — 查找CSS源规则

Use
Grep
to locate every CSS rule producing the failing color:
bash
undefined
使用
Grep
定位所有导致颜色不符合的CSS规则:
bash
undefined

Search for the failing value (rgba or hex)

Search for the failing value (rgba or hex)

grep -n "rgba(255,255,255,.45)|#7a7878|var(--steel)" src/styles.css
grep -n "rgba(255,255,255,.45)|#7a7878|var(--steel)" src/styles.css

Or search by selector

Or search by selector

grep -n ".sb-num|.btn-water" src/styles.css

Read the surrounding context to confirm background color and element role.
grep -n ".sb-num|.btn-water" src/styles.css

阅读上下文以确认背景颜色和元素角色。

Step 5 — Calculate compliant replacement values

步骤5 — 计算合规的替代值

For white text on dark backgrounds (
rgba(255,255,255, A)
):
RequiredMin alpha on
#022b45
Min alpha on
#034f7d
Min alpha on
#7a4f99
4.5:1>= 0.75 (use 0.82)>= 0.75 (use 0.82)>= 0.85 (use 0.88)
3.0:1>= 0.50 (use 0.60)>= 0.52 (use 0.60)>= 0.60 (use 0.68)
For brand blue on light backgrounds:
  • #1499e8
    on white = 3.1:1 (fail) — use
    #0d77c4
    for button bg (4.72:1)
  • #1499e8
    as text on white — use
    #0d77c4
For brand blue as text on dark backgrounds:
  • #1499e8
    on
    #022b45
    = 2.8:1 (fail) — use
    #8accf4
    (~5.0:1)
For muted gray on white/cream:
  • #7a7878
    on white = 4.4:1 (fail) — darken to
    #686666
    (5.7:1)
  • #7a7878
    on
    #ededed
    = 3.7:1 (fail) —
    #686666
    also passes cream (4.9:1)
Manual luminance calculation (when needed):
Contrast = (L_lighter + 0.05) / (L_darker + 0.05)
L = 0.2126 * R_lin + 0.7152 * G_lin + 0.0722 * B_lin
R_lin = ((R/255 + 0.055) / 1.055)^2.4   for values > 0.04045
R_lin = (R/255) / 12.92                  for values <= 0.04045
深色背景上的白色文本(
rgba(255,255,255, A)
):
要求比值
#022b45
上的最小透明度
#034f7d
上的最小透明度
#7a4f99
上的最小透明度
4.5:1>= 0.75(建议使用0.82>= 0.75(建议使用0.82>= 0.85(建议使用0.88
3.0:1>= 0.50(建议使用0.60>= 0.52(建议使用0.60>= 0.60(建议使用0.68
浅色背景上的品牌蓝色:
  • 白色背景上的
    #1499e8
    = 3.1:1(不符合)—— 按钮背景使用
    #0d77c4
    (4.72:1)
  • 白色背景上的
    #1499e8
    文本 —— 使用
    #0d77c4
深色背景上的品牌蓝色文本:
  • #022b45
    上的
    #1499e8
    = 2.8:1(不符合)—— 使用
    #8accf4
    (约5.0:1)
白色/奶油色背景上的柔和灰色:
  • 白色背景上的
    #7a7878
    = 4.4:1(不符合)—— 加深至
    #686666
    (5.7:1)
  • #ededed
    背景上的
    #7a7878
    = 3.7:1(不符合)——
    #686666
    在奶油色背景上也符合要求(4.9:1)
手动亮度计算(必要时):
Contrast = (L_lighter + 0.05) / (L_darker + 0.05)
L = 0.2126 * R_lin + 0.7152 * G_lin + 0.0722 * B_lin
R_lin = ((R/255 + 0.055) / 1.055)^2.4   for values > 0.04045
R_lin = (R/255) / 12.92                  for values <= 0.04045

Step 6 — Apply fixes efficiently

步骤6 — 高效应用修复

Batch all changes in a single Python pass to avoid making 50+ Edit calls:
python
with open('styles.css', 'r') as f:
    css = f.read()

changes = [
    ('--steel: #7a7878',         '--steel: #686666'),
    ('.btn-water { background: var(--vibrant-blue)',
     '.btn-water { background: #0d77c4'),
    # Add targeted string replacements for each failing rule...
]

results = []
for old, new in changes:
    if old in css:
        css = css.replace(old, new)
        results.append(f"  changed: {old[:50]}")
    else:
        results.append(f"  NOT FOUND: {old[:50]}")

with open('styles.css', 'w') as f:
    f.write(css)

print('\n'.join(results))
Warning:
rgba(255,255,255,.X)
appears in backgrounds, borders, and box-shadows — not only text color. Always read context before replacing. Use targeted string matches (include surrounding lines) rather than broad single-value replacements.
在单次Python处理中批量完成所有更改,避免进行50多次编辑操作:
python
with open('styles.css', 'r') as f:
    css = f.read()

changes = [
    ('--steel: #7a7878',         '--steel: #686666'),
    ('.btn-water { background: var(--vibrant-blue)',
     '.btn-water { background: #0d77c4'),
    # Add targeted string replacements for each failing rule...
]

results = []
for old, new in changes:
    if old in css:
        css = css.replace(old, new)
        results.append(f"  changed: {old[:50]}")
    else:
        results.append(f"  NOT FOUND: {old[:50]}")

with open('styles.css', 'w') as f:
    f.write(css)

print('\n'.join(results))
警告:
rgba(255,255,255,.X)
会出现在背景、边框和盒阴影中——不仅是文本颜色。替换前务必阅读上下文。使用针对性的字符串匹配(包含周围代码行)而非宽泛的单值替换。

Step 7 — Verify fixes

步骤7 — 验证修复效果

Re-run the Step 2 script on the updated page. Target: 0 color-contrast violations.
If the file is only local (not yet deployed), navigate Playwright directly:
Navigate to: file:///path/to/page.html

在更新后的页面上重新运行步骤2的脚本。目标:0项颜色对比度不符合项
如果文件仅在本地(尚未部署),直接通过Playwright导航:
Navigate to: file:///path/to/page.html

Common Failure Patterns

常见不符合模式

PatternFailing valueFix
Sidebar nav links on dark bg
rgba(255,255,255,.5-.62)
Raise to
.82
Section labels on dark bg
rgba(255,255,255,.18-.25)
Raise to
.75
Body text on dark bg
rgba(255,255,255,.52-.62)
Raise to
.85
Small labels/captions on dark bg
rgba(255,255,255,.38-.45)
Raise to
.82
Any text on purple bg (#7a4f99)alpha < .85Raise to
.88
CTA button (white on #1499e8)3.1:1Change bg to
#0d77c4
Muted gray on white
#7a7878
Darken to
#686666
Brand blue as text on white
#1499e8
Change to
#0d77c4
Brand blue as text on dark
#1499e8
Change to
#8accf4
Brand palette 25% tint on color bge.g.
#ded3e6
on purple
Use
var(--white)
模式不符合值修复方案
深色背景上的侧边栏导航链接
rgba(255,255,255,.5-.62)
透明度提升至
.82
深色背景上的章节标签
rgba(255,255,255,.18-.25)
透明度提升至
.75
深色背景上的正文文本
rgba(255,255,255,.52-.62)
透明度提升至
.85
深色背景上的小标签/说明文字
rgba(255,255,255,.38-.45)
透明度提升至
.82
紫色背景(#7a4f99)上的任何文本透明度 < .85提升至
.88
CTA按钮(深色背景上的白色文本
#1499e8
3.1:1将背景改为
#0d77c4
白色背景上的柔和灰色
#7a7878
加深至
#686666
白色背景上的品牌蓝色文本
#1499e8
改为
#0d77c4
深色背景上的品牌蓝色文本
#1499e8
改为
#8accf4
彩色背景上的品牌调色板25%色调例如紫色背景上的
#ded3e6
使用
var(--white)

WCAG 2.2 AA Thresholds (Quick Reference)

WCAG 2.2 AA 阈值(快速参考)

Text sizeRequired ratio
Normal (< 18px regular, < 14px bold)4.5 : 1
Large (>= 18px regular, >= 14px bold)3.0 : 1
UI components and icons3.0 : 1
文本尺寸要求对比度比值
常规(<18px常规字体,<14px粗体)4.5 : 1
大尺寸(>=18px常规字体,>=14px粗体)3.0 : 1
UI组件和图标3.0 : 1

Limitations

局限性

  • axe-core skips elements where background is a CSS gradient,
    background-image
    , or
    filter
    — use WAVE's eyedropper tool for those manually
  • WAVE counts each text node individually; axe deduplicates by color pair — expect axe to report fewer total violations than WAVE (e.g. 53 axe vs 187 WAVE)
  • Hover and focus states are not evaluated by either tool — test manually
  • Decorative elements (watermarks, icon chevrons, spacers) should receive
    aria-hidden="true"
    rather than contrast fixes
  • axe-core会跳过背景为CSS渐变、
    background-image
    filter
    的元素——这些元素需手动使用WAVE的吸管工具检测
  • WAVE会逐个统计文本节点;axe会按颜色配对去重——axe报告的总不符合项数量会少于WAVE(例如axe报告53项 vs WAVE报告187项)
  • 悬停和聚焦状态不会被任一工具评估——需手动测试
  • 装饰性元素(水印、图标箭头、间隔符)应添加
    aria-hidden="true"
    而非修复对比度

Resources

资源