処理中・データ取得中であることをユーザーに伝えるUIパターン全体を指す設計概念。具体的な実装として Spinner(回転アニメーション)・Skeleton(コンテンツ形状のプレースホルダー)・Progress Bar(進捗バー) の3種類があり、それぞれ異なる用途に使われる。
この記事を読むと、3種類のローディングUIの使い分け・知覚的な待機時間の最適化(遅延表示・アニメーション速度)・aria-busy と aria-live の正しい実装が自分でできるようになります。
1. UI例(Preview / Live)
実装で見る(GunjoUI)
この部品を、デザインシステム GUNJO の実装で確かめられます。
2. 定義(Definition)
処理中・データ取得中であることをユーザーに伝えるUIパターン全体。単体のコンポーネントではなく、Spinner・Skeleton・Progress Bar・オーバーレイ の4つの実装パターンを状況に応じて使い分ける設計概念。
3種類のローディングUIの使い分け:
| 種類 | 進捗計測 | 形状保持 | 使い所 |
|---|---|---|---|
| Spinner | ✗ | ✗ | 短い不定時間の処理(送信・認証) |
| Skeleton | ✗ | ✅ | コンテンツ一覧・カード・プロフィールの読み込み |
| Progress Bar | ✅ | ✗ | ファイルアップロード・インストール・長い処理 |
| オーバーレイ | ✗ | — | 既存コンテンツを操作不可にしながら処理 |
3. 使い分け(When to use / When NOT to use)
3.1 When to use(種類別)
- Spinner:送信ボタンの処理中・認証中・数秒以内の短い不定時間の待機
- Skeleton:リスト・カード・フィード・テーブルの初回データ取得
- Progress Bar:ファイルアップロード・ダウンロード・インストール・進捗が計測できる長い処理
- オーバーレイ:フォーム送信中に背後のフォームを操作不能にしたい場合
3.2 When NOT to use
- 100ms以下の処理:Spinnerが一瞬点滅して逆にUIが不安定に見える → 表示しない
- ページ全体を覆うSpinner:Skeletonで形状を保てる場合は Skeleton の方がユーザー体験が良い
- 進捗が計測できないのにProgress Bar:偽の進捗(フェイクプログレス)はユーザーの信頼を損なう
3.3 代替UI(Alternatives)
- 処理完了後の結果 →
Alert(成功/エラー)/Toast - データが空の時 →
Empty State - 個別コンポーネントの詳細 →
Spinner・Progress Bar
4. 設計判断の核(Decision Principles)
Loadingの核は「待機時間の知覚最適化」——実際の処理時間は変えられなくても、表示方法次第でユーザーの体感待機時間を大幅に短くできる。
判断の優先順位:① 処理時間の分類(100ms / 1秒 / 10秒)→ ② 進捗計測可否 → ③ 形状保持可否 → ④ aria-busy 設定
- 100ms以下はローディングを表示しない:表示と消去の点滅がかえってUIを不安定に見せる。
setTimeoutで100〜200ms 遅延させてからローディングを表示し、それ以前に処理が完了した場合は表示しない - Skeletonはコンテンツの形状を保つことで不安を減らす:真っ白なSpinnerより「こういうコンテンツが来る」という予告があると体感待ち時間が短くなる。UIの形状が決まっているリスト・カード・プロフィールには必ずSkeletonを使う
aria-busy="true"を処理中の要素に付与する:スクリーンリーダーがaria-busy="true"の要素を読み上げる時に「読み込み中」と伝えてくれる。ボタンの場合はdisabled+aria-busy="true"を組み合わせる- アニメーション速度は1〜2秒サイクルが最適:速すぎるSpinnerは焦りを与え、遅すぎると止まっているように見える。CSSの
animation-duration: 0.8s〜1.2sが自然に感じられる範囲
5. 状態設計(States)
5.1 主な状態遷移
- Idle(待機) → Loading(処理中) → Success / Error(完了)
- Loading が長引く場合:Loading → Timeout / Retry(タイムアウト)
5.2 遅延表示のパターン
| 処理時間 | 表示するもの |
|---|---|
| 〜100ms | 何も表示しない |
| 100ms〜1秒 | Skeleton または インラインSpinner |
| 1秒〜10秒 | Spinner + テキスト(「読み込み中...」) |
| 10秒以上 | Progress Bar + キャンセルボタン |
6. バリエーション設計(Variants)
| バリアント | 特徴 | 使用例 |
|---|---|---|
| インラインSpinner | ボタン・入力欄内に小型Spinnerを埋め込む | 送信ボタン・検索フィールド |
| ページSpinner | ページまたはセクション全体の中央に表示 | 初期ページロード |
| Skeleton | コンテンツ形状のグレープレースホルダー | カード・リスト・プロフィール |
| Progress Bar | 進捗率を数値と棒グラフで表示 | ファイル操作・インストール |
| オーバーレイ | 半透明背景 + 中央Spinner | フォーム送信・保存処理 |
禁止パターン:進捗が計測できないのに 0% → 100% のフェイクプログレスを実装する → ユーザーが偽の進捗に気づいた時の信頼低下が大きい。不定時間の処理はSpinner / Skeletonを使う。
7. パターン集(Good / Bad / How to fix)
7.0 よく崩れる設計パターン(3つ)
- 100ms以下の処理にSpinnerを表示:高速な操作のたびにSpinnerが一瞬点滅し、UIが不安定に見える
- Skeletonの形状がコンテンツと全く異なる:Skeleton が単なる横線3本で、実際に表示されるのは複雑なカードグリッドという場合、コンテンツ出現時の「ずれ」がUXを損なう
- ローディング完了後もスクリーンリーダーに伝えない:
aria-busy="false"に戻すかaria-liveでコンテンツが更新されたことを伝えないと、スクリーンリーダーユーザーがコンテンツの読み込み完了を認識できない
7.1 Bad(典型3つ)
onClick={() => setLoading(true)}を即時実行し、100ms以下で完了する処理にもSpinnerが表示される- ページ全体に
position: fixedのSpinnerオーバーレイを表示し、実際のコンテンツ形状が全く見えない - 進捗が計測できないAPI呼び出しに Progress Bar を使い、
0%→30%→30%(止まる)→100%のような偽の進捗を表示
7.2 Good(対になる3つ)
useEffectでsetTimeout(200ms)の遅延タイマーを設定し、200ms以内に処理が完了した場合はSpinnerを表示しない(点滅防止)- Skeletonの各プレースホルダーを実際のコンテンツの形状(アバター円・タイトル行・説明文行)に合わせてデザインし、コンテンツ出現時のレイアウトシフトを最小化する
- コンテンツ読み込み完了後に
aria-busy="false"に戻し、読み込んだコンテンツのコンテナにaria-live="polite"を付与して更新を通知する
7.3 How to fix(手順)
- 処理時間を計測し100ms / 1秒 / 10秒で分類する
useState+useEffectで遅延表示を実装:const [showLoading, setShowLoading] = useState(false)→useEffect(() => { const t = setTimeout(() => setShowLoading(true), 200); return () => clearTimeout(t); }, [isLoading])aria-busy={isLoading}を処理中の要素に付与する- Skeletonを使う場合は実際のコンテンツレイアウトに合わせて形状を設計する
8. ルール(Must / Better)
Must(守らないと壊れる)
Better(品質が跳ねる)
9. アクセシビリティ要件(必須)
Screen Reader
<!-- ボタンのローディング -->
<button disabled aria-busy="true">
<span aria-hidden="true">⟳</span>
処理中...
</button>
<!-- セクションのローディング -->
<div aria-busy="true" aria-label="コンテンツを読み込み中">
<!-- Skeleton または Spinner -->
</div>
<!-- ページレベルのローディング -->
<main aria-busy="true">
<!-- コンテンツ -->
</main>
Contrast / Readability
- Spinnerの色と背景のコントラストは3:1以上(大きな非テキスト要素の基準)
prefers-reduced-motionメディアクエリでanimation: noneにしてアニメーションを無効化するオプションを提供する
@media (prefers-reduced-motion: reduce) {
.spinner {
animation: none;
opacity: 0.5;
}
}
10. 実装メモ(Implementation Notes)
- shadcn/ui には
Skeletonコンポーネントがある(animate-pulseを使った実装)。Spinnerは shadcn/ui にないため、Tailwind のanimate-spin+border-t-[color]で実装するか、lucide-reactのLoader2アイコンを使う - 遅延表示の実装は
useDeferredValue(React 18)またはsetTimeout+useEffectで行う。useDeferredValueは Concurrent Mode での非同期レンダリングと組み合わせる際に有効 prefers-reduced-motionの対応は Tailwind のmotion-reduce:プレフィックスで実装できる:className="animate-spin motion-reduce:animate-none"
11. 関連リンク
- 関連するUIデザイン原則: フィードバック (Feedback), 状態の可視化 (Visibility of System Status)
- 用語集(定義): アクセシビリティ (Accessibility), フィードバック (Feedback)
- 関連するUIコンポーネント(横): Spinner(スピナー), Progress Bar(プログレスバー), Skeleton(スケルトン), Empty State(空状態)
12. まとめ
Loadingの設計で最重要なのは「処理時間の分類と適切なUIパターンの選択」です。迷ったら 4. 設計判断の核 に戻り、「100ms以下は非表示か」「進捗が計測できるかどうかでSpinner/Progress Barを選んでいるか」「コンテンツ形状が決まっているならSkeletonを使っているか」の3点を確認してください。aria-busy="true" の付与はアクセシビリティの最低ラインとして必ず実装してください。