web-ui-best-practices について
開発者向けの Web UI 設計スキルです。操作速度、アクセシビリティ、ナビゲーション、状態の永続化、最新の CSS を実践的な指針とともに扱います。
主な機能
- 01素早い操作フィードバック
- 02レスポンシブ CSS パターン
- 03再開可能なユーザー状態
- 04ナビゲーションと URL の指針
- 05アクセシビリティと入力の指針
活用シーン
- 01Web インターフェースのレビュー
- 02レスポンシブコンポーネントの設計
- 03フォームやナビゲーションの改善
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jamditis/ claude-skills-journalism --skill web-ui-best-practicesインストール後、Claude Code で/web-ui-best-practicesを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yaml
詳細
Web UI のベストプラクティス
速く、意図が明確で、ユーザーの時間を尊重するウェブインターフェースを構築するための原則です。ここにあるルールはどれも目安です。理由があれば破っても問題ありませんが、いくつも破っているなら UI を見直す必要があります。
速度
あらゆる操作は 100ms 未満で完了します。それが難しい場合は、即座に完了したように見せます。
- 楽観的 UI 更新を行い、サーバーの確認を待たずに結果を表示します
- 入力はデバウンスしますが、体感上の応答は決して遅らせません
- 次に表示される可能性の高いルートを、ホバー時またはビューポートに入った時点でプリフェッチします
- アニメーションには
will-changeとtransformを使い、top/leftは使いません - 感覚ではなく
performance.now()で計測します
// Optimistic delete, remove from UI immediately, reconcile later
async function handleDelete(id) {
setItems(prev => prev.filter(i => i.id !== id));
try {
await api.delete(`/items/${id}`);
} catch {
setItems(prev => [...prev, originalItem]);
toast("Couldn't delete. Restored.");
}
}
スケルトンローディング状態
表示される内容の形が分かっている場合、スピナーは表示しません。レイアウトに合ったスケルトンを表示し、実際のコンテンツに切り替えます。
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
モダン CSS ツールキット
2023 年から 2026 年にかけて成熟した 4 つの機能により、JavaScript を使わずにコンポーネント単位のレスポンシブレイアウトや SPA のような画面遷移を実現できます。フレームワークを追加する前に、これらを活用してください。
コンテナクエリ
コンテナクエリを使うと、コンポーネントはビューポートではなく、そのコンテナのサイズに応じて表示を変えられます。同じカードを、ページ単位でメディアクエリを調整することなく、幅 300px のサイドバーにも幅 900px のメインカラムにも表示できます。
.card-list {
container-type: inline-size;
container-name: cards;
}
@container cards (min-width: 480px) {
.card { display: grid; grid-template-columns: 120px 1fr; }
}
2023 年以降、主要なすべてのブラウザーで安定して動作します。「同じコンポーネントを 2 か所で使うときに見た目を変える必要がある」という場合のハックの大半を置き換えられます。
:has() 親セレクター
:has() を使うと、子孫要素に基づいて親要素自身のスタイルを設定できます。長らく求められていた「親セレクター」です。JavaScript を使わずに、フォームフィールドをエラー状態にしたり、画像が添付されたカードを示したり、フォーカスされた入力欄を含む行を示したりする場合に便利です。
/* Highlight a form group when its input has focus */
.form-group:has(input:focus) {
outline: 2px solid var(--color-primary);
}
/* Add bottom margin to articles that contain a figure */
article:has(figure) {
margin-bottom: 2rem;
}
2023 年後半以降、Chrome、Safari、Firefox で安定して動作します。JavaScript によるクラス切り替えが必要だった具体的な場面を減らせます。
ビュートランジション
View Transitions API を使うと、フレームワークなしで、2 つの DOM 状態間のアニメーション(ルート変更、モーダルの開閉、リスト項目の入れ替え)を実行できます。ブラウザーが以前の状態をスナップショットし、新しい状態に切り替えた後、クロスフェードまたはスライドでつなぎます。
// Same-document transition (Chrome 111+, Safari TP, Firefox behind a flag)
function navigate(newView) {
if (!document.startViewTransition) {
renderView(newView);
return;
}
document.startViewTransition(() => renderView(newView));
}
/* Smooth crossfade by default; override per element */
::view-transition-old(*) { animation-duration: 200ms; }
::view-transition-new(*) { animation-duration: 200ms; }
ドキュメント間のビュートランジション(ページ全体のナビゲーション間)は 2024 年に Chrome 126 で導入され、MPA を SPA のように感じさせることができます。動きに敏感なユーザーにアニメーションではなく瞬時の切り替えを提供できるよう、prefers-reduced-motion と組み合わせてください。
スクロール駆動アニメーション
animation-timeline: scroll() と animation-timeline: view() を使うと、時計の経過時間ではなくスクロール位置に応じて CSS アニメーションを動かせます。典型的な用途は、スクロールに合わせて進捗状況を表示する、記事上部のインジケーターです。
@keyframes fill { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.read-progress {
position: fixed; top: 0; left: 0; right: 0; height: 3px;
background: var(--color-primary);
transform-origin: left;
animation: fill linear;
animation-timeline: scroll(root);
}
Chromium ベースのブラウザー(Chrome 115 以降、Edge)では安定して動作しますが、2026-05 時点では Safari と Firefox ではまだ利用できません。プログレッシブエンハンスメントとして使い、JavaScript のフォールバックを用意するか、ほかのブラウザーでは控えめな表示にします。
プロダクトツアーをなくす
UI の説明にツアーが必要なら、UI に問題があります。代わりに、次の方法を使います。
- 実際の操作を通じて学べる空の状態(「最初のプロジェクトを作成」など)
- プログレッシブディスクロージャーを使い、機能が必要になったときに表示します
- 初回利用後に消えるインラインヒント
- 設定なしで使えるデフォルト値
URL
スラッグは短く、読みやすく、人が推測しやすいものにします。UUID やクエリパラメーターだらけの URL は使いません。
Good: /projects/weather-app
/settings/billing
/docs/api/auth
Bad: /projects/550e8400-e29b-41d4-a716-446655440000
/app?view=settings&tab=billing&subsection=plan
/dashboard#!/module/documents/list?filter=active
- ユーザーが入力した名前をもとにスラッグを作成します
- 階層は最大 3 セグメントにします
- URL はコピーや共有ができるようにします。URL はプロダクトの記憶です
永続化され、再開できる状態
ユーザーは離れた後、また戻ってきます。それを考慮してください。
- フォームの下書き状態を
localStorageまたはサーバーに保存します - 戻る操作時にスクロール位置を復元します
- フィルターや並べ替えの選択状態をセッションをまたいで保持します
- URL に現在の表示状態をエンコードし、URL を共有すると同じ表示を再現できるようにします
// Persist form state across sessions
function usePersistentForm(key, defaults) {
const [state, setState] = useState(() => {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : defaults;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(state));
}, [key, state]);
return [state, setState];
}
色を控えめに
色は 3 色までにします。メインカラー、アクセントカラー、危険・破壊的操作用の色を 1 色ずつにします。それ以外はすべてグレーの濃淡にします。
:root {
--color-primary: #2563eb;
--color-accent: #f59e0b;
--color-danger: #ef4444;
--gray-50: #fafafa;
--gray-100: #f4f4f5;
--gray-200: #e4e4e7;
--gray-400: #a1a1aa;
--gray-600: #52525b;
--gray-900: #18181b;
}
- 新しい色相ではなく、不透明度や明度で階層を作ります
- ダークモードでは、同じ 3 色を使い、グレーの明暗を反転させます
- 4 色目が必要になったら、弱いレイアウトを色で補おうとしている可能性があります
スクロールバーを表示しない
ユーザーがスクロールしている間だけ表示します。コンテンツが無限に続くように感じられ、閉じ込められた印象を与えません。
/* Hide scrollbar across browsers */
.scroll-container {
overflow-y: auto;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
.scroll-container::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
スクロールシャドウを使って、余計な装飾なしで続きがあることを示します。
.scroll-shadow {
background:
linear-gradient(white 30%, transparent),
linear-gradient(transparent, white 70%) 0 100%,
radial-gradient(farthest-side at 50% 0, rgba(0,0,0,.15), transparent),
radial-gradient(farthest-side at 50% 100%, rgba(0,0,0,.15), transparent) 0 100%;
background-repeat: no-repeat;
background-size: 100% 40px, 100% 40px, 100% 12px, 100% 12px;
background-attachment: local, local, scroll, scroll;
}
ナビゲーションの階層
どこからでも、ナビゲーションは3ステップ以内にします。目的地にたどり着くまでに3回以上クリックする必要がある場合は、階層をフラットにします。
- パンくずリストは階層を示すために使い、ナビゲーションには使わない
- グローバルナビゲーションは常に表示し、デスクトップではハンバーガーメニューの中に隠さない
- パワーユーザー向けの抜け道として、
Cmd+K/Ctrl+Kを使う
コマンドパレット(Cmd+K)
ページが複数あるアプリには、コマンドパレットが必要です。
// Minimal Cmd+K listener
useEffect(() => {
function handleKeyDown(e) {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
setCommandPaletteOpen(true);
}
}
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, []);
パレットはシンプルに保ちます:
- ページ名、最近の操作、設定をあいまい検索する
- キーボードショートカットをインラインで表示する
- 最近使った項目を先に表示する
- コマンドが20個以上になるまでは、カテゴリを設けない
クリップボード
ユーザーが期待する場所では、どこでもコピーと貼り付けが使えるようにします。
- コード、URL、API キー、IDはワンクリックでコピーできるようにする
- ファイルのアップロード欄や画像フィールドにクリップボードから貼り付けられるようにする
- コピー時に短い確認(「コピーしました!」)を表示し、自動的に消す
async function copyToClipboard(text, label = "Copied") {
await navigator.clipboard.writeText(text);
toast(label, { duration: 1500 });
}
タップ領域
ボタンや入力欄のタップ領域は大きくします。WCAG 2.2(成功基準 2.5.8)では、最低サイズを24×24 CSS ピクセルと定めています。Apple のヒューマンインターフェイスガイドラインや、iOS/Android のネイティブアプリで一般的な規約では、使いやすいサイズとして44×44 ポイントが推奨されています。主要な操作には44pxを実用上の最小サイズとして使い、二次的なコントロール(密度の高いテーブル行のアイコンなど)には、絶対的な最低基準である24pxを使います。
button, .btn, [role="button"] {
min-height: 44px;
min-width: 44px;
padding: 10px 20px;
}
input, select, textarea {
min-height: 44px;
padding: 10px 12px;
font-size: 16px; /* Prevents iOS Safari zoom on focus */
}
- 隣り合うクリック可能な要素の間には、少なくとも8pxの間隔を設ける
- アイコンのみのボタンには、ラベル付きボタンより大きなパディングを設定する
- 重要な操作の手がかりをホバー状態だけに頼らない。タッチ操作では使えないためです
誠実なキャンセル
ワンクリックでキャンセルできるようにします。罪悪感をあおったり、ダークパターンを使ったり、「本当に損をしてもよいですか?」と聞いたりしません。
- 確定ボタンの横に、キャンセルボタンを常に表示する
- アカウント削除は、一度で完了できるようにする
- 配信停止はワンクリックでできるようにし、設定画面を挟まない
- ダウングレードにサポートへの問い合わせを求めない
ツールチップ
できる限り使いません。ツールチップは、UIだけでは説明できないことの表れです。
- アイコンのみのボタンに限って使う(ラベルを補うため)
- すでに読めるテキストには使わない
- すぐに表示せず、ホバーしてから300ms後に表示する
- スクロールしたら閉じる
- 必須情報にツールチップを使わない
コピー文
能動態にします。1文は最大7語にします。法的文書ではなく、人に話しかけるように書きます。
Good: "Project created"
"Saved 2 minutes ago"
"Delete this file?"
Bad: "Your project has been successfully created!"
"Changes were last saved approximately 2 minutes ago"
"Are you sure you want to permanently delete this file? This action cannot be undone."
- ボタンには「保存」「削除」「送信」のような動詞を使い、「送信する」「OK」「確認」は使わない
- エラーメッセージでは、何が起きたかと次にすべきことを伝える
- ユーザーのせいにしない(「入力が無効です」ではなく「有効なメールアドレスを入力してください」)
- UIのコピーはすべてセンテンスケースにし、タイトルケースは使わない
視覚的な位置合わせ
幾何学的な位置合わせより、視覚的な位置合わせを優先します。目に映るのはピクセルではなく、重みです。
- 再生アイコンは円の中で2~3px右にずらし、中央に見えるようにする
- 先頭が大文字のテキストは、バウンディングボックスの左端より視覚的に左にそろえる
- テキストの横に置くアイコンは、グリフに応じて上下に1~2pxずらす
- テキストの周囲のパディングは、数学的な均等さではなく視覚的なバランスを取る。下のパディングは上より1~2px大きくすることがよくあります
/* Geometric center ≠ optical center */
.play-button svg {
transform: translateX(2px);
}
/* Visually balanced card padding */
.card {
padding: 20px 24px 22px 24px;
}
左から右への読み進め方
左から右への読み進め方と、F字型の視線走査に最適化します。
- 最も重要なコンテンツは左上の領域に置く
- 主要な操作は右側に置く(視線は行の終わりに向かうため)
- ラベルは入力欄の横ではなく上に置く
- テーブルでは、最もよく見られる列を左端に置く
- 本文を中央揃えにしない。1行の見出し以外はすべて左揃えにする
データ損失への不安を和らげる
ユーザーは作業内容を失うことを恐れます。防止策を講じ、保存されたことを示します。
- 自動保存を行い、「保存済み」の表示とタイムスタンプを見えるようにする
- 破壊的な操作の後に元に戻せるようにする(完全削除ではなくソフト削除)
- ページを離れるときに「未保存の変更があります」と表示する
- ツイートより長いコンテンツには、バージョン履歴を用意する
- 確認が必要なのは取り消せない操作だけにし、日常的な操作では求めない
// Warn on unsaved changes
useEffect(() => {
function handleBeforeUnload(e) {
if (hasUnsavedChanges) {
e.preventDefault();
e.returnValue = "";
}
}
window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [hasUnsavedChanges]);
ダウンロード可能なブランド素材
ダウンロード可能な SVG ロゴとブランドキットを掲載した /brand または /press ページを用意してください。ロゴをスクリーンショットさせないでください。
- 背景が透明な SVG ロゴ
- カラーコード(hex、RGB、HSL)
- フォント名とウェイト
- 使用ガイドライン(最小サイズ、クリアスペース、禁止事項)
- ワンクリックで ZIP をダウンロード
チェックリスト
Web UI をレビューするときは、次の項目を確認してください。
- すべての操作が 100ms 以内に完了する(または楽観的に処理される)
- プロダクトツアーやオンボーディングモーダルがない
- URL が短く読みやすく、UUID を含まない
- 状態がセッション間やページ再読み込み後も保持される
- 色は最大 3 色(グレーを除く)
- 操作していないときにスクロールバーが表示されない
- すべての遷移先に 3 ステップ以内で到達できる
- SVG ロゴとブランドキットをダウンロードできる
- スピナーではなくスケルトンローダーを使っている
- コード、キー、URL をクリップボードにコピーできる
- タッチターゲットの最小サイズが 44px
- キャンセルが誠実で、ワンクリックで完了する
- Cmd+K のコマンドパレットがある
- ツールチップはアイコンのみのボタンに限る
- コピーは能動態で、7 語以内
- 幾何学的ではなく、視覚的に整列している
- コンテンツが左から右への F 字型パターンに沿っている
- 状態が見える自動保存と元に戻す機能がある