概要
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)と揃えることが重要。構造が異なると表示切り替え時に視覚的なジャンプが起きる countをpropsとして受け取ることで、ページサイズ(PAGE_SIZE)に合わせたグリッド数を呼び出し側から制御できるloading.tsxはルートセグメント全体に適用される。<Suspense fallback>はコンポーネント単位で制御したい場合に使う。両方の使い方に対応できる設計にしておくと汎用性が高いkey={i}の使用は一覧が静的(並び替えや追加がない)なため許容される。動的なリストでは安定した key を使うこと