design-system

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Design System Accessibility Skill

设计系统无障碍Skill

This skill provides reference data for design token contrast validation, focus ring compliance, and spacing audits. Used by
design-system-auditor.agent.md
.

本Skill提供设计token对比度验证、焦点环合规检查、间距审核的参考数据,供
design-system-auditor.agent.md
使用。

WCAG Contrast Ratio - Computation Reference

WCAG 对比度计算参考

Step 1: Linearize sRGB Channel

步骤1:线性化sRGB通道

For each channel
C
in
[0, 255]
:
text
c = C / 255
c_lin = c / 12.92              if c <= 0.04045
c_lin = ((c + 0.055) / 1.055)^2.4   otherwise
对于
[0, 255]
范围内的每个通道
C
text
c = C / 255
c_lin = c / 12.92              if c <= 0.04045
c_lin = ((c + 0.055) / 1.055)^2.4   otherwise

Step 2: Relative Luminance

步骤2:相对亮度

text
L = 0.2126 * R_lin + 0.7152 * G_lin + 0.0722 * B_lin
text
L = 0.2126 * R_lin + 0.7152 * G_lin + 0.0722 * B_lin

Step 3: Contrast Ratio

步骤3:对比度

text
ratio = (L_lighter + 0.05) / (L_darker + 0.05)
text
ratio = (L_lighter + 0.05) / (L_darker + 0.05)

Quick JavaScript Implementation

快速JavaScript实现

js
function relativeLuminance(hex) {
  const c = hex.replace('#', '').match(/.{2}/g)
    .map(h => parseInt(h, 16) / 255)
    .map(c => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
  return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
}

function contrastRatio(hex1, hex2) {
  const L1 = relativeLuminance(hex1);
  const L2 = relativeLuminance(hex2);
  const lighter = Math.max(L1, L2);
  const darker = Math.min(L1, L2);
  return (lighter + 0.05) / (darker + 0.05);
}

// Example
contrastRatio('#6B7280', '#FFFFFF'); // 5.74:1 - PASSES AA (was a common misconception)
contrastRatio('#9CA3AF', '#FFFFFF'); // 2.85:1 - FAILS AA
js
function relativeLuminance(hex) {
  const c = hex.replace('#', '').match(/.{2}/g)
    .map(h => parseInt(h, 16) / 255)
    .map(c => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
  return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
}

function contrastRatio(hex1, hex2) {
  const L1 = relativeLuminance(hex1);
  const L2 = relativeLuminance(hex2);
  const lighter = Math.max(L1, L2);
  const darker = Math.min(L1, L2);
  return (lighter + 0.05) / (darker + 0.05);
}

// Example
contrastRatio('#6B7280', '#FFFFFF'); // 5.74:1 - PASSES AA (was a common misconception)
contrastRatio('#9CA3AF', '#FFFFFF'); // 2.85:1 - FAILS AA

HSL to Hex Conversion (for CSS variable tokens)

HSL转Hex转换(适用于CSS变量token)

Many design systems store colors as HSL triplets (e.g., shadcn/ui, Radix):
js
function hslToHex(h, s, l) {
  s /= 100; l /= 100;
  const a = s * Math.min(l, 1 - l);
  const f = n => {
    const k = (n + h / 30) % 12;
    return l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
  };
  return '#' + [f(0), f(8), f(4)]
    .map(x => Math.round(x * 255).toString(16).padStart(2, '0'))
    .join('');
}

// shadcn/ui: --muted-foreground: 215.4 16.3% 46.9%
hslToHex(215.4, 16.3, 46.9); // -> approximately #6B7280

很多设计系统将颜色存储为HSL三元组(例如shadcn/ui、Radix):
js
function hslToHex(h, s, l) {
  s /= 100; l /= 100;
  const a = s * Math.min(l, 1 - l);
  const f = n => {
    const k = (n + h / 30) % 12;
    return l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
  };
  return '#' + [f(0), f(8), f(4)]
    .map(x => Math.round(x * 255).toString(16).padStart(2, '0'))
    .join('');
}

// shadcn/ui: --muted-foreground: 215.4 16.3% 46.9%
hslToHex(215.4, 16.3, 46.9); // -> approximately #6B7280

WCAG Contrast Thresholds

WCAG 对比度阈值

Use CaseAAAAANotes
Normal text (< 18pt / < 14pt bold)4.5:17:1Most body text
Large text (>= 18pt / >= 14pt bold)3:14.5:1Headings, display text
UI components (borders, icons)3:1-Input borders, icon buttons
Focus indicators (WCAG 2.4.13, 2.2)3:1-Against adjacent colors
Placeholder text4.5:1-Counts as normal text
Disabled stateExemptExemptDocumented exemption
Logo / brandExemptExemptNo requirement
Decorative contentExemptExemptMust be marked decorative

使用场景AAAAA备注
普通文本(< 18pt / < 14pt粗体)4.5:17:1绝大多数正文文本
大文本(>= 18pt / >= 14pt粗体)3:14.5:1标题、展示文本
UI组件(边框、图标)3:1-输入框边框、图标按钮
焦点指示器(WCAG 2.4.13, 2.2)3:1-与相邻颜色的对比度
占位符文本4.5:1-按普通文本要求计算
禁用状态豁免豁免有明确记录的豁免项
Logo / 品牌标识豁免豁免无要求
装饰性内容豁免豁免必须标记为装饰性

Framework Token Paths - Complete Reference

框架Token路径完整参考

Tailwind CSS

Tailwind CSS

js
// tailwind.config.js / tailwind.config.ts
module.exports = {
  theme: {
    // Base colors (Tailwind default palette)
    colors: {
      // All color scales: slate, gray, zinc, neutral, stone, red, orange, amber,
      // yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet,
      // purple, fuchsia, pink, rose
      // Each scale: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950
    },
    extend: {
      colors: {
        // Custom semantic colors - CHECK ALL PAIRS
        brand: { primary: '#...', secondary: '#...' },
        background: '#...',
        foreground: '#...',
        muted: '#...',
        'muted-foreground': '#...',
        accent: '#...',
        'accent-foreground': '#...',
        destructive: '#...',
        'destructive-foreground': '#...',
        card: '#...',
        'card-foreground': '#...',
        popover: '#...',
        'popover-foreground': '#...',
        border: '#...',    // UI component - check 3:1 against background
        input: '#...',     // UI component - check 3:1 against background
        ring: '#...',      // Focus ring - check 3:1 against background
        primary: '#...',
        'primary-foreground': '#...',
        secondary: '#...',
        'secondary-foreground': '#...',
      },
      ringColor: { DEFAULT: '...' },  // Focus state
      ringWidth: { DEFAULT: '2px' },  // Must be >= 2px for WCAG 2.4.13
    }
  }
}
js
// tailwind.config.js / tailwind.config.ts
module.exports = {
  theme: {
    // Base colors (Tailwind default palette)
    colors: {
      // All color scales: slate, gray, zinc, neutral, stone, red, orange, amber,
      // yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet,
      // purple, fuchsia, pink, rose
      // Each scale: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950
    },
    extend: {
      colors: {
        // Custom semantic colors - CHECK ALL PAIRS
        brand: { primary: '#...', secondary: '#...' },
        background: '#...',
        foreground: '#...',
        muted: '#...',
        'muted-foreground': '#...',
        accent: '#...',
        'accent-foreground': '#...',
        destructive: '#...',
        'destructive-foreground': '#...',
        card: '#...',
        'card-foreground': '#...',
        popover: '#...',
        'popover-foreground': '#...',
        border: '#...',    // UI component - check 3:1 against background
        input: '#...',     // UI component - check 3:1 against background
        ring: '#...',      // Focus ring - check 3:1 against background
        primary: '#...',
        'primary-foreground': '#...',
        secondary: '#...',
        'secondary-foreground': '#...',
      },
      ringColor: { DEFAULT: '...' },  // Focus state
      ringWidth: { DEFAULT: '2px' },  // Must be >= 2px for WCAG 2.4.13
    }
  }
}

shadcn/ui / Radix CSS Variables

shadcn/ui / Radix CSS变量

css
/* globals.css - HSL triplets without hsl() wrapper */
:root {
  --background: 0 0% 100%;
  --foreground: 222.2 84% 4.9%;
  --card: 0 0% 100%;
  --card-foreground: 222.2 84% 4.9%;
  --popover: 0 0% 100%;
  --popover-foreground: 222.2 84% 4.9%;
  --primary: 222.2 47.4% 11.2%;
  --primary-foreground: 210 40% 98%;
  --secondary: 210 40% 96.1%;
  --secondary-foreground: 222.2 47.4% 11.2%;
  --muted: 210 40% 96.1%;
  --muted-foreground: 215.4 16.3% 46.9%;     /* HIGH RISK - check on --background */
  --accent: 210 40% 96.1%;
  --accent-foreground: 222.2 47.4% 11.2%;
  --destructive: 0 84.2% 60.2%;               /* HIGH RISK - red on white */
  --destructive-foreground: 210 40% 98%;
  --border: 214.3 31.8% 91.4%;               /* UI component - check 3:1 */
  --input: 214.3 31.8% 91.4%;                /* UI component - check 3:1 */
  --ring: 222.2 84% 4.9%;                    /* Focus ring - check 3:1 */
}

.dark {
  --background: 222.2 84% 4.9%;
  --foreground: 210 40% 98%;
  /* ... all dark mode variants */
}
css
/* globals.css - HSL triplets without hsl() wrapper */
:root {
  --background: 0 0% 100%;
  --foreground: 222.2 84% 4.9%;
  --card: 0 0% 100%;
  --card-foreground: 222.2 84% 4.9%;
  --popover: 0 0% 100%;
  --popover-foreground: 222.2 84% 4.9%;
  --primary: 222.2 47.4% 11.2%;
  --primary-foreground: 210 40% 98%;
  --secondary: 210 40% 96.1%;
  --secondary-foreground: 222.2 47.4% 11.2%;
  --muted: 210 40% 96.1%;
  --muted-foreground: 215.4 16.3% 46.9%;     /* HIGH RISK - check on --background */
  --accent: 210 40% 96.1%;
  --accent-foreground: 222.2 47.4% 11.2%;
  --destructive: 0 84.2% 60.2%;               /* HIGH RISK - red on white */
  --destructive-foreground: 210 40% 98%;
  --border: 214.3 31.8% 91.4%;               /* UI component - check 3:1 */
  --input: 214.3 31.8% 91.4%;                /* UI component - check 3:1 */
  --ring: 222.2 84% 4.9%;                    /* Focus ring - check 3:1 */
}

.dark {
  --background: 222.2 84% 4.9%;
  --foreground: 210 40% 98%;
  /* ... all dark mode variants */
}

Material UI (MUI) v5+

Material UI (MUI) v5+

js
// Token paths in createTheme()
palette: {
  primary: {
    main: '#1976d2',            // text-on-white: 4.56:1 
    light: '#42a5f5',           // text-on-white: 2.86:1  (do not use as text color)
    dark: '#1565c0',            // text-on-white: 5.91:1 
    contrastText: '#fff',       // check on main
  },
  secondary: {
    main: '#9c27b0',            // text-on-white: 4.56:1  (barely)
    light: '#ba68c8',           // text-on-white: 2.55:1 
    dark: '#7b1fa2',
    contrastText: '#fff',
  },
  error: {
    main: '#d32f2f',            // text-on-white: 5.08:1 
    light: '#ef5350',           // text-on-white: 3.04:1 
  },
  warning: {
    main: '#ed6c02',            // text-on-white: 2.94:1  COMMON FAILURE
    light: '#ff9800',           // text-on-white: 2.02:1 
    dark: '#e65100',            // text-on-white: 3.84:1  (still fails!)
    contrastText: 'rgba(0, 0, 0, 0.87)',  // check on warning.main
  },
  info: {
    main: '#0288d1',            // text-on-white: 4.54:1  (barely)
    light: '#03a9f4',           // text-on-white: 2.88:1 
  },
  success: {
    main: '#2e7d32',            // text-on-white: 7.24:1 
    light: '#4caf50',           // text-on-white: 2.52:1 
  },
  text: {
    primary: 'rgba(0,0,0,0.87)',   // -> ~#212121: 16.07:1 on white 
    secondary: 'rgba(0,0,0,0.6)', // -> ~#666: 5.74:1 on white 
    disabled: 'rgba(0,0,0,0.38)', // -> ~#9E9E9E: 2.34:1  (exempt when disabled)
  },
  background: { paper: '#fff', default: '#fafafa' },
  action: {
    active: 'rgba(0,0,0,0.54)',   // ~4.48:1  for small icons
    disabled: 'rgba(0,0,0,0.26)', // exempt when disabled
  }
}
js
// Token paths in createTheme()
palette: {
  primary: {
    main: '#1976d2',            // text-on-white: 4.56:1 
    light: '#42a5f5',           // text-on-white: 2.86:1  (do not use as text color)
    dark: '#1565c0',            // text-on-white: 5.91:1 
    contrastText: '#fff',       // check on main
  },
  secondary: {
    main: '#9c27b0',            // text-on-white: 4.56:1  (barely)
    light: '#ba68c8',           // text-on-white: 2.55:1 
    dark: '#7b1fa2',
    contrastText: '#fff',
  },
  error: {
    main: '#d32f2f',            // text-on-white: 5.08:1 
    light: '#ef5350',           // text-on-white: 3.04:1 
  },
  warning: {
    main: '#ed6c02',            // text-on-white: 2.94:1  COMMON FAILURE
    light: '#ff9800',           // text-on-white: 2.02:1 
    dark: '#e65100',            // text-on-white: 3.84:1  (still fails!)
    contrastText: 'rgba(0, 0, 0, 0.87)',  // check on warning.main
  },
  info: {
    main: '#0288d1',            // text-on-white: 4.54:1  (barely)
    light: '#03a9f4',           // text-on-white: 2.88:1 
  },
  success: {
    main: '#2e7d32',            // text-on-white: 7.24:1 
    light: '#4caf50',           // text-on-white: 2.52:1 
  },
  text: {
    primary: 'rgba(0,0,0,0.87)',   // -> ~#212121: 16.07:1 on white 
    secondary: 'rgba(0,0,0,0.6)', // -> ~#666: 5.74:1 on white 
    disabled: 'rgba(0,0,0,0.38)', // -> ~#9E9E9E: 2.34:1  (exempt when disabled)
  },
  background: { paper: '#fff', default: '#fafafa' },
  action: {
    active: 'rgba(0,0,0,0.54)',   // ~4.48:1  for small icons
    disabled: 'rgba(0,0,0,0.26)', // exempt when disabled
  }
}

Chakra UI v2/v3

Chakra UI v2/v3

js
// Token paths in extendTheme()
const theme = extendTheme({
  colors: {
    // Direct palette values
    brand: { 50: '#f5f3ff', 500: '#7C3AED', 600: '#6D28D9', 700: '#5B21B6', 900: '#2E1065' },
    gray: { 50: '#F9FAFB', 100: '#F3F4F6', 200: '#E5E7EB', 300: '#D1D5DB',
            400: '#9CA3AF',  // text-on-white: 2.85:1 
            500: '#6B7280',  // text-on-white: 4.48:1  (near-miss)
            600: '#4B5563',  // text-on-white: 7.44:1 
            700: '#374151', 800: '#1F2937', 900: '#111827' },
  },
  semanticTokens: {
    colors: {
      'chakra-body-text': { default: 'gray.800', _dark: 'whiteAlpha.900' },
      'chakra-body-bg': { default: 'white', _dark: 'gray.800' },
      'chakra-placeholder-color': { default: 'gray.400', _dark: 'whiteAlpha.400' },
      // gray.400 on white = 2.85:1  - placeholder fails AA
    }
  },
  components: {
    Button: {
      variants: {
        solid: (props) => ({
          bg: `${props.colorScheme}.500`,  // check colorScheme.500 on white
          color: 'white',                  // white on colorScheme.500 - check 3:1
        }),
        ghost: (props) => ({
          color: `${props.colorScheme}.600`,  // text-on-white variant
        }),
      }
    }
  }
});
js
// Token paths in extendTheme()
const theme = extendTheme({
  colors: {
    // Direct palette values
    brand: { 50: '#f5f3ff', 500: '#7C3AED', 600: '#6D28D9', 700: '#5B21B6', 900: '#2E1065' },
    gray: { 50: '#F9FAFB', 100: '#F3F4F6', 200: '#E5E7EB', 300: '#D1D5DB',
            400: '#9CA3AF',  // text-on-white: 2.85:1 
            500: '#6B7280',  // text-on-white: 4.48:1  (near-miss)
            600: '#4B5563',  // text-on-white: 7.44:1 
            700: '#374151', 800: '#1F2937', 900: '#111827' },
  },
  semanticTokens: {
    colors: {
      'chakra-body-text': { default: 'gray.800', _dark: 'whiteAlpha.900' },
      'chakra-body-bg': { default: 'white', _dark: 'gray.800' },
      'chakra-placeholder-color': { default: 'gray.400', _dark: 'whiteAlpha.400' },
      // gray.400 on white = 2.85:1  - placeholder fails AA
    }
  },
  components: {
    Button: {
      variants: {
        solid: (props) => ({
          bg: `${props.colorScheme}.500`,  // check colorScheme.500 on white
          color: 'white',                  // white on colorScheme.500 - check 3:1
        }),
        ghost: (props) => ({
          color: `${props.colorScheme}.600`,  // text-on-white variant
        }),
      }
    }
  }
});

Style Dictionary (W3C Design Tokens)

Style Dictionary (W3C设计Tokens)

json
{
  "color": {
    "text": {
      "primary": { "$value": "#111827", "$type": "color" },
      "secondary": { "$value": "#6B7280", "$type": "color" },   // 4.48:1 on white 
      "muted": { "$value": "#9CA3AF", "$type": "color" },       // 2.85:1 on white 
      "inverse": { "$value": "#FFFFFF", "$type": "color" },
      "on-primary": { "$value": "#FFFFFF", "$type": "color" }
    },
    "background": {
      "default": { "$value": "#FFFFFF", "$type": "color" },
      "subtle": { "$value": "#F9FAFB", "$type": "color" },
      "primary": { "$value": "#1D4ED8", "$type": "color" }
    },
    "status": {
      "error": { "$value": "#DC2626", "$type": "color" },
      "warning": { "$value": "#D97706", "$type": "color" },     // 3:1 on white  for normal text
      "success": { "$value": "#16A34A", "$type": "color" },
      "info": { "$value": "#2563EB", "$type": "color" }
    },
    "border": {
      "default": { "$value": "#D1D5DB", "$type": "color" },     // 1.44:1 on white  UI component
      "focus": { "$value": "#2563EB", "$type": "color" }        // focus ring
    }
  }
}

json
{
  "color": {
    "text": {
      "primary": { "$value": "#111827", "$type": "color" },
      "secondary": { "$value": "#6B7280", "$type": "color" },   // 4.48:1 on white 
      "muted": { "$value": "#9CA3AF", "$type": "color" },       // 2.85:1 on white 
      "inverse": { "$value": "#FFFFFF", "$type": "color" },
      "on-primary": { "$value": "#FFFFFF", "$type": "color" }
    },
    "background": {
      "default": { "$value": "#FFFFFF", "$type": "color" },
      "subtle": { "$value": "#F9FAFB", "$type": "color" },
      "primary": { "$value": "#1D4ED8", "$type": "color" }
    },
    "status": {
      "error": { "$value": "#DC2626", "$type": "color" },
      "warning": { "$value": "#D97706", "$type": "color" },     // 3:1 on white  for normal text
      "success": { "$value": "#16A34A", "$type": "color" },
      "info": { "$value": "#2563EB", "$type": "color" }
    },
    "border": {
      "default": { "$value": "#D1D5DB", "$type": "color" },     // 1.44:1 on white  UI component
      "focus": { "$value": "#2563EB", "$type": "color" }        // focus ring
    }
  }
}

High-Risk Token Pairs - Known Failures

高风险Token对-已知不合规项

Token pairCommon valueRatio on whiteStatusNotes
MUI
warning.main
#ed6c02
2.94:1FAILOrange on white - always fails
MUI
warning.light
#ff9800
2.02:1FAILLight orange - critical failure
Tailwind
amber-400
#FBBF24
1.73:1FAILNever use amber-400 as text
Tailwind
yellow-400
#FACC15
1.60:1FAILYellow always fails on white
gray-400 (Tailwind)
#9CA3AF
2.85:1FAILCommon placeholder color
gray-500 (Tailwind)
#6B7280
4.48:1FAILNear-miss - very common
Chakra
gray.400
#9CA3AF
2.85:1FAILChakra placeholder default
MUI
text.disabled
rgba(0,0,0,0.38)
~2.34:1(exempt)Disabled = exempt per WCAG
MUI
action.active
rgba(0,0,0,0.54)
~4.48:1FAILIcon color on white
shadcn
--muted-foreground
hsl(215.4 16.3% 46.9%)
~4.48:1FAILDefault shadcn theme
shadcn
--destructive
hsl(0 84.2% 60.2%)
~3.13:1FAILRed badge on white
Style Dictionary
text.secondary
#6B7280
4.48:1FAILUbiquitous - always check
Token对常见取值白色背景下对比度状态备注
MUI
warning.main
#ed6c02
2.94:1不通过橙色在白色背景上 - 始终不通过
MUI
warning.light
#ff9800
2.02:1不通过浅橙色 - 严重不通过
Tailwind
amber-400
#FBBF24
1.73:1不通过切勿将amber-400用作文本颜色
Tailwind
yellow-400
#FACC15
1.60:1不通过黄色在白色背景上始终不通过
gray-400 (Tailwind)
#9CA3AF
2.85:1不通过常用占位符颜色
gray-500 (Tailwind)
#6B7280
4.48:1不通过接近达标 - 非常常见
Chakra
gray.400
#9CA3AF
2.85:1不通过Chakra默认占位符颜色
MUI
text.disabled
rgba(0,0,0,0.38)
~2.34:1(豁免)禁用状态按WCAG规定豁免
MUI
action.active
rgba(0,0,0,0.54)
~4.48:1不通过白色背景上的图标颜色
shadcn
--muted-foreground
hsl(215.4 16.3% 46.9%)
~4.48:1不通过shadcn默认主题
shadcn
--destructive
hsl(0 84.2% 60.2%)
~3.13:1不通过白色背景上的红色徽章
Style Dictionary
text.secondary
#6B7280
4.48:1不通过普遍存在 - 务必检查

Compliant Replacements

合规替换方案

Failing tokenReplacementNew ratioNotes
#9CA3AF
(gray-400)
#6B7280
(gray-500)
4.48:1Still near-miss; use
#595959
for safety
#6B7280
(gray-500)
#4B5563
(gray-600)
7.44:1Safest option
#ed6c02
(MUI warning)
#b45309
(amber-700)
4.57:1Minimum pass
#ff9800
(MUI warning.light)
#b45309
(amber-700)
4.57:1
#FBBF24
(amber-400)
#92400e
(amber-800)
8.80:1Use as background, not text
#FACC15
(yellow-400)
#713f12
(yellow-900)
12.04:1Use as background, not text
hsl(0 84.2% 60.2%)
(shadcn destructive)
#b91c1c
(red-700)
5.56:1

不合规Token替换方案新对比度备注
#9CA3AF
(gray-400)
#6B7280
(gray-500)
4.48:1仍接近达标;建议使用
#595959
更安全
#6B7280
(gray-500)
#4B5563
(gray-600)
7.44:1最安全的选择
#ed6c02
(MUI warning)
#b45309
(amber-700)
4.57:1最低达标值
#ff9800
(MUI warning.light)
#b45309
(amber-700)
4.57:1
#FBBF24
(amber-400)
#92400e
(amber-800)
8.80:1用作背景色,不要用作文本
#FACC15
(yellow-400)
#713f12
(yellow-900)
12.04:1用作背景色,不要用作文本
hsl(0 84.2% 60.2%)
(shadcn destructive)
#b91c1c
(red-700)
5.56:1

Storybook addon-a11y Configuration

Storybook addon-a11y配置

bash
npm install --save-dev @storybook/addon-a11y
js
// .storybook/main.js
module.exports = {
  addons: ['@storybook/addon-a11y'],
};

// .storybook/preview.js - global configuration
export const parameters = {
  a11y: {
    config: {
      rules: [
        { id: 'color-contrast', enabled: true },
        { id: 'button-name', enabled: true },
        { id: 'image-alt', enabled: true },
        { id: 'focus-visible', enabled: true },    // Requires axe-core 4.4+
        { id: 'target-size', enabled: true },       // WCAG 2.5.5 / 2.5.8
      ],
    },
    // Disable for specific stories (use sparingly)
    disable: false,
  },
};

// Per-story override
export const MyStory = {
  parameters: {
    a11y: {
      config: {
        rules: [{ id: 'color-contrast', enabled: false }],  // Document WHY
      }
    }
  }
};
bash
npm install --save-dev @storybook/addon-a11y
js
// .storybook/main.js
module.exports = {
  addons: ['@storybook/addon-a11y'],
};

// .storybook/preview.js - global configuration
export const parameters = {
  a11y: {
    config: {
      rules: [
        { id: 'color-contrast', enabled: true },
        { id: 'button-name', enabled: true },
        { id: 'image-alt', enabled: true },
        { id: 'focus-visible', enabled: true },    // Requires axe-core 4.4+
        { id: 'target-size', enabled: true },       // WCAG 2.5.5 / 2.5.8
      ],
    },
    // Disable for specific stories (use sparingly)
    disable: false,
  },
};

// Per-story override
export const MyStory = {
  parameters: {
    a11y: {
      config: {
        rules: [{ id: 'color-contrast', enabled: false }],  // Document WHY
      }
    }
  }
};

Running Storybook a11y Checks in CI

在CI中运行Storybook无障碍检查

bash
undefined
bash
undefined

Install storybook test runner

Install storybook test runner

npm install --save-dev @storybook/test-runner
npm install --save-dev @storybook/test-runner

package.json scripts

package.json scripts

{ "scripts": { "storybook:test": "test-storybook", "storybook:test:a11y": "test-storybook --ci" } }
{ "scripts": { "storybook:test": "test-storybook", "storybook:test:a11y": "test-storybook --ci" } }

Run in CI

Run in CI

npx storybook dev --port 6006 & npx wait-on tcp:6006 npx test-storybook --ci

---
npx storybook dev --port 6006 & npx wait-on tcp:6006 npx test-storybook --ci

---

WCAG 2.4.13 Focus Appearance Requirements (AAA, exceeds AA baseline)

WCAG 2.4.13 焦点外观要求(AAA级,高于AA基线)

WCAG 2.4.13 Focus Appearance (Level AAA in WCAG 2.2) - exceeds the 2.4.7 Focus Visible (AA) baseline, but recommended as best practice:
  1. Area: Focus indicator encloses the component OR has a perimeter >= component's perimeter x 2px
  2. Contrast change: The focus indicator area must change contrast by >= 3:1 between focused and unfocused states
  3. Not obscured: The focus indicator must not be entirely hidden by author-created content
WCAG 2.4.13 焦点外观(WCAG 2.2中的AAA级) - 要求高于2.4.7焦点可见(AA级)基线,推荐作为最佳实践:
  1. 面积: 焦点指示器需包围组件,或者周长 >= 组件周长 * 2px
  2. 对比度变化: 聚焦和未聚焦状态下,焦点指示器区域的对比度变化 >= 3:1
  3. 不被遮挡: 焦点指示器不能完全被开发者创建的内容隐藏

Minimum Compliant Focus Ring Implementation

最低合规焦点环实现

css
/* Minimum WCAG 2.4.13 compliant focus ring */
:focus-visible {
  outline: 2px solid #0054B3;      /* >= 2px width */
  outline-offset: 2px;             /* Separates from component edge */
  /* #0054B3 on #FFF = 8.28:1 -> passes 3:1 for UI components */
}

/* Dark mode variant */
@media (prefers-color-scheme: dark) {
  :focus-visible {
    outline-color: #7CAFFF;        /* lighter blue on dark background */
    /* #7CAFFF on #1E1E1E = 5.74:1  */
  }
}

/* VIOLATION patterns to detect */
:focus { outline: none; }                           /* Hard fail */
:focus { outline: 0; }                              /* Hard fail */
:focus-visible { box-shadow: none; outline: none; } /* Hard fail */
button:focus { outline: none; }                     /* Hard fail */
*:focus { outline-color: transparent; }             /* Hard fail */
css
/* Minimum WCAG 2.4.13 compliant focus ring */
:focus-visible {
  outline: 2px solid #0054B3;      /* >= 2px width */
  outline-offset: 2px;             /* Separates from component edge */
  /* #0054B3 on #FFF = 8.28:1 -> passes 3:1 for UI components */
}

/* Dark mode variant */
@media (prefers-color-scheme: dark) {
  :focus-visible {
    outline-color: #7CAFFF;        /* lighter blue on dark background */
    /* #7CAFFF on #1E1E1E = 5.74:1  */
  }
}

/* VIOLATION patterns to detect */
:focus { outline: none; }                           /* Hard fail */
:focus { outline: 0; }                              /* Hard fail */
:focus-visible { box-shadow: none; outline: none; } /* Hard fail */
button:focus { outline: none; }                     /* Hard fail */
*:focus { outline-color: transparent; }             /* Hard fail */

Focus Ring Token Validation Checklist

焦点环Token验证检查清单

CheckRequirementTool
outline-width
>= 2px
WCAG 2.4.13 area requirementCSS audit
Focus color contrast >= 3:1Against adjacent backgroundContrast calculator
Focus state differs from unfocusedVisible change requiredVisual inspection
No
outline: none
without replacement
N/Agrep / CSS audit
Present in both light and dark modesConsistentVisual inspection

检查项要求工具
outline-width
>= 2px
WCAG 2.4.13面积要求CSS审核
焦点颜色对比度 >= 3:1与相邻背景的对比度对比度计算器
聚焦状态与未聚焦状态有差异需要可见变化视觉检查
outline: none
且无替代方案
grep / CSS审核
浅色和深色模式下都存在一致性视觉检查

Design Token File Discovery Commands

设计Token文件查找命令

bash
undefined
bash
undefined

Find all token files in a project

Find all token files in a project

find . -type f (
-name "tokens.json"
-o -name "design-tokens.json"
-o -name "colors.json"
-o -name "variables.css"
-o -name "tokens.css"
-o -name "_variables.scss"
-o -name "theme.ts"
-o -name "theme.js"
-o -name "tailwind.config."
)
-not -path "
/node_modules/"
-not -path "
/.next/"
-not -path "
/dist/*"
find . -type f (
-name "tokens.json"
-o -name "design-tokens.json"
-o -name "colors.json"
-o -name "variables.css"
-o -name "tokens.css"
-o -name "_variables.scss"
-o -name "theme.ts"
-o -name "theme.js"
-o -name "tailwind.config."
)
-not -path "
/node_modules/"
-not -path "
/.next/"
-not -path "
/dist/*"

PowerShell equivalent

PowerShell equivalent

Get-ChildItem -Recurse -File -Include tokens.json,design-tokens.json,colors.json,
  variables.css,tokens.css,_variables.scss,theme.ts,theme.js,tailwind.config.js,tailwind.config.ts
| Where-Object { $_.FullName -notmatch 'node_modules|.next|dist' }

---
Get-ChildItem -Recurse -File -Include tokens.json,design-tokens.json,colors.json,
  variables.css,tokens.css,_variables.scss,theme.ts,theme.js,tailwind.config.js,tailwind.config.ts
| Where-Object { $_.FullName -notmatch 'node_modules|.next|dist' }

---

Severity Classification

严重等级分类

FindingSeverity
Text token below 3:1Critical
Text token 3:1-4.49:1 (normal text)Error
Text token 4.5:1-6.99:1, AAA targetWarning
UI component token below 3:1Error
Focus ring missing completelyCritical
Focus ring below 2pxError
Focus ring contrast below 3:1Error
Touch target token below 24 x 24px (WCAG 2.5.8)Error
Touch target token below 44 x 44px (WCAG 2.5.5)Warning
No
prefers-reduced-motion
reset
Warning
Placeholder color below 4.5:1Error
Disabled token below 3:1Info (documented exemption, note for transparency)
发现问题严重等级
文本token对比度低于3:1严重
普通文本文本token对比度在3:1-4.49:1之间错误
文本token对比度在4.5:1-6.99:1之间,AAA级目标警告
UI组件token对比度低于3:1错误
完全缺失焦点环严重
焦点环宽度低于2px错误
焦点环对比度低于3:1错误
触摸目标token尺寸低于24 x 24px(WCAG 2.5.8)错误
触摸目标token尺寸低于44 x 44px(WCAG 2.5.5)警告
未重置
prefers-reduced-motion
警告
占位符颜色对比度低于4.5:1错误
禁用状态token对比度低于3:1提示(有记录的豁免项,为透明起见备注)