【Next.js】ローディング画面の作り方を loading.tsx から初回だけのオープニングまで解説
公開日: 2026年10月8日 · 読了目安 9 分
Next.js でローディング画面を作る手順です。出すべき場面と出さない場面、App Router の loading.tsx と Suspense、スケルトンとスピナーの使い分け、セッションで1回だけ出るオープニングとフェードアウト、LCP を落とさない注意点まで、コード付きで解説します。
ローディング画面は待ち時間があるときだけ出す
Next.js でサイトを作っていて、開いた瞬間のローディング画面を入れたくなった。でも、ただ読者を待たせるだけにならないかが気になっている。
答えは次のとおりです。ローディング画面は、本当に待ち時間があるときだけ出します。
ページ移動中の待ちは、App Router の loading.tsx で出せます。ブランドの演出として見せる場合も、出すのはセッションで1回、長くても1.5秒ほどが目安です。
この記事で分かることは次のとおりです。
ローディング画面を出すべき場面と、出さないほうがよい場面
loading.tsx と Suspense で、待っている間の画面を出す方法
スケルトンとスピナーの使い分け
初回だけ出る全画面のオープニングと、Motion でのフェードアウト
表示速度の指標(LCP)を落とさないための注意点
ローディング画面を出す場面と出さない場面
ローディング画面の役目は、待ち時間に「ちゃんと動いている」と伝えることです。待ち時間がないのに出すと、ただの足止めになります。
出す場面は、主に次の3つです。
データの取得に時間がかかるページへ移動するとき
フォームの送信など、押したあとに結果を待つとき
ブランドサイトで、最初の1回だけ演出として見せるとき
反対に、すぐ表示できるページをわざと数秒隠すのは避けます。読者は中身を見に来ているので、待たせた時間はそのまま負担になります。
演出としてのローディング画面は、ポートフォリオやブランドサイトに限って考えるのが安全です。資料請求や購入が目的の LP では、まず中身を見せてください。
Next.js でローディング画面を作る3つの方法
目的によって、作る場所が変わります。
Step1: loading.tsx で、ページ移動中にスケルトンを出す
Step2: ボタンや一部の領域に、スピナーやスケルトンを出す
Step3: 初回だけ全画面のオープニングを出し、フェードアウトさせる
多くのサイトは、Step1 と Step2 で足ります。Step3 は、演出そのものがブランドの一部になる場合だけ足してください。
Step1:loading.tsx でページ移動中のローディング画面を出す
App Router では、フォルダに loading.tsx を置くだけでローディング画面になります。同じフォルダの page.tsx がデータを待つ間、代わりにこの画面が出ます。
仕組みとしては、React の Suspense(待っている間に代わりの表示を出す機能)で動いています。Next.js が、page.tsx を自動で Suspense に包んでくれます。
下の例は、商品一覧のカードと同じ形をしたスケルトンです。
export default function Loading() {
return (
<div role="status" className="mx-auto max-w-5xl px-6 py-16">
<span className="sr-only">商品を読み込んでいます</span>
<div className="h-8 w-48 animate-pulse rounded-md bg-zinc-200 motion-reduce:animate-none" />
<div className="mt-8 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="space-y-3">
<div className="aspect-[4/3] animate-pulse rounded-xl bg-zinc-200 motion-reduce:animate-none" />
<div className="h-4 w-3/4 animate-pulse rounded bg-zinc-200 motion-reduce:animate-none" />
<div className="h-4 w-1/2 animate-pulse rounded bg-zinc-200 motion-reduce:animate-none" />
</div>
))}
</div>
</div>
);
}コードのポイント
role="status" と見えないテキストで、読み上げソフトにも読み込み中だと伝える
本物のカードと同じ並びにして、切り替わったときのずれを小さくする
点滅(animate-pulse)は、動きを減らす設定のときに止める
loading.tsx はサーバーで組み立てられるので、ブラウザで動かす印は要らない
ページの一部だけ待つなら Suspense で包む
ページ全体ではなく、レビューの欄だけが遅い場合もあります。そのときは、遅い部品だけを Suspense で包みます。
見出しや商品の説明は先に表示され、レビューの欄だけがスケルトンになります。
import { Suspense } from "react";
import { Reviews } from "./Reviews";
import { ReviewsSkeleton } from "./ReviewsSkeleton";
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return (
<main className="mx-auto max-w-5xl px-6 py-16">
<h1 className="text-3xl font-semibold">商品の詳細</h1>
<p className="mt-4 text-zinc-600">ここに商品の説明が入ります。</p>
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews productId={id} />
</Suspense>
</main>
);
}Step2:ローディング画面のスケルトンとスピナーを使い分ける
待っている間の見せ方は、スケルトンとスピナーの2種類が基本です。
スケルトン:これから出る中身の形を、灰色の箱で先に見せる。一覧やカードなど、形が決まっているものに向く
スピナー:回る印で、処理中であることだけを伝える。ボタンの送信など、短くて形のない待ちに向く
ページ全体を中央のスピナーだけにすると、何が出てくるのか分かりません。形が分かるものは、スケルトンにしてください。
"use client";
import { useFormStatus } from "react-dom";
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
className="inline-flex min-h-11 items-center gap-2 rounded-lg bg-zinc-900 px-5 text-sm font-medium text-white disabled:opacity-70"
>
{pending && (
<span
aria-hidden="true"
className="size-4 animate-spin rounded-full border-2 border-white/40 border-t-white motion-reduce:animate-none"
/>
)}
{pending ? "送信中…" : "送信する"}
</button>
);
}送信ボタンにスピナーを出すときのポイント
useFormStatus は、親の form が送信中かどうかを教えてくれる React の機能です。form の action と組み合わせて使います。
送信中はボタンを押せなくして、二重送信を防ぎます。文字も「送信中…」に変えると、回る印が見えない人にも伝わります。
Step3:初回だけ出るオープニングのローディング画面を作る
ブランドサイトでは、ロゴが現れてから中身に切り替わる演出を入れることがあります。ただ、この全画面のローディング画面は、何度も見せると邪魔になります。
そこで、sessionStorage(タブを閉じるまで値を覚えておく仕組み)に「見た」という印を残します。同じタブでは、2回目以降は出しません。
表示は長くても1.5秒にして、スキップのボタンも置きます。消えるときは、Motion でフェードアウトさせます。
"use client";
import { useEffect, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
const STORAGE_KEY = "splash-seen";
const MAX_MS = 1500;
function markSeen() {
try {
sessionStorage.setItem(STORAGE_KEY, "1");
} catch {}
}
export function Splash() {
const [visible, setVisible] = useState(true);
useEffect(() => {
let seen = false;
try {
seen = sessionStorage.getItem(STORAGE_KEY) === "1";
} catch {}
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (seen || reduce) {
setVisible(false);
return;
}
const timer = window.setTimeout(() => {
markSeen();
setVisible(false);
}, MAX_MS);
return () => window.clearTimeout(timer);
}, []);
const skip = () => {
markSeen();
setVisible(false);
};
return (
<AnimatePresence>
{visible && (
<motion.div
key="splash"
exit={{ opacity: 0 }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="fixed inset-0 z-50 grid place-items-center bg-zinc-950 text-white [.splash-seen_&]:hidden"
>
<motion.p
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="text-3xl font-semibold tracking-tight"
>
Acme Studio
</motion.p>
<button
type="button"
onClick={skip}
className="absolute bottom-8 right-6 min-h-11 rounded-full px-5 text-sm text-white/70 hover:text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-white"
>
スキップ
</button>
</motion.div>
)}
</AnimatePresence>
);
}2回目以降のローディング画面をちらつかせない
Splash はブラウザで動く部品なので、印を確かめる前に一瞬だけ画面に出てしまいます。これを防ぐため、描画の前に動く小さなスクリプトを layout.tsx に置きます。
印がある場合や動きを減らす設定の場合は、html に splash-seen というクラスを付けます。Splash の [.splash-seen_&]:hidden が、このクラスを見て最初から隠します。
スクリプトが html の class を書き換えるので、html には suppressHydrationWarning を付けています。
import type { ReactNode } from "react";
import { Splash } from "@/components/Splash";
import "./globals.css";
const splashScript =
'try{if(sessionStorage.getItem("splash-seen")==="1"||matchMedia("(prefers-reduced-motion: reduce)").matches){document.documentElement.classList.add("splash-seen")}}catch(e){}';
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="ja" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: splashScript }} />
</head>
<body>
<Splash />
{children}
</body>
</html>
);
}コードのポイント
「見た」印は、時間切れかスキップのときに保存する。開発中に処理が2回動いても、1回目として正しく扱える
動きを減らす設定の人には、オープニングを最初から出さない
本体のページは下で普通に描画しておき、透明にしない
AnimatePresence で包むと、消えるときにも exit のアニメーションが動く
Motion の基本は、https://design-layer.com/ja/articles/nextjs-framer-motion-guide で解説しています。
ローディング画面で LCP を落とさないための注意点
LCP は、ページでいちばん大きな画像や文章が表示されるまでの時間です。Google が重視する、表示速度の指標の1つです。
全画面のローディング画面は、この数値を悪くすることがあります。次の3点を守ってください。
本体の文章や画像を、opacity: 0 で隠したまま待たない。透明な要素は LCP の対象にならず、計測が後ろに延びる
最低表示時間のような、わざと待たせるタイマーを入れない
オープニングの中に、大きな画像や重い動画を置かない
演出は本体の読み込みを止めず、上に重ねるだけにします。重ねている間も、下ではページの準備が進みます。
動きを減らす設定のときのローディング画面
点滅や回転は、動きを減らす設定のときに止めるか、ゆっくりにします。Tailwind では motion-reduce: を付けると、その設定のときだけ別の指定にできます。
回る印を止める場合は、「読み込み中」や「送信中…」の文字を必ず添えてください。
ローディング画面でよくある失敗と公開前チェック
待ち時間のないページに、演出のためだけのローディング画面を出す
ページを移動するたびに、全画面のオープニングを出す
スキップの手段がなく、終わるまで何もできない
ページ全体を中央のスピナーだけにして、何が出るか分からない
読み込み中であることが、読み上げソフトに伝わらない
ページ全体のファイルに、ブラウザで動かす印を書いてしまう
公開前には、ブラウザの開発者ツールで回線を遅くして確かめます。Network タブで Slow 4G などを選ぶと、遅い回線での待ち時間に近い状態で見られます。
ページを開いたあとの最初の見た目は、https://design-layer.com/ja/articles/nextjs-animated-hero-section-guide で解説しています。
完成したローディング画面をコピーして使う方法

ロゴの文字が1文字ずつ波打つような、凝ったローディング画面を作りたい場合もあります。文字ごとの動きと背景の重なりを整えるのは、意外と手間がかかります。
DesignLayer のカタログにある「ピクセルアーク・ローダー」は、全画面のローディング画面です。ぼかした風景写真の上で、アーチ状に並んだ立体的なピクセルのロゴが文字ごとに波打ちます。
詳細ページのライブプレビューで動きを確かめ、「Cursor 用にコピー」を押します。組み込みの指示・ライブプレビューの URL・本体の TSX・必要な npm パッケージが、まとめてコピーされます。
Cursor や Claude に貼れば、自分の Next.js に組み込めます。そのあとは、ロゴの文言と背景写真を差し替えます。
組み込むときは、Step3 の sessionStorage の仕組みと組み合わせると、初回だけ表示できます。
閲覧とライブプレビューは無料で、アカウントもカードも要りません。無料のソースコピーは、日本時間で1日3回までです。https://design-layer.com/ja/components
ローディング画面についてよくある質問
同じフォルダの page.tsx が、データを待っている間に表示されます。待ちがなければ、ほとんど見えずに切り替わります。
まとめ:Next.js でローディング画面を作る手順
ローディング画面は、本当に待ち時間があるときだけ出す
ページ移動中の待ちは、loading.tsx にスケルトンを置く
一部だけ遅いなら、その部品だけを Suspense で包む
形が決まっているものはスケルトン、短い送信はスピナーにする
オープニングはセッションで1回、1.5秒以内にして、スキップを置く
本体は透明にせず、演出は上に重ねるだけにする
まずは、データを取得しているページのフォルダに loading.tsx を1つ置いてみてください。
ロゴを Lottie で動かす場合は、https://design-layer.com/ja/articles/nextjs-lottie-animation-guide で読み込みを軽くする方法を解説しています。