画像アクセシビリティ実務:ALT / コントラスト / テキスト埋め込み 最短チェックリスト
画像アクセシビリティは ALT + 視認性 + 代替手段 の 3 点で初期カバーできます。特別なライブラリ導入より、記述と設計の一貫性
が最速の改善です。
低解像度プレースホルダや背景除去画像の利用時も alt
の意味を主画像と同じに保ち、差し替え“途中”状態で情報欠落させない設計にします。
TL;DR
- 装飾は空alt
- 意味は“画像が果たす目的”を簡潔化
- 色依存UIには非色チャネル表現を併設
1. ALT の最短ルール
- 装飾のみ:
alt=""
(空)+role=presentation
も検討 - 意味画像: 画像が伝える用途/意図 を文章化(視覚と同じ目的到達か)
- 冗長回避: 直後にキャプションが同内容なら
alt=""
2. コントラスト迅速検証
スクリーンショット内文字 / オーバーレイテキストは WCAG AA (4.5:1 / 大きな文字 3:1) を閾値に。色相ブランド固定でも P3 → sRGB 変換
で暗度が変わるため、最終生成画像で測定。
// 簡易 contrast 計算 (相対輝度) function contrast(rgb1, rgb2){ const lum = (r:number,g:number,b:number)=>{ const f=(v:number)=>{v/=255;return v<=0.03928? v/12.92:((v+0.055)/1.055)**2.4}; const [R,G,B]=[f(r),f(g),f(b)]; return 0.2126*R+0.7152*G+0.0722*B; }; const L1=lum(...rgb1),L2=lum(...rgb2); const hi=Math.max(L1,L2),lo=Math.min(L1,L2); return (hi+0.05)/(lo+0.05); }
3. 画像内テキストを減らす
翻訳・テーマ・ダークモード対応を阻害するため、UIテキストは可能な限り HTML / CSS レイヤで表現。どうしても埋め込みたい場合は構造化データで補助説明を付与します。
4. lazy / LCP / INP の両立
- 最初の視覚的セクション画像は
loading="eager"
- 遅延画像は
decoding="async"
+ 適正なintrinsic size
- 超小型プレビューは /placeholders を活用
5. ALT パターン集 (Good vs Avoid)
# 装飾
<img src="..." alt="" role="presentation" />
# 意味 (製品スクリーンショット)
<img src="billing-dashboard.png" alt="請求ダッシュボード 月次集計グラフ" />
# 冗長 (避ける)
<img src="logo.png" alt="Acme ロゴ" /> <!-- 見出し直後に同名テキストあり -->
“画素の見た目” ではなく “利用者が得る情報” を要約。
6. ALT 判断テーブル
- 意味 + テキスト含む: alt = 目的 + 状態 (例: "成功通知: プラン更新完了")
- チャート類: alt = 主要傾向 (例: "月次売上は右肩上がり") + 詳細は隣接表/CSV で補完
- 複雑インフォグラフィック: alt="主要データの概要" + 図下に詳細構造化リスト
- ボタン画像: alt = ボタン機能動詞 (視覚ラベルの単純再掲でOK)
7. コントラスト自動検査スクリプト例
// contrast-scan.ts (サムネ内オーバーレイ文字をOCR無しで推定)
import sharp from 'sharp';
import fs from 'node:fs/promises';
import ArticleNextPrev from '@/components/ArticleNextPrev';
async function scan(file:string){
const img = sharp(file).ensureAlpha();
const { data, info } = await img.raw().toBuffer({ resolveWithObject:true });
// 単純: 高輝度/低輝度クラスタから候補抽出 (K=2 近似)
let sum=[0,0,0,0], sum2=[0,0,0,0], n=0;
for(let i=0;i<data.length;i+=info.channels){
const r=data[i],g=data[i+1],b=data[i+2];
const l = 0.2126*r + 0.7152*g + 0.0722*b;
const bucket = l > 128 ? 1 : 0;
sum[bucket]+=l; sum2[bucket]+=l*l; n++;
}
const m = [sum[0]/(n/2), sum[1]/(n/2)];
const contrast = (Math.max(...m)+0.05)/(Math.min(...m)+0.05);
return { contrast: +(contrast.toFixed(2)), file };
}
(async ()=>{
const files = process.argv.slice(2);
const results = await Promise.all(files.map(scan));
for(const r of results){
if(r.contrast < 4.5) console.warn('LOW_CONTRAST', r.file, r.contrast);
}
})();
単純二値クラスタで概算。誤差許容 → 手動再確認前の粗スクリーニング。
8. LQ プレースホルダ時の注意
- blurhash / gradient など抽象画像は alt を空にせず最終画像と同意図
- 低コントラスト段階で文字を含むなら skeleton + SR-only テキストを重ね情報保持
- INP 影響を避けるため LCP 画像は placeholder 避け直読み (prefetch + priority)
9. テストワークフロー
# alt 空画像検出
npx axe --url http://localhost:3000 | grep 'Image elements do not have [alt]'
# contrast スキャン (閾値4.5)
node scripts/contrast-scan.js public/ogp/*.png
CI で失敗させず warning 集計 → 週次 SLA レポートで改善追跡。
10. トラブルシュート
- 冗長 alt 増加: 直後キャプション重複 → ESLint ルール/カスタム AST で検出。
- 低コントラスト報告: デザインツール値は P3 → 実配信 sRGB 差異。変換後値で再測定。
- INP 劣化: 過剰 observer で遅延読込遅れ → viewport margin 最適化。
Version: 2025-09-08 初版 + 拡張 (パターン/自動検査/テスト)。後続で色覚シミュレーション統合を追記予定。