ローディングオーバーレイ(Loading Overlay)— 全画面 / 要素内

表示・インジケーター 初級

このコンポーネントについて

ローディングオーバーレイは、フォーム送信やデータ更新などの処理中に半透明の背景とスピナーで画面や特定要素をブロックするUIパターンです。スケルトンローディングが「初期データ取得中の表示」を目的とするのに対し、ローディングオーバーレイは「ユーザー操作をトリガーとした処理中の二重送信・誤操作防止」が主目的です。

全画面をブロックするパターンと、テーブルやカードなど特定要素内だけをブロックするパターンの2種類があり、用途に応じて使い分けます。CSSアニメーションのみで実装でき、外部ライブラリは不要です。

  • Pattern 1 — 全画面オーバーレイposition: fixed; inset: 0 で画面全体をブロック。フォーム送信・CSV出力など全操作を停止させたい場面に使う
  • Pattern 2 — 要素内オーバーレイposition: absolute; inset: 0 で特定要素内だけをブロック。テーブル更新・部分リフレッシュに使う
  • CSSスピナーborder-radius: 50%rotate アニメーションのみで実装。外部ライブラリ不要
  • スピナー切り替え — サンプルCSSのコメントアウトを差し替えるだけでドット点滅スピナーに変更可能

実装のポイント・注意点

全画面オーバーレイは position: fixed; inset: 0; z-index: 9999 で実装します。inset: 0top: 0; right: 0; bottom: 0; left: 0 の省略形で、ビューポート全体を覆います。z-index: 9999 はモーダルなど他の重なり要素と競合する場合は適宜調整してください。

要素内オーバーレイには2つのCSSプロパティが必ず必要です。親要素に position: relative(子の position: absolute の基準点を作るため)と overflow: hidden(オーバーレイが角丸を突き抜けるのを防ぐため)のセットで設定してください。どちらか一方でも欠けると意図した表示になりません。

スピナーのビジュアルは背景色に合わせて変えます。全画面(暗い背景)では白スピナー、要素内(明るい背景)では青スピナーを使うと視認性が上がります。--spinner-light / --spinner-dark のCSS変数を変更するだけで色を調整できます。

hidden 属性はデフォルトで display: none を適用しますが、要素に display: flex が指定されていると上書きされてしまいます。サンプルCSSに含まれる [hidden] { display: none !important; } の一行がこれを防いでいます。

スクリーンリーダー対応として、オーバーレイ要素に aria-live="polite"aria-label="処理中" を設定しています。オーバーレイが表示(hidden 属性を除去)されたタイミングで「処理中」とスクリーンリーダーが読み上げます。

HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。

デモ

Pattern 1 — 全画面オーバーレイ

Pattern 2 — 要素内オーバーレイ

氏名部署ステータス

サンプルソース

3つのファイルを同じフォルダに保存し、index.html をブラウザで開くとすぐに動作確認できます。
ファイル名:index.html / style.css / script.js — 保存時の文字コードは UTF-8 を指定してください(Shift-JISだと日本語が文字化けします)。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ローディングオーバーレイ サンプル</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>

<!-- Pattern 1: 全画面オーバーレイ -->
<section class="demo-section">
  <h2 class="pattern-label">Pattern 1 — 全画面オーバーレイ</h2>
  <form class="demo-form" id="my-form" onsubmit="return false;">
    <div class="form-row">
      <label for="name">氏名</label>
      <input type="text" id="name" value="田中 太郎">
    </div>
    <div class="form-row">
      <label for="email">メールアドレス</label>
      <input type="email" id="email" value="[email protected]">
    </div>
    <p class="success-msg" id="success-msg" hidden>送信が完了しました</p>
  </form>
  <div class="controls">
    <button id="p1-btn" onclick="startFullOverlay()">送信する</button>
    <button onclick="resetP1()">リセット</button>
  </div>
</section>

<!-- Pattern 2: 要素内オーバーレイ -->
<section class="demo-section">
  <h2 class="pattern-label">Pattern 2 — 要素内オーバーレイ</h2>
  <div class="table-wrap" id="table-wrap">
    <table class="data-table">
      <thead>
        <tr><th>氏名</th><th>部署</th><th>ステータス</th></tr>
      </thead>
      <tbody id="tbody"></tbody>
    </table>
    <!-- 要素内オーバーレイ(初期非表示)-->
    <div class="ov-inner" id="ov-inner" hidden aria-live="polite" aria-label="処理中">
      <div class="spinner spinner--dark">
        <span></span><span></span><span></span>
      </div>
      <p class="ov-label ov-label--dark">処理中...</p>
    </div>
  </div>
  <div class="controls">
    <button id="p2-btn" onclick="startInnerOverlay()">データを更新する</button>
    <button onclick="resetP2()">リセット</button>
  </div>
</section>

<!-- 全画面オーバーレイ(position:fixed のため body 直下に配置)-->
<div class="ov-full" id="ov-full" hidden aria-live="polite" aria-label="処理中">
  <div class="spinner">
    <span></span><span></span><span></span>
  </div>
  <p class="ov-label">処理中...</p>
</div>

<script src="./script.js"></script>
</body>
</html>
/* ===== CSS 変数(色の調整はここで)===== */
:root {
  --spinner-light: #fff;
  --spinner-dark:  #2563EB;
}

*, *::before, *::after { box-sizing: border-box; }
body { font-family: sans-serif; padding: 24px; background: #f0f2f5; }

/* hidden 属性に display: none を強制(flex/grid に上書きされるのを防ぐ)*/
[hidden] { display: none !important; }

/* ===== デモセクション ===== */
.demo-section {
  background: #fff;
  border: 1px solid #e2e8f0;
  border-radius: 12px;
  padding: 20px;
  margin-bottom: 24px;
  max-width: 540px;
}
.pattern-label {
  margin: 0 0 16px;
  font-size: 13px;
  font-weight: 700;
  color: #2B7FE8;
}

/* ===== フォーム(Pattern 1)===== */
.demo-form { margin-bottom: 16px; }
.form-row { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; }
.form-row label { font-size: 13px; font-weight: 600; color: #374151; }
.form-row input {
  padding: 8px 10px; font-size: 14px;
  border: 1.5px solid #D0D7E0; border-radius: 6px; outline: none;
  font-family: sans-serif;
}
.form-row input:focus { border-color: #2B7FE8; }
.success-msg { margin: 0; font-size: 14px; font-weight: 700; color: #16A34A; }

/* ===== テーブル(Pattern 2)===== */
/* position: relative と overflow: hidden の両方が必要 */
.table-wrap {
  position: relative;
  border-radius: 8px;
  overflow: hidden;
  margin-bottom: 16px;
}
.data-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.data-table th {
  background: #F4F6F9; padding: 10px 12px;
  text-align: left; font-size: 12px; color: #5A6A7A;
  border-bottom: 1px solid #E2E8F0;
}
.data-table td { padding: 10px 12px; border-bottom: 1px solid #F0F2F5; }
.data-table tr:last-child td { border-bottom: none; }

/* ===== 全画面オーバーレイ ===== */
.ov-full {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

/* ===== 要素内オーバーレイ ===== */
.ov-inner {
  position: absolute;
  inset: 0;
  background: rgba(255, 255, 255, 0.85);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

/* ===== スピナー:くるくる回る円形(デフォルト)===== */
@keyframes ov-spin {
  to { transform: rotate(360deg); }
}
.spinner {
  width: 36px;
  height: 36px;
  border: 3px solid rgba(255, 255, 255, 0.3);
  border-top-color: var(--spinner-light);
  border-radius: 50%;
  animation: ov-spin 0.8s linear infinite;
}
.spinner--dark {
  border-color: rgba(37, 99, 235, 0.2);
  border-top-color: var(--spinner-dark);
}
.spinner span { display: none; }

/* ===== スピナー:ドット点滅に切り替える場合は上の .spinner ブロック(7行)を削除してこちらを使う =====
@keyframes ov-dot-blink {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0.2; }
}
.spinner { display: flex; gap: 6px; align-items: center; }
.spinner span {
  display: block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--spinner-light);
  animation: ov-dot-blink 1.2s ease-in-out infinite;
}
.spinner--dark span { background: var(--spinner-dark); }
.spinner span:nth-child(2) { animation-delay: 0.2s; }
.spinner span:nth-child(3) { animation-delay: 0.4s; }
===== */

/* ===== オーバーレイテキスト ===== */
.ov-label { margin-top: 12px; font-size: 14px; color: #fff; }
.ov-label--dark { color: #2563EB; }

/* ===== コントロールボタン ===== */
.controls { display: flex; gap: 8px; justify-content: flex-end; }
.controls button {
  padding: 6px 16px; font-size: 13px;
  border-radius: 6px; cursor: pointer;
  font-family: sans-serif; transition: background 0.15s;
}
.controls button:first-child {
  background: #2B7FE8; color: #fff; border: none;
}
.controls button:first-child:hover    { background: #1a6fd4; }
.controls button:first-child:disabled { background: #9AB8E8; cursor: not-allowed; }
.controls button:last-child {
  background: #fff; color: #5A6A7A; border: 1.5px solid #D0D7E0;
}
.controls button:last-child:hover { background: #F4F6F9; }

/* ===== レスポンシブ ===== */
@media (max-width: 480px) {
  .demo-section { padding: 16px; }
  .data-table th, .data-table td { padding: 8px; }
}
// ===== ダミーデータ =====
var employeesA = [
  { name: '田中 太郎',   dept: '営業部', status: '在席' },
  { name: '鈴木 花子',   dept: '開発部', status: '在席' },
  { name: '佐藤 次郎',   dept: '総務部', status: '外出中' },
  { name: '山田 さくら', dept: '営業部', status: '在席' },
  { name: '伊藤 健一',   dept: '開発部', status: 'テレワーク' },
];
var employeesB = [
  { name: '田中 太郎',   dept: '営業部', status: 'テレワーク' },
  { name: '鈴木 花子',   dept: '開発部', status: '外出中' },
  { name: '佐藤 次郎',   dept: '総務部', status: '在席' },
  { name: '山田 さくら', dept: '営業部', status: '在席' },
  { name: '伊藤 健一',   dept: '開発部', status: '在席' },
];

// ===== Pattern 1: 全画面オーバーレイ =====

function startFullOverlay() {
  var overlay = document.getElementById('ov-full');
  var btn     = document.getElementById('p1-btn');
  btn.disabled = true;
  overlay.removeAttribute('hidden');
  setTimeout(function() {
    overlay.setAttribute('hidden', '');
    document.getElementById('success-msg').removeAttribute('hidden');
    btn.disabled = false;
  }, 2000);
}

function resetP1() {
  document.getElementById('ov-full').setAttribute('hidden', '');
  document.getElementById('success-msg').setAttribute('hidden', '');
  document.getElementById('p1-btn').disabled = false;
}

// ===== Pattern 2: 要素内オーバーレイ =====

var isUpdated = false;

// テーブル行を生成して tbody に挿入する
function renderTable(data) {
  var tbody = document.getElementById('tbody');
  tbody.innerHTML = '';
  for (var i = 0; i < data.length; i++) {
    var tr  = document.createElement('tr');
    var td1 = document.createElement('td'); td1.textContent = data[i].name;
    var td2 = document.createElement('td'); td2.textContent = data[i].dept;
    var td3 = document.createElement('td'); td3.textContent = data[i].status;
    tr.appendChild(td1);
    tr.appendChild(td2);
    tr.appendChild(td3);
    tbody.appendChild(tr);
  }
}

function startInnerOverlay() {
  var overlay = document.getElementById('ov-inner');
  var btn     = document.getElementById('p2-btn');
  btn.disabled = true;
  overlay.removeAttribute('hidden');
  setTimeout(function() {
    isUpdated = !isUpdated;
    renderTable(isUpdated ? employeesB : employeesA);
    overlay.setAttribute('hidden', '');
    btn.disabled = false;
  }, 2000);
}

function resetP2() {
  isUpdated = false;
  renderTable(employeesA);
  document.getElementById('ov-inner').setAttribute('hidden', '');
  document.getElementById('p2-btn').disabled = false;
}

// 初期化
renderTable(employeesA);

AI用プロンプト

各パターンのプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。

ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。処理時間やメッセージの変更など、要件を追記して使うのがおすすめです。

※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。

💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。

Pattern 1 — 全画面オーバーレイ

# ローディングオーバーレイ(全画面)作成依頼

## 概要
フォーム送信中に画面全体をローディングオーバーレイでブロックするUIを作成してください。

## 要件
- 氏名・メールアドレスのシンプルなフォームを用意する
- 「送信する」ボタン押下で画面全体を半透明の背景とスピナーでブロックする
- ブロック中は「処理中...」テキストを表示し、ボタンはdisabledにする
- 2秒後にオーバーレイを消し、「送信が完了しました」メッセージを表示する
- リセットボタンで初期状態に戻る

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要

## 動作詳細
position: fixed; inset: 0; z-index: 9999 のオーバーレイ要素を使用する。
スピナーは border-radius: 50% + rotate アニメーションのCSSのみで実装する。
オーバーレイのON/OFFはhidden属性の付け外しで制御する。
hidden属性が display: flex に上書きされないよう [hidden] { display: none !important; } を定義すること。

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。

Pattern 2 — 要素内オーバーレイ

# ローディングオーバーレイ(要素内)作成依頼

## 概要
テーブルなど特定の要素内だけをローディングオーバーレイでブロックするUIを作成してください。

## 要件
- 5行の社員テーブル(氏名・部署・ステータス)を用意する
- 「データを更新する」ボタン押下でテーブル領域内だけを半透明の背景とスピナーでブロックする
- ブロック中はボタンをdisabledにする
- 2秒後にオーバーレイを消し、テーブルのデータを別セットに差し替える(押すたびにトグル)
- リセットボタンで元のデータに戻る

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要

## 動作詳細
テーブルラッパーに position: relative と overflow: hidden を設定する(両方必須)。
ラッパー内に position: absolute; inset: 0 のオーバーレイを配置する。
スピナーはCSSアニメーションで実装。
テーブル行の差し替えは createElement + textContent + appendChild で安全に行う(innerHTML に変数を結合しない)。
hidden属性が display: flex に上書きされないよう [hidden] { display: none !important; } を定義すること。

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。