web-design-guidelines について
Web デザイナーや開発者向けのフレームワーク非依存ガイドです。WCAG 2.2、MDN、最新の Web API を基に、アクセシビリティ、レスポンシブレイアウト、フォーム、タイポグラフィを扱います。
主な機能
- 01WCAG 2.2 のアクセシビリティ実践
- 02セマンティック HTML と ARIA の指針
- 03モバイルファーストのレスポンシブレイアウト
- 04フォームのラベルと検証の指針
- 05タイポグラフィと可読性のルール
活用シーン
- 01操作可能な要素のキーボード対応確認
- 02異なる画面幅に適したページの設計
- 03フォームのラベルとエラーメッセージの改善
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ ehmo/ platform-design-skills --skill web-design-guidelinesインストール後、Claude Code で/web-design-guidelinesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
AGENTS.mdmetadata.jsonrules/_sections.md詳細
Web プラットフォームのデザインガイドライン
アクセシブルで、高速かつレスポンシブな Web インターフェースを実現する、フレームワークに依存しないルールです。WCAG 2.2、MDN Web Docs、最新の Web プラットフォーム API に基づいています。
1. アクセシビリティ / WCAG[重要]
アクセシビリティは任意ではありません。このセクションのルールの大半は、WCAG 2.2 の適合基準レベル A または AA に対応しています。一部のベストプラクティス(本文中に注記があります)は、レベル AAA を対象とするか、WCAG の要件を上回ります。
1.1 セマンティックな HTML 要素を使用する
要素は意図された用途に使用します。セマンティックな構造を使用すると、アクセシビリティ、SEO、リーダーモードへの対応を追加の手間なく実現できます。
| 要素 | 用途 |
|---|---|
<main> | ページの主要コンテンツ(ページごとに1つ) |
<nav> | ナビゲーションブロック |
<header> | 導入コンテンツまたはナビゲーション補助要素 |
<footer> | 最も近いセクショニングコンテンツのフッター |
<article> | 独立して配布できる、自己完結したコンテンツ |
<section> | 見出しを伴うテーマ別のまとまり |
<aside> | 間接的に関連するコンテンツ(サイドバー、補足など) |
<figure> / <figcaption> | イラスト、図、コードリスト |
<details> / <summary> | 展開・折りたたみ可能な開示ウィジェット |
<dialog> | モーダルまたは非モーダルのダイアログボックス |
<time> | 機械可読な日付・時刻 |
<mark> | 強調表示または参照対象のテキスト |
<address> | 最も近い article/body の連絡先情報 |
<main>
<article>
<h1>Article Title</h1>
<p>Content...</p>
</article>
<aside>Related links</aside>
</main>
<div class="main">
<div class="article">
<div class="title">Article Title</div>
<div class="content">Content...</div>
</div>
</div>
アンチパターン:インタラクティブ要素に <div> や <span> を使用することです。<button> がある場合、<div onclick> は決して使用しないでください。
1.2 インタラクティブ要素に ARIA ラベルを付ける
すべてのインタラクティブ要素には、アクセシブルな名前が必要です。表示テキストを優先し、表示テキストだけでは不十分な場合に限り、aria-label または aria-labelledby を使用してください(SC 4.1.2)。
<button aria-label="Close dialog">
<svg aria-hidden="true">...</svg>
</button>
<h2 id="section-title">Notifications</h2>
<ul aria-labelledby="section-title">...</ul>
<button>Save Changes</button>
1.3 キーボード操作
すべてのインタラクティブ要素は、キーボードから移動して操作できる必要があります(SC 2.1.1)。
- ネイティブのインタラクティブ要素(
<button>、<a href>、<input>、<select>)を使用します。これらはデフォルトでキーボード操作に対応しています。 - カスタムウィジェットをタブ順序に含めるには
tabindex="0"を設定し、アクティブ化するための keydown ハンドラーを実装します。 - 0 より大きい値の
tabindexは使用しないでください。 - モーダル内ではフォーカスを閉じ込め、閉じたときにフォーカスを元の位置に戻します。
// Focus trap for modal
dialog.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
const focusable = dialog.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
});
1.4 視認可能なフォーカスインジケーター
視認可能な代替手段を用意せずに、フォーカスアウトラインを削除しないでください(SC 2.4.7、WCAG 2.2 で強化された SC 2.4.11(AA)および SC 2.4.12(AAA))。
/* Good: custom focus indicator */
:focus-visible {
outline: 3px solid var(--focus-color, #4A90D9);
outline-offset: 2px;
}
/* Remove default only when :focus-visible is supported */
:focus:not(:focus-visible) {
outline: none;
}
/* Bad: removing all focus styles */
/* *:focus { outline: none; } */
WCAG 2.2 では、フォーカスインジケーターに、コンポーネントの周囲長に 2px を掛けた値以上の面積と、隣接する色に対する 3:1 以上のコントラスト比が求められます。
1.5 スキップナビゲーションリンク
繰り返し表示されるコンテンツブロックをスキップする仕組みを用意します(SC 2.4.1)。
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<nav>...</nav>
<main id="main-content">...</main>
</body>
.skip-link {
position: absolute;
top: -100%;
left: 0;
z-index: 1000;
padding: 0.75rem 1.5rem;
background: var(--color-primary);
color: var(--color-on-primary);
}
.skip-link:focus {
top: 0;
}
1.6 画像の代替テキスト
すべての <img> に alt 属性を設定する必要があります(SC 1.1.1)。
- 情報を伝える画像:内容と機能を説明します。
alt="第4四半期に売上が倍増したことを示す棒グラフ"。 - 装飾画像:スクリーンリーダーが読み飛ばせるように、
alt=""(空文字列)を設定します。 - 機能を持つ画像(リンクやボタン内の画像):動作を説明します。
alt="検索"。 - 複雑な画像:短い説明を
altに設定し、詳細な説明へのリンクを設けるか、<figcaption>を使用します。
<img src="chart.png" alt="Revenue chart: Q1 $2M, Q2 $2.4M, Q3 $3.1M, Q4 $4.5M">
<img src="decorative-wave.svg" alt="">
1.7 色のコントラスト
最低限のコントラスト比を確保します(SC 1.4.3、1.4.6、1.4.11)。
| コンテンツ | 最低比率 |
|---|---|
| 通常のテキスト(<24px / 太字で <18.66px) | 4.5:1 |
| 大きなテキスト(>=24px / 太字で >=18.66px) | 3:1 |
| UI コンポーネントおよびグラフィックオブジェクト | 3:1 |
情報を伝える際に色だけに頼らないでください(SC 1.4.1)。色とアイコン、テキスト、またはパターンを組み合わせてください。
/* Check contrast of these tokens */
:root {
--text-primary: #1a1a2e; /* on white: ~16:1 */
--text-secondary: #555770; /* on white: ~6.5:1 */
--text-disabled: #767693; /* on white: ~4.5:1, borderline */
}
1.8 フォームのラベル
すべてのフォーム入力には、プログラムから関連付けられたラベルが必要です(SC 1.3.1、3.3.2)。
<label for="email">Email address</label>
<input id="email" type="email" autocomplete="email">
<label>
Email address
<input type="email" autocomplete="email">
</label>
1.9 エラーの特定
エラーをテキストで特定し、説明します(SC 3.3.1)。aria-describedby または aria-errormessage を使用して、エラーメッセージを入力欄に関連付けます。
<label for="email">Email</label>
<input id="email" type="email" aria-describedby="email-error" aria-invalid="true">
<p id="email-error" role="alert">Enter a valid email address, e.g. [email protected]</p>
1.10 ARIA ライブリージョン
動的なコンテンツの変更をスクリーンリーダーに通知します(SC 4.1.3)。
<div aria-live="polite" aria-atomic="true">
3 results found
</div>
<div role="alert">
Your session will expire in 2 minutes.
</div>
<div role="status">
File uploaded successfully.
</div>
デフォルトでは aria-live="polite" を使用します。時間的制約のある警告に限り、role="alert" / aria-live="assertive" を使用してください。
1.11 ARIA ロールのクイックリファレンス
| ロール | 目的 | ネイティブ相当 |
|---|---|---|
button | クリック可能なアクション | <button> |
link | ナビゲーション | <a href> |
tab / tablist / tabpanel | タブインターフェース | なし |
dialog | モーダル | <dialog> |
alert | アサーティブなライブリージョン | なし |
status | ポライトなライブリージョン | <output> |
navigation | ナビゲーションランドマーク | <nav> |
main | メインランドマーク | <main> |
complementary | 補足ランドマーク | <aside> |
search | 検索ランドマーク | <search> (HTML5) |
img | 画像 | <img> |
list / listitem | リスト | <ul>/<li> |
heading | 見出し(aria-level を使用) | <h1>-<h6> |
menu / menuitem | メニューウィジェット | なし |
tree / treeitem | ツリービュー | なし |
grid / row / gridcell | データグリッド | <table> |
progressbar | 進捗状況 | <progress> |
slider | 範囲入力 | <input type="range"> |
switch | 切り替え | <input type="checkbox"> |
ルール:ARIA よりもネイティブ HTML を優先してください。そのパターンに適したネイティブ要素がない場合にのみ ARIA を使用してください。
1.12 名前のラベル(WCAG 2.5.3 レベル A)
インタラクティブ要素に表示テキストがある場合、そのアクセシブルな名前には、その表示テキストが部分文字列として含まれている必要があります(SC 2.5.3)。音声コントロールのユーザー(Dragon NaturallySpeaking、macOS Voice Control)は、コントロールを操作するために表示ラベルを発声します。aria-label が表示テキストを置き換えたり、矛盾したりすると、音声コマンドは機能しません。
<button aria-label="Delete item from cart">Delete</button>
<button>Save Changes</button>
<button aria-label="Close dialog">
<svg aria-hidden="true">...</svg>
</button>
<button aria-label="Remove">Delete</button>
<button aria-label="Proceed to next step">Submit</button>
ルール:表示テキストがある場合、aria-label にはその表示テキストを(大文字と小文字を区別せず、一字一句そのまま)含める必要があります。表示テキストだけで十分な場合は、aria-label を付けないでください。
2. レスポンシブデザイン [CRITICAL]
2.1 モバイルファーストのアプローチ
最小のビューポート向けに基本スタイルを記述します。min-width メディアクエリで段階的に機能を追加します。
/* Base: mobile */
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
/* Tablet */
@media (min-width: 48rem) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Desktop */
@media (min-width: 64rem) {
.grid {
grid-template-columns: repeat(3, 1fr);
}
}
2.2 モダン CSS 関数を使った流動的なレイアウト
ブレークポイントを使わずにサイズを流動的に調整するには、clamp()、min()、max() を使用します。
/* Fluid typography */
h1 {
font-size: clamp(1.75rem, 1.2rem + 2vw, 3rem);
}
/* Fluid spacing */
.section {
padding: clamp(1.5rem, 4vw, 4rem);
}
/* Fluid container */
.container {
width: min(90%, 72rem);
margin-inline: auto;
}
2.3 コンテナクエリ
ビューポートではなく、コンポーネントのコンテナに基づいてサイズを調整します。
.card-container {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
}
}
@container card (min-width: 700px) {
.card {
grid-template-columns: 300px 1fr;
gap: 2rem;
}
}
2.4 コンテンツに基づくブレークポイント
デバイスの画面幅ではなく、コンテンツが崩れる位置にブレークポイントを設定します。一般的な開始点は次のとおりです。
/* Content-based, not "iPhone" or "iPad" */
@media (min-width: 30rem) { /* ~480px: single column gets cramped */ }
@media (min-width: 48rem) { /* ~768px: room for 2 columns */ }
@media (min-width: 64rem) { /* ~1024px: room for sidebar + content */ }
@media (min-width: 80rem) { /* ~1280px: wide multi-column */ }
2.5 タッチターゲット
タッチターゲットの最小サイズは 44x44 CSS ピクセルです(WCAG SC 2.5.5 AAA。SC 2.5.8 が AA で求めるサイズは 24x24px のみです)。隣接するターゲット間には少なくとも 24px の間隔を設けてください。
button, a, input, select, textarea {
min-height: 44px;
min-width: 44px;
}
/* Enlarge tap area without changing visual size */
.icon-button {
position: relative;
width: 24px;
height: 24px;
}
.icon-button::after {
content: "";
position: absolute;
inset: -10px; /* expands clickable area */
}
2.6 ビューポートメタタグ
ドキュメントの <head> に必ず含めてください。
<meta name="viewport" content="width=device-width, initial-scale=1">
maximum-scale=1 や user-scalable=no は使用しないでください。これらはピンチズームのアクセシビリティを損ないます(SC 1.4.4)。
2.7 水平スクロールをなくす
コンテンツは、水平スクロールなしで幅 320px の画面にリフローする必要があります(SC 1.4.10)。
/* Prevent overflow */
img, video, iframe, svg {
max-width: 100%;
height: auto;
}
/* Contain long words/URLs */
.prose {
overflow-wrap: break-word;
}
/* Tables: scroll container, not page */
.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
3. フォーム [HIGH]
3.1 すべての入力にラベルを付ける
すべての入力には、視認でき、プログラムで関連付けられたラベルが必要です。セクション 1.8 を参照してください。
3.2 オートコンプリート属性
ブラウザーの自動入力を有効にするため、一般的なフィールドには autocomplete を使用してください(SC 1.3.5)。
<input type="text" autocomplete="name" name="full-name">
<input type="email" autocomplete="email" name="email">
<input type="tel" autocomplete="tel" name="phone">
<input type="text" autocomplete="street-address" name="address">
<input type="text" autocomplete="postal-code" name="zip">
<input type="text" autocomplete="cc-name" name="card-name">
<input type="text" autocomplete="cc-number" name="card-number">
<input type="password" autocomplete="new-password" name="password">
<input type="password" autocomplete="current-password" name="current-pw">
3.3 適切な入力タイプ
適切なモバイルキーボードとネイティブの入力検証が利用されるよう、正しい type を使用してください。
| タイプ | 用途 |
|---|---|
email | メールアドレス |
tel | 電話番号 |
url | URL |
number | スピナー付きの数値(電話番号、郵便番号、カード番号には使用しない) |
search | 検索フィールド(クリアボタンが表示される) |
date / time / datetime-local | 日時の値 |
password | パスワード(パスワードマネージャーが起動する) |
text with inputmode="numeric" | スピナーなしの数値データ(PIN、郵便番号) |
<input type="tel" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code">
3.4 インライン検証
blur 時に検証してください(キー入力のたびには行わないでください)。成功状態とエラー状態を表示します。
<div class="field" data-state="error">
<label for="username">Username</label>
<input id="username" type="text" aria-describedby="username-hint username-error" aria-invalid="true">
<p id="username-hint" class="hint">3-20 characters, letters and numbers only</p>
<p id="username-error" class="error" role="alert">Username must be at least 3 characters</p>
</div>
.field[data-state="error"] input {
border-color: var(--color-error);
box-shadow: 0 0 0 1px var(--color-error);
}
.field[data-state="error"] .error { display: block; }
.field:not([data-state="error"]) .error { display: none; }
3.5 グループには Fieldset と Legend を使用する
関連する入力を <fieldset> でグループ化し、<legend> でグループにラベルを付けてください。
<fieldset>
<legend>Shipping Address</legend>
<label for="street">Street</label>
<input id="street" type="text" autocomplete="street-address">
</fieldset>
<fieldset>
<legend>Preferred contact method</legend>
<label><input type="radio" name="contact" value="email"> Email</label>
<label><input type="radio" name="contact" value="phone"> Phone</label>
</fieldset>
3.6 必須フィールドの表示
必須フィールドは、視覚的にもプログラム上でも判別できるようにしてください。required 属性と視認できるマーカーを使用します。
<label for="name">
Full name <span aria-hidden="true">*</span>
<span class="sr-only">(required)</span>
</label>
<input id="name" type="text" required autocomplete="name">
大半のフィールドが必須の場合は、任意のフィールドを示してください。
3.7 送信ボタンの状態
送信ボタンは無効にしないでください。代わりに、送信時に検証し、エラーを表示します。
<button type="submit">Create Account</button>
無効なボタンでは、ユーザーが先に進めない理由が伝わりません。無効にする必要がある場合は、目に見える説明を表示してください。
3.8 入力欄の近くに説明を配置する
形式の例、制約、エラーからの回復方法は、ヒントやエラーテキストを使って該当する入力欄の近くに配置してください。導入文で一度だけ要件を説明し、ユーザーが後まで覚えていることを期待してはいけません。
4. タイポグラフィ [HIGH]
4.1 フォントスタック
パフォーマンスを重視する場合はシステムフォントスタックを使用し、ウェブフォントを使用する場合は適切なフォールバックを設定してください。
/* System font stack */
body {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
/* Monospace stack */
code, pre, kbd {
font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;
}
/* Web font with fallbacks and size-adjust */
@font-face {
font-family: "CustomFont";
src: url("/fonts/custom.woff2") format("woff2");
font-display: swap;
font-weight: 100 900;
}
body {
font-family: "CustomFont", system-ui, sans-serif;
}
4.2 相対単位
フォントサイズと余白には rem を使用してください。コンポーネントに対する相対的なサイズには em を使用してください。
html {
font-size: 100%; /* = 16px default, respects user preference */
}
body {
font-size: 1rem; /* 16px */
}
h1 { font-size: 2.5rem; } /* 40px */
h2 { font-size: 2rem; } /* 32px */
h3 { font-size: 1.5rem; } /* 24px */
small { font-size: 0.875rem; } /* 14px */
/* Never: font-size: 16px; (ignores user zoom settings) */
4.3 行の高さと余白
本文の行の高さは少なくとも 1.5(SC 1.4.12)にしてください。段落間の余白はフォントサイズの少なくとも 2 倍にしてください。
body {
line-height: 1.6;
}
h1, h2, h3 {
line-height: 1.2;
}
p + p {
margin-top: 1em;
}
4.4 行の最大長
読みやすさのため、行の長さを約 75 文字に制限してください。
.prose {
max-width: 75ch;
}
/* Or for a content column */
.content {
max-width: 40rem; /* roughly 65-75ch depending on font */
margin-inline: auto;
}
4.5 タイポグラフィの詳細
データには、適切な引用符、ダッシュ、等幅数字を使用してください。
/* Smart quotes */
q { quotes: "\201C" "\201D" "\2018" "\2019"; } /* curly double then single */
/* Tabular numbers for aligned data */
.data-table td {
font-variant-numeric: tabular-nums;
}
/* Oldstyle numbers for running prose (optional) */
.prose {
font-variant-numeric: oldstyle-nums;
}
/* Proper list markers */
ul { list-style-type: disc; }
ol { list-style-type: decimal; }
4.6 見出しの階層
h1 から h6 までを順番に使用してください。階層を飛ばしてはいけません。1 ページにつき h1 は 1 つにしてください。
<h1>Page Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>
<h2>Another Section</h2>
<h1>Page Title</h1>
<h3>Subsection</h3>
階層とは異なる見た目にしたい場合は、CSS クラスを使用してください。
<h2 class="text-lg">Visually smaller but semantically h2</h2>
5. パフォーマンス [HIGH]
5.1 ファーストビューより下の画像を遅延読み込みする
初回読み込み時に表示されない画像には、ネイティブの遅延読み込みを使用してください。
<img src="hero.webp" alt="Hero image" fetchpriority="high" width="1200" height="600">
<img src="feature.webp" alt="Feature image" loading="lazy" width="600" height="400">
5.2 画像の寸法を明示する
レイアウトシフト(CLS)を防ぐため、必ず width と height を指定してください。
<img src="photo.webp" alt="Description" width="800" height="600">
/* Responsive images with aspect ratio preservation */
img {
max-width: 100%;
height: auto;
}
5.3 リソースヒント
サードパーティのオリジンには preconnect を、重要なリソースには preload を使用してください。
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/css/critical.css" as="style">
<link rel="dns-prefetch" href="https://analytics.example.com">
</head>
5.4 コード分割
必要なときにのみ JavaScript を読み込んでください。ルート単位やコンポーネント単位の分割には、動的な import() を使用してください。
// Route-based splitting
const routes = {
'/dashboard': () => import('./pages/dashboard.js'),
'/settings': () => import('./pages/settings.js'),
};
// Interaction-based splitting
button.addEventListener('click', async () => {
const { openEditor } = await import('./editor.js');
openEditor();
});
5.5 長いリストを仮想化する
数百件を超えるリストでは、表示されている行だけをレンダリングしてください。
// Concept: virtual scrolling
// Render only items in viewport + buffer
const visibleStart = Math.floor(scrollTop / itemHeight);
const visibleEnd = visibleStart + Math.ceil(containerHeight / itemHeight);
const buffer = 5;
const renderStart = Math.max(0, visibleStart - buffer);
const renderEnd = Math.min(totalItems, visibleEnd + buffer);
5.6 レイアウトのスラッシングを避ける
DOM の読み取りと書き込みをまとめて実行してください。両者を交互に行ってはいけません。
// Bad: read-write-read-write (forces synchronous layout)
elements.forEach(el => {
const height = el.offsetHeight; // read
el.style.height = height + 10 + 'px'; // write
});
// Good: batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight); // all reads
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px'; // all writes
});
5.7 will-change は控えめに使用する
アニメーションする要素にのみ will-change を適用し、アニメーションの完了後に削除してください。
/* Good: scoped and temporary */
.card:hover {
will-change: transform;
}
.card.animating {
will-change: transform, opacity;
}
/* Bad: blanket will-change */
/* * { will-change: transform; } */
5.8 待機中の状態をすぐに示す
ユーザーの操作後、新しい状態をすぐに知らせてください。処理を短時間で完了できない場合は、インターフェースを変化させないままにせず、進行状況、スケルトン、オプティミスティック UI、または aria-busy によるフィードバックを表示してください。
6. アニメーションとモーション [MEDIUM]
6.1 prefers-reduced-motion を尊重する
必ずモーションを抑えた代替手段を用意してください(SC 2.3.3、レベル AAA)。
/* Define animations normally */
.fade-in {
animation: fadeIn 300ms ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* Remove or reduce for users who prefer it */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
// Check in JavaScript
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
6.2 コンポジターに適したアニメーション
滑らかな 60fps のアニメーションには transform と opacity のみをアニメーションさせてください。これらは GPU のコンポジタースレッドで実行されます。
/* Good: compositor-only properties */
.slide-in {
transition: transform 200ms ease-out, opacity 200ms ease-out;
}
/* Bad: triggers layout/paint */
.slide-in-bad {
transition: left 200ms, width 200ms, height 200ms;
}
6.3 点滅するコンテンツを避ける
コンテンツを 1 秒間に 3 回を超えて点滅させてはいけません(SC 2.3.1)。発作を引き起こす可能性があります。
6.4 状態変化にトランジションを適用する
ホバー、フォーカス、開閉などの状態変化にトランジションを使用し、連続性を保ってください。
.dropdown {
opacity: 0;
transform: translateY(-4px);
transition: opacity 150ms ease-out, transform 150ms ease-out;
pointer-events: none;
}
.dropdown.open {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
6.5 意味のあるモーションのみを使う
アニメーションは、状態を伝えたり、注意を誘導したり、空間的な関係を示したりするものであるべきです。装飾だけを目的としてアニメーションさせてはいけません。
7. ダークモードとテーマ設定 [MEDIUM]
7.1 システム設定を検出する
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f0f17;
--text: #e4e4ef;
--surface: #1c1c2e;
--border: #2e2e44;
}
}
7.2 テーマ設定に CSS カスタムプロパティを使う
すべてのテーマ値をカスタムプロパティとして定義してください。プロパティ値を変更してテーマを切り替えます。
:root {
color-scheme: light dark;
/* Light theme (default) */
--color-bg: #ffffff;
--color-surface: #f5f5f7;
--color-text-primary: #1a1a2e;
--color-text-secondary: #555770;
--color-border: #d1d1e0;
--color-primary: #2563eb;
--color-primary-text: #ffffff;
--color-error: #dc2626;
--color-success: #16a34a;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0f0f17;
--color-surface: #1c1c2e;
--color-text-primary: #e4e4ef;
--color-text-secondary: #a0a0b8;
--color-border: #2e2e44;
--color-primary: #60a5fa;
--color-primary-text: #0f0f17;
--color-error: #f87171;
--color-success: #4ade80;
}
}
7.3 color-scheme メタタグ
ネイティブ UI 要素(スクロールバーやフォームコントロール)で利用できるカラースキームをブラウザーに伝えてください。
<meta name="color-scheme" content="light dark">
7.4 どちらのモードでもコントラストを保つ
ライトモードとダークモードの両方でコントラスト比を確認してください。ダークモードでは、暗い背景上のテキストのコントラストが低くなりがちです。
7.5 画像を適応させる
ライトモードとダークモードのそれぞれに適した画像を用意してください。
<picture>
<source srcset="logo-dark.svg" media="(prefers-color-scheme: dark)">
<img src="logo-light.svg" alt="Company logo">
</picture>
/* Or use CSS filter for simple cases */
@media (prefers-color-scheme: dark) {
.decorative-img {
filter: brightness(0.9) contrast(1.1);
}
}
7.6 prefers-contrast の尊重
@media (prefers-contrast: more) と @media (prefers-contrast: forced) を使用して、ユーザーのコントラスト設定を反映します。prefers-contrast: more は、システム設定の macOS/iOS「コントラストを上げる」に対応します。prefers-contrast: forced は、色を完全に上書きする別個の OS 機能である Windows のハイコントラストモードに対応します。
/* Default theme */
:root {
--color-text: #555770;
--color-border: #d1d1e0;
--color-bg: #ffffff;
}
/* High contrast mode: stronger text and border colors */
@media (prefers-contrast: more) {
:root {
--color-text: #1a1a2e; /* Darker text for higher ratio */
--color-border: #1a1a2e; /* Stronger borders */
--color-bg: #ffffff;
}
/* Ensure interactive elements are clearly delineated */
button, input, select, textarea {
border: 2px solid currentColor;
}
}
/* Forced colors (Windows High Contrast mode) */
@media (prefers-contrast: forced) {
/* Use system color keywords to respect OS color palette */
:root {
--color-text: ButtonText;
--color-bg: ButtonFace;
--color-border: ButtonBorder;
}
}
8. ナビゲーションと状態[MEDIUM]
8.1 URL に状態を反映する
意味のあるビューごとに、一意の URL を用意します。ユーザーがどの状態でもブックマーク、共有、再読み込みできるようにします。
// Update URL without full page reload
function updateFilters(filters) {
const params = new URLSearchParams(filters);
history.pushState(null, '', `?${params}`);
renderResults(filters);
}
// Restore state from URL on load
const params = new URLSearchParams(location.search);
const initialFilters = Object.fromEntries(params);
8.2 ブラウザーの戻る/進む
ブラウザーのナビゲーションに対応するため、popstate を処理します。
window.addEventListener('popstate', () => {
const params = new URLSearchParams(location.search);
renderResults(Object.fromEntries(params));
});
8.3 アクティブなナビゲーション状態
ナビゲーションで現在のページまたはセクションを示します。アクティブなリンクには aria-current="page" を使用します。
<nav aria-label="Main">
<a href="/" aria-current="page">Home</a>
<a href="/products">Products</a>
<a href="/about">About</a>
</nav>
[aria-current="page"] {
font-weight: 700;
border-bottom: 2px solid var(--color-primary);
}
8.4 パンくずリスト
階層が深いサイトでは、パンくずリストを用意します。
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/products">Products</a></li>
<li><a href="/products/widgets" aria-current="page">Widgets</a></li>
</ol>
</nav>
8.5 スクロール位置の復元
SPA のナビゲーションでは、スクロール位置を管理します。
// Disable browser auto-restoration for manual control
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
// Save scroll position before navigation
function saveScrollPosition() {
sessionStorage.setItem(`scroll-${location.pathname}`, window.scrollY);
}
// Restore on back/forward
window.addEventListener('popstate', () => {
const saved = sessionStorage.getItem(`scroll-${location.pathname}`);
if (saved) {
requestAnimationFrame(() => window.scrollTo(0, parseInt(saved)));
}
});
9. タッチとインタラクション[MEDIUM]
9.1 スクロール制御のための touch-action
インタラクティブな要素でジェスチャーの動作を制御するには、touch-action を使用します。
/* Allow only vertical scrolling (disable horizontal pan and pinch-zoom) */
.vertical-scroll {
touch-action: pan-y;
}
/* Carousel: horizontal scroll only */
.carousel {
touch-action: pan-x;
}
/* Canvas/map: disable all browser gestures */
.canvas {
touch-action: none;
}
9.2 タップ時のハイライト
モバイル版 WebKit ブラウザーで、タップ時のハイライトを制御します。
button, a {
-webkit-tap-highlight-color: transparent;
}
9.3 ホバーとフォーカスの同等性
ホバー時のインタラクションはすべて、キーボードフォーカス時にも機能する必要があります。
/* Always pair :hover with :focus-visible */
.card:hover,
.card:focus-visible {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
9.4 ホバーのみのインタラクションを避ける
重要な機能をホバー時にのみ表示しないでください。タッチデバイスにはホバー状態がありません。
/* Bad: content only accessible on hover */
/* .tooltip { display: none; }
.trigger:hover .tooltip { display: block; } */
/* Good: works with focus and click too */
.trigger:hover .tooltip,
.trigger:focus-within .tooltip,
.tooltip:focus-within {
display: block;
}
9.5 カルーセルのスクロールスナップ
カードカルーセルや横方向のリストには、CSS スクロールスナップを使用します。
.carousel {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
gap: 1rem;
scroll-padding: 1rem;
}
.carousel > .slide {
scroll-snap-align: start;
flex: 0 0 min(85%, 400px);
}
10. 国際化[MEDIUM]
10.1 dir 属性と lang 属性
<html> 要素に lang を設定します。ユーザーが生成するコンテンツには dir="auto" を使用します。
<html lang="en" dir="ltr">
<p dir="auto">User-submitted text here</p>
<blockquote lang="ar" dir="rtl">...</blockquote>
10.2 書式設定には Intl API を使用する
ロケールに応じた書式設定には Intl API を使用します。日付や数値の書式をハードコードしないでください。
// Dates
new Intl.DateTimeFormat('en-US', { dateStyle: 'long' }).format(date);
// "January 15, 2026"
// Numbers
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(1234.56);
// "1.234,56 EUR"
// Relative time
new Intl.RelativeTimeFormat('en', { numeric: 'auto' }).format(-1, 'day');
// "yesterday"
// Lists
new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }).format(['a', 'b', 'c']);
// "a, b, and c"
// Plurals
const pr = new Intl.PluralRules('en');
const suffixes = { one: 'st', two: 'nd', few: 'rd', other: 'th' };
function ordinal(n) { return `${n}${suffixes[pr.select(n)]}`; }
10.3 画像内のテキストを避ける
画像内のテキストは翻訳やサイズ変更ができず、スクリーンリーダーでも読み上げられません。スタイル付きのテキストを画像に重ねる必要がある場合は、背景画像と HTML/CSS のテキストを使用します。
10.4 CSS 論理プロパティ
LTR と RTL の両方のレイアウトに対応するには、物理プロパティではなく論理プロパティを使用します。
/* Physical (breaks in RTL) */
/* margin-left: 1rem; padding-right: 2rem; border-left: 1px solid; */
/* Logical (works in LTR and RTL) */
.sidebar {
margin-inline-start: 1rem;
padding-inline-end: 2rem;
border-inline-start: 1px solid var(--color-border);
}
.stack > * + * {
margin-block-start: 1rem;
}
/* Logical shorthands */
.box {
margin-inline: auto; /* left + right */
padding-block: 2rem; /* top + bottom */
inset-inline-start: 0; /* left in LTR, right in RTL */
border-start-start-radius: 8px; /* top-left in LTR, top-right in RTL */
}
| 物理プロパティ | 論理プロパティ |
|---|---|
left / right | inline-start / inline-end |
top / bottom | block-start / block-end |
margin-left | margin-inline-start |
padding-right | padding-inline-end |
border-top-left-radius | border-start-start-radius |
width | inline-size |
height | block-size |
text-align: left | text-align: start |
10.5 RTL レイアウトへの対応
RTL モードでレイアウトをテストします。Flexbox と Grid は、論理プロパティを使用すると RTL に自動的に対応します。
/* This layout works in both LTR and RTL without changes */
.layout {
display: flex;
gap: 1rem;
}
/* Icons that indicate direction need flipping */
[dir="rtl"] .arrow-icon {
transform: scaleX(-1);
}
11. プログレッシブウェブアプリ[MEDIUM]
PWA を使用すると、ウェブアプリをインストールしてオフラインで実行できます。インストール可能なウェブアプリを構築する際は、次のルールに従うことで、一貫性と信頼性のあるエクスペリエンスを実現できます。
11.1 完全なウェブアプリマニフェストを用意する
インストールに必要なすべてのフィールドを含む manifest.json を用意し、<head> からリンクします。フィールドが不足していると、インストールのプロンプトが表示されないままになります。
<link rel="manifest" href="/manifest.json">
{
"name": "My App",
"short_name": "App",
"start_url": "/",
"display": "standalone",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
誤った例:
{
"name": "My App"
// Missing start_url, display, and icons — app is not installable
}
11.2 theme_color と background_color を設定する
theme_color はブラウザーのクロームと OS のタスクスイッチャーに色を付けます。background_color はアプリの読み込み前にスプラッシュ画面を塗りつぶします。どちらもブランドカラーに合わせる必要があります。
{
"theme_color": "#1a73e8",
"background_color": "#ffffff"
}
11.3 オフライン対応のために Service Worker を登録する
インストール可能にし、オフラインで利用できるようにするには、Service Worker が必要です。インストール時に重要なアセットをキャッシュし、オフライン時にはキャッシュから応答します。
// In your main entry point
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
// sw.js — cache on install, serve from cache when offline
const CACHE = 'v1';
const PRECACHE = ['/', '/index.html', '/app.js', '/app.css'];
self.addEventListener('install', e =>
e.waitUntil(caches.open(CACHE).then(c => c.addAll(PRECACHE)))
);
self.addEventListener('fetch', e =>
e.respondWith(
caches.match(e.request).then(hit => hit ?? fetch(e.request))
)
);
11.4 インストール可能性の基準を満たす
ブラウザーのインストールプロンプトを表示するには、アプリを HTTPS 経由で提供し、fetch ハンドラーを持つ Service Worker を登録したうえで、name、icons、start_url、display: standalone(または fullscreen/minimal-ui)を含むマニフェストを用意する必要があります。
11.5 display モードを適切に使用する
| 値 | 使用する場面 |
|---|---|
standalone | アプリがブラウザー UI に置き換わる場合。最も一般的な選択肢です |
fullscreen | 画面全体を必要とするゲームやメディアアプリ |
minimal-ui | 最小限のブラウザーコントロール(戻る、再読み込み)を残す場合 |
browser | インストール動作を行わず、ブラウザーのタブで開く場合 |
評価チェックリスト
Web インターフェースの構築やレビューには、このチェックリストを使用してください。
アクセシビリティ
- すべての画像に適切な
altテキストがある - 色のコントラスト比が、テキストでは 4.5:1、UI コンポーネントでは 3:1 を満たしている
- すべてのインタラクティブ要素をキーボードで操作できる
- フォーカスインジケーターが見える(コントラスト比 3:1、最小周囲幅 2px)
- ナビゲーションをスキップするリンクがある
- フォーム入力に対応するラベルがある
- エラーメッセージが入力項目に関連付けられている
- 動的コンテンツの更新に ARIA ライブリージョンを使用している
- コンテンツが 1 秒あたり 3 回を超えて点滅しない
- ページに適切な見出し階層がある(h1-h6、階層を飛ばさない)
- ランドマークを正しく使用している(main、nav、header、footer)
レスポンシブ対応
- 幅 320px で横スクロールが発生しない
- タッチターゲットが少なくとも 44x44px ある
- viewport メタタグがある(user-scalable=no を指定しない)
- モバイル、タブレット、デスクトップでレイアウトが適切に動作する
- モバイルで拡大しなくてもテキストを読みやすい
フォーム
- すべての入力項目に視認できるラベルがある
- 一般的なフィールドに autocomplete 属性が設定されている
- 適切な入力タイプによって、モバイルで適切なキーボードが表示される
- エラーメッセージが明確で具体的である
- 必須フィールドが示されている
- 送信ボタンが無効化されていない
パフォーマンス
- ファーストビューより下の画像に
loading="lazy"を使用している - 画像に明示的な
widthとheightが設定されている - 重要なフォントがプリロードされている
- サードパーティのオリジンに
preconnectを使用している - 大きな JS バンドルをコード分割している
モーションとテーマ
-
prefers-reduced-motionを尊重している - アニメーションには
transformとopacityのみを使用している - ダークモードでもコントラスト比が維持されている
-
color-schemeメタタグがある - テーマに CSS カスタムプロパティを使用している
-
prefers-contrast: moreでテキストと境界線のコントラストが強くなる -
prefers-contrast: forcedでシステムカラーキーワードを使用している
国際化
-
<html>にlang属性がある - CSS の論理プロパティを使用している(物理プロパティは使用しない)
- Intl API で日付と数値をフォーマットしている
- 画像にテキストを埋め込んでいない
- RTL モードでレイアウトをテストしている
プログレッシブウェブアプリ
-
name、icons、start_url、displayを含む Web App Manifest が<head>からリンクされている -
theme_colorとbackground_colorがブランドのカラーパレットと一致している - オフライン対応のために、
fetchハンドラーを備えたサービスワーカーが登録されている - アプリが HTTPS で配信されている
よくあるアンチパターン
| アンチパターン | 修正方法 |
|---|---|
<div onclick="..."> | <button> を使用する |
置き換えを指定せずに outline: none を設定する | カスタムアウトラインを指定した :focus-visible を使用する |
placeholder をラベルとして使う | <label> 要素を追加する |
tabindex="5" | tabindex="0" または自然な順序を使用する |
user-scalable=no | 削除する |
font-size: 12px | font-size: 0.75rem を使用する |
width/height/top/left をアニメーションさせる | transform と opacity をアニメーションさせる |
| 送信ボタンを無効化する | 送信時に検証し、エラーを表示する |
| 状態を色だけで示す | アイコン、テキスト、またはパターンを追加する |
margin-left / padding-right | margin-inline-start / padding-inline-end を使用する |
寸法のない <img> | width と height 属性を追加する |
| ホバーのみで開示する | :focus-within とクリックハンドラーを追加する |