design-system について
デザインシステムの監査向けアクセシビリティ資料で、`design-system-auditor.agent.md`から利用されます。デザイントークンのコントラスト計算、フォーカスリングの基準、フレームワークのトークンパス、スペーシング監査に関する確認事項を扱います。
主な機能
- 01WCAGコントラスト計算リファレンス
- 02テキストとUIのコントラスト基準
- 03一般的なデザイントークンのパス
- 04フォーカスリングの適合チェック項目
- 05トークンのリスクと置き換え例
活用シーン
- 01デザイントークンの色コントラスト確認
- 02フォーカスインジケーターの要件確認
- 03一般的なフレームワークの色トークン特定
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ community-access/ accessibility-agents --skill design-systemインストール後、Claude Code で/design-systemを入力して読み込まれたか確認します。
詳細
デザインシステムアクセシビリティスキル
このスキルでは、デザイントークンのコントラスト検証、フォーカスリングの準拠状況、スペーシングの監査に関する参照データを提供します。design-system-auditor.agent.md で使用されます。
WCAG コントラスト比 - 計算リファレンス
ステップ 1:sRGB チャンネルの線形化
各チャンネル C([0, 255])について:
c = C / 255
c_lin = c / 12.92 if c <= 0.04045
c_lin = ((c + 0.055) / 1.055)^2.4 otherwise
ステップ 2:相対輝度
L = 0.2126 * R_lin + 0.7152 * G_lin + 0.0722 * B_lin
ステップ 3:コントラスト比
ratio = (L_lighter + 0.05) / (L_darker + 0.05)
JavaScript による簡易実装
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 から Hex への変換(CSS 変数トークン用)
多くのデザインシステムでは、色を HSL の三つ組として保存します(例:shadcn/ui、Radix)。
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 コントラストしきい値
| 用途 | AA | AAA | 備考 |
|---|---|---|---|
| 通常のテキスト(< 18pt / < 14pt 太字) | 4.5:1 | 7:1 | ほとんどの本文 |
| 大きなテキスト(>= 18pt / >= 14pt 太字) | 3:1 | 4.5:1 | 見出し、ディスプレイテキスト |
| UI コンポーネント(境界線、アイコン) | 3:1 | - | 入力欄の境界線、アイコンボタン |
| フォーカスインジケーター(WCAG 2.4.13、2.2) | 3:1 | - | 隣接する色に対して |
| プレースホルダーテキスト | 4.5:1 | - | 通常のテキストとして扱われます |
| 無効状態 | 適用除外 | 適用除外 | 文書化された適用除外 |
| ロゴ/ブランド | 適用除外 | 適用除外 | 要件なし |
| 装飾コンテンツ | 適用除外 | 適用除外 | 装飾としてマークする必要があります |
フレームワークトークンのパス - 完全リファレンス
Tailwind CSS
// 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 変数
/* 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+
// 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
// 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 デザイントークン)
{
"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
}
}
}
高リスクなトークンの組み合わせ - 既知の不適合例
| トークンの組み合わせ | 一般的な値 | 白背景での比率 | ステータス | 備考 |
|---|---|---|---|---|
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 | 不適合 | 広く使われているため、必ず確認してください |
適合する置き換え
| 不適合トークン | 置き換え | 新しい比率 | 備考 |
|---|---|---|---|
#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 の設定
npm install --save-dev @storybook/addon-a11y
// .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
}
}
}
};
CI で Storybook の a11y チェックを実行する
# Install storybook test runner
npm install --save-dev @storybook/test-runner
# package.json scripts
{
"scripts": {
"storybook:test": "test-storybook",
"storybook:test:a11y": "test-storybook --ci"
}
}
# Run in CI
npx storybook dev --port 6006 &
npx wait-on tcp:6006
npx test-storybook --ci
WCAG 2.4.13 フォーカス表示要件(AAA、AA の基準を上回る)
WCAG 2.4.13 フォーカス表示(WCAG 2.2 のレベル AAA) - 2.4.7 フォーカスの可視化(AA)の基準を上回りますが、ベストプラクティスとして推奨されます。
- 面積: フォーカスインジケーターはコンポーネントを囲むか、コンポーネントの周囲長 × 2px 以上の周囲長を持つ必要があります
- コントラストの変化: フォーカスインジケーターの面積は、フォーカス時と非フォーカス時の間で 3:1 以上コントラストが変化する必要があります
- 隠されないこと: フォーカスインジケーターが、作成者が追加したコンテンツによって完全に隠されてはなりません
最低限の適合フォーカスリングの実装
/* 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 */
フォーカスリングトークンの検証チェックリスト
| チェック | 要件 | ツール |
|---|---|---|
outline-width >= 2px | WCAG 2.4.13 の面積要件 | CSS 監査 |
| フォーカス色のコントラスト >= 3:1 | 隣接する背景に対して | コントラスト計算ツール |
| フォーカス状態が非フォーカス状態と異なる | 視認できる変化が必要 | 目視検査 |
代替指定のない outline: none がない | 該当なし | grep / CSS 監査 |
| ライトモードとダークモードの両方に存在する | 一貫性 | 目視検査 |
デザイントークンファイルの検出コマンド
# 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/*"
# 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' }
重大度の分類
| 指摘事項 | 重大度 |
|---|---|
| テキストトークンが 3:1 未満 | 重大 |
| テキストトークンが 3:1~4.49:1(通常のテキスト) | エラー |
| テキストトークンが 4.5:1~6.99:1、AAA の目標 | 警告 |
| UI コンポーネントのトークンが 3:1 未満 | エラー |
| フォーカスリングが完全に欠落 | 重大 |
| フォーカスリングが 2px 未満 | エラー |
| フォーカスリングのコントラストが 3:1 未満 | エラー |
| タッチターゲットのトークンが 24 x 24px 未満(WCAG 2.5.8) | エラー |
| タッチターゲットのトークンが 44 x 44px 未満(WCAG 2.5.5) | 警告 |
prefers-reduced-motion のリセットがない | 警告 |
| プレースホルダーの色が 4.5:1 未満 | エラー |
| 無効状態のトークンが 3:1 未満 | 情報(免除が文書化されています。透明性のために注記してください) |
コメント
Markdown 対応