Skip to content

颜色对比度优化 - 完整视觉可访问性指南

1. 颜色对比度基础

1.1 什么是颜色对比度

颜色对比度是前景色和背景色之间的亮度差异比例,用于衡量文本的可读性。

typescript
// 对比度比率计算公式
// Contrast Ratio = (L1 + 0.05) / (L2 + 0.05)
// L1 = 较亮颜色的相对亮度
// L2 = 较暗颜色的相对亮度

function getRelativeLuminance(r: number, g: number, b: number): number {
  // 转换为0-1范围
  const [rs, gs, bs] = [r, g, b].map(val => {
    const s = val / 255;
    return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
  });
  
  return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}

function getContrastRatio(color1: string, color2: string): number {
  // 解析颜色为RGB
  const rgb1 = parseColor(color1);
  const rgb2 = parseColor(color2);
  
  const l1 = getRelativeLuminance(rgb1.r, rgb1.g, rgb1.b);
  const l2 = getRelativeLuminance(rgb2.r, rgb2.g, rgb2.b);
  
  const lighter = Math.max(l1, l2);
  const darker = Math.min(l1, l2);
  
  return (lighter + 0.05) / (darker + 0.05);
}

// 示例
const ratio = getContrastRatio('#000000', '#FFFFFF'); // 21:1 (最高对比度)

1.2 WCAG标准

typescript
const WCAGStandards = {
  AA: {
    normalText: {
      ratio: 4.5,
      description: '普通文本最低对比度',
      fontSize: '< 18pt或14pt加粗'
    },
    largeText: {
      ratio: 3,
      description: '大文本最低对比度',
      fontSize: '>= 18pt或14pt加粗'
    },
    uiComponents: {
      ratio: 3,
      description: 'UI组件和图形元素',
      examples: ['按钮边框', '表单边框', '图标']
    }
  },
  
  AAA: {
    normalText: {
      ratio: 7,
      description: '普通文本增强对比度',
      fontSize: '< 18pt或14pt加粗'
    },
    largeText: {
      ratio: 4.5,
      description: '大文本增强对比度',
      fontSize: '>= 18pt或14pt加粗'
    }
  }
};

// 检查是否符合WCAG标准
function meetsWCAG(ratio: number, level: 'AA' | 'AAA', isLargeText = false): boolean {
  const standards = WCAGStandards[level];
  const requirement = isLargeText ? standards.largeText.ratio : standards.normalText.ratio;
  return ratio >= requirement;
}

2. 颜色对比度检测

2.1 在线检测工具

typescript
const contrastCheckerTools = {
  WebAIM: {
    url: 'https://webaim.org/resources/contrastchecker/',
    features: ['实时检测', 'WCAG评级', '颜色调整建议']
  },
  
  Colorable: {
    url: 'https://colorable.jxnblk.com/',
    features: ['批量检测', '调色板生成', '可访问配色']
  },
  
  ContrastRatio: {
    url: 'https://contrast-ratio.com/',
    features: ['简洁界面', '实时预览', 'Lea Verou开发']
  },
  
  WhoCanUse: {
    url: 'https://www.whocanuse.com/',
    features: ['模拟视觉障碍', '不同光照条件', '年龄因素']
  }
};

2.2 浏览器开发者工具

typescript
// Chrome DevTools使用
const chromeDevTools = {
  step1: '打开DevTools (F12)',
  step2: '选择Elements面板',
  step3: '点击元素的color样式',
  step4: '查看对比度信息',
  step5: '使用颜色选择器调整至符合标准',
  
  features: [
    '自动显示对比度比率',
    '标记AA/AAA合规性',
    '建议符合标准的颜色',
    '实时预览效果'
  ]
};

2.3 自动化检测

typescript
// contrast-checker.ts
export function checkContrast(
  foreground: string,
  background: string,
  fontSize: number = 16,
  isBold: boolean = false
): {
  ratio: number;
  AA: boolean;
  AAA: boolean;
  isLargeText: boolean;
} {
  const ratio = getContrastRatio(foreground, background);
  
  // 18pt = 24px, 14pt = 18.66px
  const isLargeText = fontSize >= 24 || (fontSize >= 18.66 && isBold);
  
  const AA = meetsWCAG(ratio, 'AA', isLargeText);
  const AAA = meetsWCAG(ratio, 'AAA', isLargeText);
  
  return { ratio, AA, AAA, isLargeText };
}

// React Hook
export function useContrastCheck(
  foreground: string,
  background: string,
  fontSize: number = 16,
  isBold: boolean = false
) {
  return useMemo(
    () => checkContrast(foreground, background, fontSize, isBold),
    [foreground, background, fontSize, isBold]
  );
}

// 使用
function MyComponent() {
  const contrast = useContrastCheck('#666', '#fff', 16, false);
  
  useEffect(() => {
    if (!contrast.AA) {
      console.warn(
        `对比度不足: ${contrast.ratio.toFixed(2)}:1 (需要 >= 4.5:1)`
      );
    }
  }, [contrast]);
  
  return <div>...</div>;
}

3. 设计可访问的配色方案

3.1 建立主题色系

typescript
// theme.ts
export const accessibleTheme = {
  // 深色背景上的浅色文本
  dark: {
    background: '#1a1a1a',
    text: '#ffffff',          // 15.8:1 ✅ AAA
    textSecondary: '#b3b3b3', // 8.6:1 ✅ AAA
    textTertiary: '#808080',  // 4.6:1 ✅ AA
    
    primary: '#4da6ff',       // 6.1:1 ✅ AAA
    success: '#4caf50',       // 4.8:1 ✅ AA
    warning: '#ffc107',       // 10.1:1 ✅ AAA
    error: '#f44336',         // 4.8:1 ✅ AA
  },
  
  // 浅色背景上的深色文本
  light: {
    background: '#ffffff',
    text: '#1a1a1a',          // 15.8:1 ✅ AAA
    textSecondary: '#4d4d4d', // 8.6:1 ✅ AAA
    textTertiary: '#808080',  // 4.6:1 ✅ AA
    
    primary: '#0066cc',       // 7.7:1 ✅ AAA
    success: '#2e7d32',       // 6.4:1 ✅ AAA
    warning: '#f57c00',       // 5.9:1 ✅ AAA
    error: '#c62828',         // 7.3:1 ✅ AAA
  }
};

// 验证主题
function validateTheme(theme: typeof accessibleTheme.light) {
  const results: Record<string, any> = {};
  
  Object.entries(theme).forEach(([key, color]) => {
    if (key === 'background') return;
    
    const contrast = checkContrast(color, theme.background);
    results[key] = {
      color,
      ratio: contrast.ratio,
      AA: contrast.AA,
      AAA: contrast.AAA
    };
  });
  
  return results;
}

3.2 语义化颜色

typescript
// semantic-colors.ts
export const semanticColors = {
  // 成功 - 需要与背景有足够对比度
  success: {
    light: '#2e7d32',  // 深绿色用于浅背景
    dark: '#4caf50',   // 浅绿色用于深背景
    contrast: '#ffffff' // 白色文本
  },
  
  // 警告 - 避免单独使用黄色
  warning: {
    light: '#f57c00',  // 深橙色而非黄色
    dark: '#ffc107',   // 金黄色
    contrast: '#000000' // 黑色文本
  },
  
  // 错误 - 不仅依赖颜色
  error: {
    light: '#c62828',  // 深红色
    dark: '#f44336',   // 浅红色
    contrast: '#ffffff',
    icon: '✕'          // 额外的视觉标识
  },
  
  // 信息
  info: {
    light: '#0277bd',  // 深蓝色
    dark: '#29b6f6',   // 浅蓝色
    contrast: '#ffffff'
  }
};

// Alert组件
export function Alert({ 
  type, 
  message 
}: { 
  type: keyof typeof semanticColors; 
  message: string;
}) {
  const colors = semanticColors[type];
  const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  const bgColor = isDark ? colors.dark : colors.light;
  
  return (
    <div
      role="alert"
      style={{
        backgroundColor: bgColor,
        color: colors.contrast,
        padding: '12px 16px',
        borderRadius: '4px',
        display: 'flex',
        alignItems: 'center',
        gap: '8px'
      }}
    >
      {colors.icon && <span aria-hidden="true">{colors.icon}</span>}
      <span>{message}</span>
    </div>
  );
}

4. 常见对比度问题

4.1 灰色文本

typescript
// ❌ 常见错误 - 灰色文本对比度不足
const badColors = {
  background: '#ffffff',
  textLight: '#cccccc',    // 1.6:1 ❌ 不合格
  textMedium: '#999999',   // 2.8:1 ❌ 不合格
  placeholder: '#aaaaaa'   // 2.3:1 ❌ 不合格
};

// ✅ 改进 - 使用更深的灰色
const goodColors = {
  background: '#ffffff',
  textSecondary: '#666666', // 5.7:1 ✅ AA
  textTertiary: '#757575',  // 4.6:1 ✅ AA
  placeholder: '#616161'    // 5.9:1 ✅ AA
};

// React组件
export function Text({
  children,
  variant = 'primary'
}: {
  children: React.ReactNode;
  variant?: 'primary' | 'secondary' | 'tertiary';
}) {
  const colors = {
    primary: '#1a1a1a',   // 15.8:1
    secondary: '#4d4d4d', // 8.6:1
    tertiary: '#666666'   // 5.7:1
  };
  
  return (
    <span style={{ color: colors[variant] }}>
      {children}
    </span>
  );
}

4.2 链接颜色

typescript
// 链接可访问性
const linkColors = {
  // ❌ 仅依赖颜色区分
  bad: {
    text: '#333333',
    link: '#0066cc',
    visited: '#800080'
  },
  
  // ✅ 颜色 + 下划线
  good: {
    text: '#1a1a1a',
    link: '#0066cc',
    visited: '#5c2d91',
    decoration: 'underline' // 额外视觉提示
  }
};

// CSS
const linkStyles = `
  a {
    color: #0066cc;
    text-decoration: underline;
    text-underline-offset: 2px;
  }
  
  a:visited {
    color: #5c2d91;
  }
  
  a:hover,
  a:focus {
    color: #004499;
    text-decoration-thickness: 2px;
  }
  
  /* 不要移除下划线,除非有其他视觉区分 */
  a {
    text-decoration: none; /* ❌ 不好 */
  }
`;

// React组件
export function Link({ href, children }: { href: string; children: React.ReactNode }) {
  return (
    <a
      href={href}
      style={{
        color: '#0066cc',
        textDecoration: 'underline',
        textUnderlineOffset: '2px'
      }}
    >
      {children}
    </a>
  );
}

4.3 按钮对比度

typescript
// 按钮配色
const buttonColors = {
  // Primary按钮
  primary: {
    background: '#0066cc',  // 背景色
    text: '#ffffff',        // 白色文本 (7.7:1) ✅
    hover: '#004499',
    active: '#003366'
  },
  
  // Secondary按钮 - 需要边框对比度
  secondary: {
    background: 'transparent',
    text: '#0066cc',
    border: '#0066cc',      // 边框与背景对比度 >= 3:1
    hover: {
      background: '#e6f2ff',
      text: '#004499'
    }
  },
  
  // Disabled状态 - 允许对比度较低
  disabled: {
    background: '#e0e0e0',
    text: '#9e9e9e',        // 禁用状态可以不符合对比度要求
    cursor: 'not-allowed'
  }
};

// Button组件
export function Button({
  variant = 'primary',
  disabled = false,
  children
}: {
  variant?: 'primary' | 'secondary';
  disabled?: boolean;
  children: React.ReactNode;
}) {
  const getStyles = () => {
    if (disabled) {
      return {
        backgroundColor: '#e0e0e0',
        color: '#9e9e9e',
        cursor: 'not-allowed',
        border: 'none'
      };
    }
    
    if (variant === 'primary') {
      return {
        backgroundColor: '#0066cc',
        color: '#ffffff',
        border: 'none'
      };
    }
    
    return {
      backgroundColor: 'transparent',
      color: '#0066cc',
      border: '2px solid #0066cc'
    };
  };
  
  return (
    <button
      disabled={disabled}
      style={getStyles()}
      aria-disabled={disabled}
    >
      {children}
    </button>
  );
}

5. 深色模式对比度

5.1 深色主题设计

typescript
// 深色主题对比度要求更高
const darkModeTheme = {
  // 背景层级
  background: {
    primary: '#121212',    // 主背景
    secondary: '#1e1e1e',  // 卡片背景
    tertiary: '#2d2d2d'    // 提升的表面
  },
  
  // 文本 - 避免纯白(会刺眼)
  text: {
    primary: '#e0e0e0',    // 主文本 (13.6:1) ✅
    secondary: '#b0b0b0',  // 次要文本 (8.2:1) ✅
    tertiary: '#808080',   // 三级文本 (4.6:1) ✅
    disabled: '#6e6e6e'    // 禁用文本 (3.4:1)
  },
  
  // 强调色 - 需要调整亮度
  colors: {
    primary: '#66b3ff',    // 比浅色模式更亮
    success: '#81c784',
    warning: '#ffb74d',
    error: '#e57373'
  }
};

// 自适应颜色Hook
export function useAdaptiveColor(lightColor: string, darkColor: string) {
  const isDark = useMediaQuery('(prefers-color-scheme: dark)');
  return isDark ? darkColor : lightColor;
}

// 使用
function MyComponent() {
  const textColor = useAdaptiveColor('#1a1a1a', '#e0e0e0');
  const bgColor = useAdaptiveColor('#ffffff', '#121212');
  
  return (
    <div style={{ color: textColor, backgroundColor: bgColor }}>
      内容
    </div>
  );
}

5.2 深色模式切换

typescript
// ThemeProvider.tsx
export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [mode, setMode] = useState<'light' | 'dark'>('light');
  
  // 检测系统偏好
  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    setMode(mediaQuery.matches ? 'dark' : 'light');
    
    const handler = (e: MediaQueryListEvent) => {
      setMode(e.matches ? 'dark' : 'light');
    };
    
    mediaQuery.addEventListener('change', handler);
    return () => mediaQuery.removeEventListener('change', handler);
  }, []);
  
  const theme = mode === 'dark' ? darkModeTheme : accessibleTheme.light;
  
  return (
    <ThemeContext.Provider value={{ theme, mode, setMode }}>
      <div
        style={{
          backgroundColor: theme.background.primary,
          color: theme.text.primary,
          minHeight: '100vh'
        }}
      >
        {children}
      </div>
    </ThemeContext.Provider>
  );
}

6. 色盲友好设计

6.1 色盲类型

typescript
const colorBlindnessTypes = {
  protanopia: {
    name: '红色盲',
    affects: '1-2%男性',
    difficulty: '区分红色和绿色',
    solution: '使用蓝色和黄色,添加图案/形状'
  },
  
  deuteranopia: {
    name: '绿色盲',
    affects: '1%男性',
    difficulty: '区分红色和绿色',
    solution: '同上'
  },
  
  tritanopia: {
    name: '蓝色盲',
    affects: '0.01%人口',
    difficulty: '区分蓝色和黄色',
    solution: '使用红色和绿色'
  },
  
  achromatopsia: {
    name: '全色盲',
    affects: '0.003%人口',
    difficulty: '只能看到灰度',
    solution: '依赖对比度而非颜色'
  }
};

6.2 不依赖颜色

typescript
// ❌ 仅依赖颜色
function BadStatus({ status }: { status: 'success' | 'error' }) {
  const color = status === 'success' ? 'green' : 'red';
  return <span style={{ color }}>{status}</span>;
}

// ✅ 颜色 + 图标 + 文本
function GoodStatus({ status }: { status: 'success' | 'error' }) {
  const config = {
    success: {
      color: '#2e7d32',
      icon: '✓',
      text: '成功'
    },
    error: {
      color: '#c62828',
      icon: '✕',
      text: '失败'
    }
  }[status];
  
  return (
    <span
      style={{ color: config.color }}
      role="status"
      aria-label={config.text}
    >
      <span aria-hidden="true">{config.icon}</span>
      {config.text}
    </span>
  );
}

// 图表使用图案而非颜色
function AccessibleChart({ data }: { data: ChartData[] }) {
  return (
    <BarChart>
      {data.map((item, index) => (
        <Bar
          key={item.id}
          fill={item.color}
          // 添加图案
          pattern={patterns[index % patterns.length]}
          // 添加标签
          label={item.value}
        />
      ))}
    </BarChart>
  );
}

7. 对比度增强

7.1 CSS强制颜色模式

css
/* Windows高对比度模式 */
@media (prefers-contrast: high) {
  body {
    background: black;
    color: white;
  }
  
  button {
    border: 2px solid white;
  }
  
  a {
    color: yellow;
    text-decoration: underline;
  }
}

/* 强制颜色模式 */
@media (forced-colors: active) {
  .custom-button {
    /* 使用系统颜色 */
    background-color: ButtonFace;
    color: ButtonText;
    border: 1px solid ButtonBorder;
  }
  
  .custom-button:hover {
    background-color: Highlight;
    color: HighlightText;
  }
}

7.2 React对比度增强

tsx
// ContrastEnhancer.tsx
export function ContrastEnhancer({ children }: { children: React.ReactNode }) {
  const [highContrast, setHighContrast] = useState(false);
  
  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-contrast: high)');
    setHighContrast(mediaQuery.matches);
    
    const handler = (e: MediaQueryListEvent) => {
      setHighContrast(e.matches);
    };
    
    mediaQuery.addEventListener('change', handler);
    return () => mediaQuery.removeEventListener('change', handler);
  }, []);
  
  if (highContrast) {
    return (
      <div className="high-contrast-mode">
        {children}
      </div>
    );
  }
  
  return <>{children}</>;
}

// CSS
.high-contrast-mode {
  filter: contrast(1.5);
}

.high-contrast-mode button {
  border: 2px solid currentColor !important;
}

8. 对比度测试

8.1 自动化测试

typescript
// contrast.test.ts
import { render } from '@testing-library/react';
import { checkContrast } from './contrast-checker';

describe('Color Contrast', () => {
  it('should have sufficient contrast for text', () => {
    const { container } = render(<MyComponent />);
    
    const element = container.querySelector('.text');
    const styles = window.getComputedStyle(element!);
    
    const fg = styles.color;
    const bg = styles.backgroundColor;
    
    const result = checkContrast(fg, bg, 16);
    expect(result.AA).toBe(true);
  });
  
  it('should meet AAA for important text', () => {
    const { container } = render(<ImportantText />);
    
    const element = container.querySelector('h1');
    const styles = window.getComputedStyle(element!);
    
    const result = checkContrast(
      styles.color,
      styles.backgroundColor,
      24
    );
    
    expect(result.AAA).toBe(true);
  });
});

8.2 视觉回归测试

typescript
// visual-contrast.test.ts
import { test } from '@playwright/test';

test('contrast in different themes', async ({ page }) => {
  await page.goto('/');
  
  // 测试浅色主题
  await page.screenshot({ path: 'light-theme.png' });
  
  // 切换到深色主题
  await page.click('[data-testid="theme-toggle"]');
  await page.screenshot({ path: 'dark-theme.png' });
  
  // 高对比度模式
  await page.emulateMedia({ colorScheme: 'dark', forcedColors: 'active' });
  await page.screenshot({ path: 'high-contrast.png' });
});

9. 最佳实践

typescript
const contrastBestPractices = {
  design: [
    '普通文本至少4.5:1对比度',
    '大文本至少3:1对比度',
    'UI组件至少3:1对比度',
    '重要内容追求7:1(AAA级)',
    '深色模式使用更亮的强调色'
  ],
  
  color: [
    '不要仅依赖颜色传达信息',
    '使用图标、图案、文本辅助',
    '提供高对比度模式',
    '测试色盲友好性',
    '避免纯黑/纯白(刺眼)'
  ],
  
  testing: [
    '使用自动化工具检测',
    '手动验证边界情况',
    '测试深色/浅色主题',
    '模拟视觉障碍',
    '真实设备测试'
  ],
  
  implementation: [
    '建立可访问的色彩系统',
    '使用CSS变量管理颜色',
    '支持系统主题偏好',
    '提供用户切换选项',
    '文档化颜色使用规范'
  ]
};

10. 总结

颜色对比度优化的关键要点:

  1. 遵循WCAG: AA级最低4.5:1,AAA级7:1
  2. 工具检测: 使用浏览器工具和自动化测试
  3. 深色模式: 调整亮度保持对比度
  4. 不依赖颜色: 使用图标、形状辅助
  5. 色盲友好: 避免红绿组合
  6. 高对比度: 支持系统高对比度模式
  7. 持续测试: 开发过程中持续验证

通过优化颜色对比度,可以让应用对所有用户都清晰可读。