【Next.js】スクロールで要素をふわっと表示する方法を解説

公開日: 2026年9月12日 · 読了目安 11 分

Next.js でセクションや要素をスクロールに合わせてふわっと表示させる(scroll reveal)方法を、追加パッケージなしの IntersectionObserver と Framer Motion の whileInView の2通りのコードで解説。一度だけ表示する設定、スマホで発火しない原因、reduced-motion への対応も。

スクロールで要素をふわっと表示したいときの答え

Next.js で LP を作っていて、スクロールしたときにセクションが下からふわっと出てくる動きを付けたい。調べると方法が多く、どれを選べばよいか迷います。

付けてみたら、スクロールのたびに消えたり出たりして落ち着かない、という声もよく聞きます。

基本の形は決まっています。画面に入ったときに一度だけ、次のように動かします。

  • 透明から、はっきり見える状態へ(opacity を 0 から 1 へ)

  • 位置を 16〜24px 下から元の位置へ

  • 時間は 0.5〜0.7 秒

付けるのは LP 全体で 2〜3 セクションまでにし、動きを減らす設定の人には最初から表示します。

この記事では、次の内容をコード付きで説明します。

  • 追加パッケージなしで、ブラウザの標準機能だけを使って作る方法

  • Motion(旧 Framer Motion)で作る方法と、カードを順番に表示する方法

  • スクロール量に連動する動き(パララックス)との違い

  • スマホで表示されない、ページが重くなるといった失敗の原因

スクロール表示を一度だけ・数か所に絞る理由

スクロールで要素を表示する演出(scroll reveal)の役割は、読む順番を示すことです。

全部のセクションに同じ動きを付けると、どこが大事なのかが伝わらず、テンポが単調になります。

目を止めてほしい場所に絞ると効きます。たとえば次のような場所です。

  • ヒーローの直後の機能紹介

  • 実績の数字

  • 最後の CTA

一度だけにする理由は、読み返しのしやすさです。上に戻るたびに要素が消えてまた出てくると、読んでいた位置を見失います。

一度表示した要素は、そのまま表示しておきます。

もう1つは表示速度です。scroll イベントで毎回位置を計算したり、大きな blur をかけた要素をスクロールに合わせて動かしたりすると、スマホで動きがカクつきます。

この記事で使う2つの方法は、どちらも画面に入ったかどうかだけをブラウザに判定させます。そのため、こうした負荷がかかりません。

IntersectionObserver でスクロール表示を作る方法

IntersectionObserver は、要素が画面に入ったかどうかを知らせてくれるブラウザの機能です。追加のパッケージは要りません。

画面に入ったら data-visible を true にし、CSS の transition で動かします。一度表示したら監視をやめるので、出たり消えたりしません。

components/Reveal.tsx
"use client";

  import { useEffect, useRef, useState, type ReactNode } from "react";

  type RevealProps = {
    children: ReactNode;
    className?: string;
  };

  export function Reveal({ children, className }: RevealProps) {
    const ref = useRef<HTMLDivElement>(null);
    const [visible, setVisible] = useState(false);

    useEffect(() => {
      const element = ref.current;
      if (!element) return;

      const observer = new IntersectionObserver(
        ([entry]) => {
          if (entry.isIntersecting) {
            setVisible(true);
            observer.disconnect(); // 一度表示したら監視をやめる
          }
        },
        // 画面の下端から 10% 手前に入ったら表示する
        { rootMargin: "0px 0px -10% 0px", threshold: 0 }
      );

      observer.observe(element);
      return () => observer.disconnect();
    }, []);

    return (
      <div ref={ref} data-visible={visible} className={["reveal", className].filter(Boolean).join(" ")}>
        {children}
      </div>
    );
  }

スクロール表示の動きは CSS に書く

見た目の変化は globals.css にまとめます。動きを減らす設定(prefers-reduced-motion)の人には、最初から表示された状態にします。

app/globals.css
.reveal {
    opacity: 0;
    transform: translateY(20px);
    transition:
      opacity 0.6s ease-out,
      transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
  }

  .reveal[data-visible="true"] {
    opacity: 1;
    transform: none;
  }

  @media (prefers-reduced-motion: reduce) {
    .reveal {
      opacity: 1;
      transform: none;
      transition: none;
    }
  }

ページはサーバー側の部品のまま、包むだけでよい

Reveal は、ブラウザで動く部品(Client Component)です。

それでも、中に入れたセクション(children)は、サーバーで組み立てる部品(Server Component)のままで動きます。

ページ全体(page.tsx)に、ブラウザで動かす印の "use client" を付ける必要はありません。動かしたいセクションだけを包みます。

app/page.tsx
import { Reveal } from "@/components/Reveal";
  import { Hero } from "@/components/sections/Hero";
  import { Features } from "@/components/sections/Features";
  import { Stats } from "@/components/sections/Stats";
  import { Faq } from "@/components/sections/Faq";
  import { FinalCta } from "@/components/sections/FinalCta";

  export default function Page() {
    return (
      <main>
        <Hero />
        <Reveal>
          <Features />
        </Reveal>
        <Reveal>
          <Stats />
        </Reveal>
        <Faq />
        <Reveal>
          <FinalCta />
        </Reveal>
      </main>
    );
  }

Framer Motion でスクロール時に順番に表示する

すでに Motion を使っているなら、画面に入ったら動かす指定(whileInView)で同じ動きを書けます。

一度だけ表示したいときは、viewport の once を true にします。

カードを左から順に出したいときは、親の variants に staggerChildren を指定します。

導入の手順は https://design-layer.com/ja/articles/nextjs-framer-motion-guide で説明しています。

components/RevealList.tsx
"use client";

  import { motion, type Variants } from "motion/react";

  const container: Variants = {
    hidden: {},
    show: { transition: { staggerChildren: 0.08 } },
  };

  const item: Variants = {
    hidden: { opacity: 0, y: 20 },
    show: {
      opacity: 1,
      y: 0,
      transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] },
    },
  };

  type Feature = { title: string; body: string };

  export function RevealList({ items }: { items: Feature[] }) {
    return (
      <motion.ul
        variants={container}
        initial="hidden"
        whileInView="show"
        viewport={{ once: true, amount: 0.3 }}
        className="grid gap-6 md:grid-cols-3"
      >
        {items.map((feature) => (
          <motion.li
            key={feature.title}
            variants={item}
            className="rounded-2xl border border-zinc-200 p-6"
          >
            <h3 className="font-semibold">{feature.title}</h3>
            <p className="mt-2 text-sm text-zinc-600">{feature.body}</p>
          </motion.li>
        ))}
      </motion.ul>
    );
  }

表示を始める割合(amount)は 0.3 前後から試す

amount は、要素のどれだけが画面に入ったら表示を始めるかの割合です。0.3 なら3割が見えた時点です。

小さすぎると画面外で動きが終わり、大きすぎると表示が遅く感じられます。

動きを減らす設定にまとめて対応する

レイアウトなど、外側にあるブラウザで動く部品で設定します。MotionConfig(Motion の全体設定)に reducedMotion="user" を指定します。

すると、OS で動きを減らす設定にしている人には、位置の移動などが無効になります。

opacity の変化は残るため、ふわっと現れる程度の控えめな表示になります。

スクロール表示とパララックス(スクロール連動)の違い

ここまでの scroll reveal は、画面に入った瞬間に一度だけ動く演出です。

これとは別に、スクロールに合わせて動き続ける演出もあります。

  • スクロールした量に合わせて画像がゆっくり動くパララックス

  • セクションを画面に固定したまま中身が切り替わるピン留め

スクロール量に連動させるには、Motion の次の2つの仕組みを使います。

  • useScroll:スクロール量を読み取る

  • useTransform:読み取った値を、動きの量に変換する

スクロールのたびに値が変わるので、使うのはヒーローの画像など1〜2か所にとどめます。

拡大も Motion の style に入れます。class の scale-110 は、Motion が書き出す transform に上書きされるためです。

components/ParallaxImage.tsx
"use client";

  import { motion, useScroll, useTransform } from "motion/react";
  import { useRef } from "react";

  export function ParallaxImage({ src, alt }: { src: string; alt: string }) {
    const ref = useRef<HTMLDivElement>(null);
    const { scrollYProgress } = useScroll({
      target: ref,
      offset: ["start end", "end start"],
    });
    const y = useTransform(scrollYProgress, [0, 1], [40, -40]);

    return (
      <div ref={ref} className="overflow-hidden rounded-2xl">
        <motion.img src={src} alt={alt} style={{ y, scale: 1.1 }} className="h-full w-full object-cover" />
      </div>
    );
  }

ピン留めのスクロールギャラリーは別の作り方になる

セクションを画面に止めたまま(sticky)、スクロールに合わせて画像を横に流すギャラリーは、scroll reveal の延長では作れません。

手順は https://design-layer.com/ja/articles/nextjs-scroll-gallery-guide にまとめています。

スクロール表示でよくある失敗と直し方

公開前に、次の点を確認してください。

  • ヒーローなど最初から見えている場所まで隠している(表示が遅れ、ページの表示速度の評価も下がる)

  • 縦に長いセクションに amount 0.5 以上を指定し、スマホで表示が始まらない

  • ページ内リンクで移動した先が、まだ透明のまま

  • スクロールのたびに表示と非表示を繰り返している

  • 大きな blur や影を付けた要素を動かして、スマホでカクつく

スマホでスクロール表示されないときは表示の割合を見直す

amount は要素の高さに対する割合です。高さ 3,000px のセクションに 0.5 を指定すると、1,500px 分が画面に入るまで表示が始まりません。

スマホの画面の高さはそれより小さいため、いつまでも透明のままになります。

縦に長いセクションは、中の見出しやカードごとに Reveal を付けるか、amount を 0.1 程度まで下げます。

最初の画面にある要素は隠さない

ヒーローの見出しを scroll reveal で隠すと、JavaScript が読み込まれるまで何も見えません。

最初の画面には、CSS だけで動く短い入場アニメーションを使います。スクロール表示は、その下のセクションから付けます。

スクロール表示入りのセクションをコピーして使う方法

DesignLayer のコンポーネント一覧。左のカテゴリで用途ごとに絞り込める
DesignLayer のコンポーネント一覧。左のカテゴリで用途ごとに絞り込める

一度だけふわっと表示する動きは、この記事のコードで十分に作れます。

一方で、次のような演出を組み合わせて一から作ると、タイミングの調整に半日かかることもあります。

  • ピン留め

  • セクションの切り替わり

  • パララックス

DesignLayer は、人が組んだ React + Tailwind CSS のモーション付きセクションを、ブラウザでライブプレビューしてからコピーできるカタログです。

実際にスクロールして、動きを確かめてから選べます。

閲覧は無料で、カード登録も要りません。https://design-layer.com/ja/components

Copy for Cursor を押すと、統合の指示・ライブプレビューの URL・本体の TSX・npm 依存がまとめてコピーされます。Cursor や Claude にそのまま渡せます。

無料アカウントでは、無料サンプル10個を無料アカウントで回数制限なくコピーでき、動画プロンプトは月1回です。Free では MCP に接続できません。カタログ全体は30日ビルドパス($19の1回払い、自動更新なし)、Lifetime Access(先着100名の$79の1回払い)、Plus(月額 $9、いつでも解約可)です。強い動きはヒーローと1セクション程度に絞ると、ページ全体が落ち着きます。

Next.js のスクロール表示についてよくある質問

  • 使えます。ただし App Router では、初期化をブラウザで動く部品の中で行う必要があります。

    具体的には、useEffect(表示のあとに処理を走らせる仕組み)で初期化し、ライブラリの CSS も読み込みます。

    数セクションに一度だけ表示する用途なら、この記事の Reveal コンポーネントで足ります。依存を増やさずに済むため、まずは自作の形から試すのがおすすめです。

まとめ:Next.js でスクロール時にふわっと表示する手順

  • スクロールでの表示は、一度だけ・2〜3 セクションまでにする

  • 動きは、透明から表示への変化と 16〜24px の移動にし、0.5〜0.7 秒を目安にする

  • 依存を増やしたくなければブラウザの標準機能(IntersectionObserver)、Motion があればその機能を使う

  • ページはサーバー側の部品のまま、動かすセクションだけを包む

  • ヒーローは隠さず、縦に長いセクションは表示を始める割合を小さくする

  • 動きを減らす設定の人には最初から表示する

まずは Reveal コンポーネントを1つ作り、機能紹介のセクションだけを包んでみてください。

スクロールに合わせた複雑な演出が必要になったら、DesignLayer のカタログで実際にスクロールして選べます。https://design-layer.com/ja/components

関連ガイド