© TOOLS BOX — Next.js / React / TypeScript コードサンプル集

サンプルガイド
←サンプル一覧
nextjsstyling

Tailwind CSS でローディング中にオーバーレイを表示する

フォーム送信や一覧更新中にコンテンツ上に半透明のローディングオーバーレイを被せる UI パターン。Tailwind CSS と React の状態管理で実装し、スケルトンカードとの責務の違いも示す。

難易度: 初級·更新: 2026-04-18·
tailwindcss

対応バージョン

nextjs 15react 19

前提環境

Tailwind CSS の基本クラスと React の useState を理解していること

概要

フォーム送信中・データ再取得中にコンテンツ上へ半透明オーバーレイを重ね、スピナーを表示するパターンを実装する。スケルトンカードが「コンテンツを置き換える」のに対し、オーバーレイは「コンテンツを保持したまま操作を無効化する」用途に使う。

インストール

npm install tailwindcss

実装

ローディングオーバーレイコンポーネント

// components/LoadingOverlay.tsx
type Props = {
  isLoading: boolean;
  children: React.ReactNode;
};

export function LoadingOverlay({ isLoading, children }: Props) {
  return (
    <div className="relative">
      {children}
      {isLoading && (
        <div
          className="absolute inset-0 flex items-center justify-center rounded bg-white/70"
          aria-live="polite"
          aria-label="読み込み中"
        >
          <svg
            className="h-8 w-8 animate-spin text-blue-500"
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 24 24"
            aria-hidden="true"
          >
            <circle
              className="opacity-25"
              cx="12"
              cy="12"
              r="10"
              stroke="currentColor"
              strokeWidth="4"
            />
            <path
              className="opacity-75"
              fill="currentColor"
              d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
            />
          </svg>
        </div>
      )}
    </div>
  );
}

フォーム送信時に使う

// components/SampleForm.tsx
"use client";

import { useState } from "react";
import { LoadingOverlay } from "./LoadingOverlay";

export function SampleForm() {
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsSubmitting(true);
    try {
      await fetch("/api/samples", { method: "POST" });
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <LoadingOverlay isLoading={isSubmitting}>
      <form onSubmit={handleSubmit} className="space-y-4 rounded border p-4">
        <input
          type="text"
          placeholder="タイトル"
          disabled={isSubmitting}
          className="w-full rounded border px-3 py-2 text-sm disabled:opacity-50"
        />
        <button
          type="submit"
          disabled={isSubmitting}
          className="rounded bg-blue-600 px-4 py-2 text-sm text-white disabled:opacity-50"
        >
          送信
        </button>
      </form>
    </LoadingOverlay>
  );
}

一覧更新時に使う

// components/SampleListWithOverlay.tsx
"use client";

import { useTransition } from "react";
import { useRouter } from "next/navigation";
import { LoadingOverlay } from "./LoadingOverlay";

type Props = {
  children: React.ReactNode;
};

export function SampleListWithOverlay({ children }: Props) {
  const [isPending, startTransition] = useTransition();
  const router = useRouter();

  const handleRefresh = () => {
    startTransition(() => {
      router.refresh();
    });
  };

  return (
    <div>
      <button
        onClick={handleRefresh}
        className="mb-4 rounded border px-3 py-1 text-sm hover:bg-gray-50"
      >
        更新
      </button>
      <LoadingOverlay isLoading={isPending}>
        {children}
      </LoadingOverlay>
    </div>
  );
}

全画面オーバーレイ(ページ遷移中)

// components/PageLoadingOverlay.tsx
type Props = {
  isLoading: boolean;
};

export function PageLoadingOverlay({ isLoading }: Props) {
  if (!isLoading) return null;

  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center bg-white/80"
      aria-live="assertive"
      aria-label="ページを読み込んでいます"
    >
      <svg
        className="h-10 w-10 animate-spin text-blue-500"
        xmlns="http://www.w3.org/2000/svg"
        fill="none"
        viewBox="0 0 24 24"
        aria-hidden="true"
      >
        <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
        <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
      </svg>
    </div>
  );
}

ポイント

  • relative + absolute inset-0 の組み合わせでコンテンツ上に正確にオーバーレイを重ねる。relative を忘れると absolute が最寄りの positioned 祖先を基準にずれる
  • bg-white/70 で半透明にする。元のコンテンツが透けて見えることでユーザーが状態遷移を認識しやすい。完全に隠すより UX がよい
  • aria-live="polite" をオーバーレイに付けることで、スクリーンリーダーが「読み込み中」と通知できる。ページ遷移など緊急性の高い場合は aria-live="assertive" を使う
  • フォームの input / button に disabled={isSubmitting} を付けて二重送信を防ぐ。オーバーレイだけでは操作がキャンセルされない
  • スケルトンカードとの使い分け: スケルトンは「初回データ取得中・コンテンツがまだない状態」、オーバーレイは「操作中・既存コンテンツの上で処理中」に使う
  • finally ブロックで setIsSubmitting(false) を呼ぶことで、エラー発生時もオーバーレイが閉じる
  • 一覧更新では useTransition + startTransition(() => router.refresh()) を使い isPending をオーバーレイに渡す。setTimeout の固定待機と異なり、実際の Router 更新完了に連動してオーバーレイが閉じる

注意点

tailwind-skeleton-card はデータ取得中のスケルトン表示。これはコンテンツを隠さず上に被せる overlay 形式のローディング表現に特化。操作中の二重送信防止とともに使う。

関連サンプル

同じテーマや技術スタックを使った実装例

  • Tailwind CSS でローディング中のスケルトンカード UI を実装する

    コンテンツ読み込み中に表示するスケルトン(骨格)カード UI を Tailwind CSS の animate-pulse で実装し、一覧グリッドに並べるパターン。

  • Tailwind CSS で空状態(EmptyState)UI を実装する

    検索結果ゼロ・データ未登録・エラー後などの空状態に表示するコンポーネントを Tailwind CSS でスタイリングし、アイコン・メッセージ・アクションボタンを組み合わせたパターン集。

  • Tailwind CSS でフィルターチップ(アクティブフィルタ表示・削除)UI を実装する

    適用中のフィルタ条件をチップ形式で横並びに表示し、個別削除ボタンと一括クリアボタンを持つフィルターチップコンポーネントを Tailwind CSS で実装する例。

  • Tailwind CSS でレスポンシブレイアウトを組む

    Tailwind CSS のブレークポイントプレフィックス(sm / md / lg)を使い、モバイルファーストのレスポンシブレイアウトを実装する例。

  • Tailwind CSS でテーブル UI をスタイリングする

    Tailwind CSS でレスポンシブ対応のテーブル UI を実装し、ヘッダー固定・行ホバー・ストライプ・空状態・ローディング状態のスタイルパターンをまとめた例。

関連仕様

このサンプルを理解するのに役立つ仕様や概念

  • FrameworkNext.jsReact ベースのフルスタックフレームワーク。SSR・SSG・App Router・API Routes を提供する。
←サンプル一覧に戻る