レビュー済み·難易度: 初級·更新: 2026-04-18

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

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

nextjsstylingui-componenttailwindcss

対応バージョン

nextjs 15react 19

前提環境

Tailwind CSS の基本クラスと React コンポーネントの実装を理解していること

概要

Tailwind CSS の animate-pulse を使い、データ取得中に本物のカードと同じレイアウトで表示するスケルトン UI を実装する。Next.js の loading.tsx または <Suspense>fallback に渡すことで、コンテンツが揃うまでレイアウトシフトなしにローディング状態を表示できる。

インストール

npm install tailwindcss

実装

スケルトンカードコンポーネント

// components/SampleCardSkeleton.tsx

export function SampleCardSkeleton() {
  return (
    <div className="animate-pulse rounded-lg border border-gray-100 bg-white p-4 shadow-sm">
      {/* バッジ行 */}
      <div className="mb-3 flex gap-2">
        <div className="h-5 w-14 rounded-full bg-gray-200" />
        <div className="h-5 w-10 rounded-full bg-gray-200" />
      </div>

      {/* タイトル */}
      <div className="mb-2 h-4 w-3/4 rounded bg-gray-200" />
      <div className="mb-4 h-4 w-1/2 rounded bg-gray-200" />

      {/* サマリー */}
      <div className="space-y-1.5">
        <div className="h-3 w-full rounded bg-gray-100" />
        <div className="h-3 w-5/6 rounded bg-gray-100" />
        <div className="h-3 w-4/6 rounded bg-gray-100" />
      </div>
    </div>
  );
}

スケルトングリッド(一覧ページ用)

// components/SampleCardSkeletonGrid.tsx
import { SampleCardSkeleton } from "./SampleCardSkeleton";

type Props = {
  count?: number;
};

export function SampleCardSkeletonGrid({ count = 9 }: Props) {
  return (
    <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
      {Array.from({ length: count }).map((_, i) => (
        <SampleCardSkeleton key={i} />
      ))}
    </div>
  );
}

loading.tsx で使う

// app/samples/loading.tsx
import { SampleCardSkeletonGrid } from "@/components/SampleCardSkeletonGrid";

export default function Loading() {
  return (
    <div className="mx-auto max-w-5xl px-4 py-8">
      {/* フィルタ行のスケルトン */}
      <div className="mb-6 animate-pulse">
        <div className="h-9 w-48 rounded bg-gray-200" />
      </div>
      <SampleCardSkeletonGrid count={9} />
    </div>
  );
}

<Suspense> の fallback で使う

// app/samples/page.tsx(抜粋)
import { Suspense } from "react";
import { SampleCardSkeletonGrid } from "@/components/SampleCardSkeletonGrid";
import { SampleList } from "@/components/SampleList";

export default function SamplesPage() {
  return (
    <div className="mx-auto max-w-5xl px-4 py-8">
      <Suspense fallback={<SampleCardSkeletonGrid count={9} />}>
        <SampleList />
      </Suspense>
    </div>
  );
}

ポイント

  • animate-pulse は Tailwind の組み込みアニメーションで、opacity を 100% → 50% → 100% と繰り返す。追加設定なしで自然なローディング感を出せる
  • スケルトンのブロックサイズ(w-3/4 など)を実際のカードのテキスト行と近い比率にすることで、コンテンツ表示後のレイアウトシフトを抑えられる
  • バッジ行・タイトル・サマリーの 3 ブロック構成を実カード(SampleCard)と揃えることが重要。構造が異なると表示切り替え時に視覚的なジャンプが起きる
  • countprops として受け取ることで、ページサイズ(PAGE_SIZE)に合わせたグリッド数を呼び出し側から制御できる
  • loading.tsx はルートセグメント全体に適用される。<Suspense fallback> はコンポーネント単位で制御したい場合に使う。両方の使い方に対応できる設計にしておくと汎用性が高い
  • key={i} の使用は一覧が静的(並び替えや追加がない)なため許容される。動的なリストでは安定した key を使うこと

注意点

tailwind-empty-state-ui は 0 件時の表示。tailwind-filter-chip-ui はフィルタ部品。これはデータ取得中のローディング状態に表示するスケルトンカードの実装に特化。animate-pulse による pulse アニメーションと一覧グリッドへの組み込みを示す。

関連サンプル