Appearance
颜色对比度优化 - 完整视觉可访问性指南
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. 总结
颜色对比度优化的关键要点:
- 遵循WCAG: AA级最低4.5:1,AAA级7:1
- 工具检测: 使用浏览器工具和自动化测试
- 深色模式: 调整亮度保持对比度
- 不依赖颜色: 使用图标、形状辅助
- 色盲友好: 避免红绿组合
- 高对比度: 支持系统高对比度模式
- 持续测试: 开发过程中持续验证
通过优化颜色对比度,可以让应用对所有用户都清晰可读。