【Next.js】ビフォーアフタースライダーの作り方をドラッグとスクロール演出まで解説

公開日: 2026年10月8日 · 読了目安 8 分

Next.js で、2枚の写真を境目を動かして見比べるビフォーアフタースライダーを作る手順です。CSS の clip-path と range 入力によるキーボード対応の土台、ポインター操作でのドラッグ、next/image での画像の揃え方、sticky とスクロール量で切り替える応用までコード付きで解説します。

Next.js でビフォーアフタースライダーを作りたいとき

リフォームや美容室のサイトで、施工前と施工後の写真を並べている。2枚を横に置いただけだと、どこが変わったのか伝わりにくい。

そこで使われるのが、ビフォーアフタースライダーです。2枚の写真を重ね、境目の線を左右に動かして見比べます。

作り方の答えは次のとおりです。写真を2枚重ね、上の1枚を CSS の clip-path(要素の一部だけを見せる指定)で切り取ります。

境目の位置は、HTML 標準の range 入力(つまみを動かす入力欄)で持ちます。これだけでキーボード操作と読み上げに対応できます。

この記事で分かることは次のとおりです。

  • clip-path と range 入力で作る、いちばん簡単なビフォーアフタースライダー

  • マウスと指で境目をドラッグする作り方

  • next/image で2枚の写真の大きさと位置を揃えるコツ

  • スクロールに合わせて写真が切り替わる応用の演出

ビフォーアフタースライダーを range 入力で作る理由

ドラッグだけで動くスライダーは、マウスや指が使えない人には操作できません。キーボードで境目を動かせることは、見比べるための最低限の条件です。

range 入力なら、矢印キーで少しずつ動き、Home と End で端まで飛べます。今の位置もスクリーンリーダーに伝わります。

自分で作るのは、見た目とドラッグの操作だけで済みます。土台の操作をブラウザに任せるので、コードも短くなります。

Step1:clip-path と range でビフォーアフタースライダーを作る

下の層に After、上の層に Before の写真を重ねます。上の層の右側を clip-path で切り取り、左側だけを見せます。

range 入力は透明にして、写真の上いっぱいに広げます。マウスでクリックやドラッグをすると、その位置まで境目が動きます。

components/BeforeAfterSlider.tsx
"use client";

import Image from "next/image";
import { useId, useState } from "react";

type Img = { src: string; alt: string };

export function BeforeAfterSlider({ before, after }: { before: Img; after: Img }) {
  const [pos, setPos] = useState(50); // Before を見せる幅(%)
  const inputId = useId();

  return (
    <div className="relative aspect-[3/2] w-full select-none overflow-hidden rounded-2xl bg-zinc-100">
      {/* 下の層:After */}
      <Image src={after.src} alt={after.alt} fill sizes="(min-width: 768px) 720px, 100vw" className="object-cover" />

      {/* 上の層:Before。右側を切り取って左だけ見せる */}
      <div className="absolute inset-0" style={{ clipPath: "inset(0 " + (100 - pos) + "% 0 0)" }}>
        <Image src={before.src} alt={before.alt} fill sizes="(min-width: 768px) 720px, 100vw" className="object-cover" />
      </div>

      {/* 境目の線 */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-y-0 w-0.5 -translate-x-1/2 bg-white"
        style={{ left: pos + "%" }}
      />

      <label htmlFor={inputId} className="sr-only">
        Before と After の境目の位置
      </label>
      <input
        id={inputId}
        type="range"
        min={0}
        max={100}
        value={pos}
        onChange={(e) => setPos(Number(e.target.value))}
        aria-valuetext={"Before を " + pos + "% 表示"}
        className="absolute inset-0 h-full w-full cursor-ew-resize opacity-0"
      />
    </div>
  );
}

コードのポイント

  • inset(0 30% 0 0) は「右から30%を切り取る」という意味。境目が70%の位置になる

  • 2枚とも同じ枠いっぱいに置くので、切り取っても写真の位置はずれない

  • label は画面に出さず(sr-only)、読み上げだけに使う

  • aria-valuetext で「Before を 70% 表示」のように、数字の意味まで伝える

Step1 だけでは足りない場面

透明な range 入力は、マウスとキーボードでは素直に動きます。一方スマホでは、見えないつまみを指で探す形になりやすく、思った位置に動かないことがあります。

スマホからの閲覧が多いサイトでは、次の Step2 で指のドラッグを自分で受け取ります。

Step2:ビフォーアフタースライダーをドラッグで動かす

マウスと指の操作は、Pointer Events(マウス・指・ペンをまとめて扱うイベント)で受け取ります。押した位置から境目を計算し、離すまで追いかけます。

range 入力はキーボードと読み上げ用に残し、マウスと指の操作は受け取らない設定にします。Step1 のファイルを、次のコードで置き換えます。

components/BeforeAfterSlider.tsx
"use client";

import Image from "next/image";
import { useId, useRef, useState } from "react";

type Img = { src: string; alt: string };

type Props = {
  before: Img;
  after: Img;
  aspect?: string; // 例: "3 / 2"
  initial?: number;
};

const SIZES = "(min-width: 768px) 720px, 100vw";

export function BeforeAfterSlider({ before, after, aspect = "3 / 2", initial = 50 }: Props) {
  const [pos, setPos] = useState(initial);
  const boxRef = useRef<HTMLDivElement>(null);
  const dragging = useRef(false);
  const inputId = useId();

  function moveTo(clientX: number) {
    const box = boxRef.current;
    if (!box) return;
    const rect = box.getBoundingClientRect();
    const next = ((clientX - rect.left) / rect.width) * 100;
    setPos(Math.round(Math.min(100, Math.max(0, next))));
  }

  function stop() {
    dragging.current = false;
  }

  return (
    <div
      ref={boxRef}
      onPointerDown={(e) => {
        dragging.current = true;
        e.currentTarget.setPointerCapture(e.pointerId);
        moveTo(e.clientX);
      }}
      onPointerMove={(e) => {
        if (dragging.current) moveTo(e.clientX);
      }}
      onPointerUp={stop}
      onPointerCancel={stop}
      className="group relative w-full cursor-ew-resize touch-pan-y select-none overflow-hidden rounded-2xl bg-zinc-100"
      style={{ aspectRatio: aspect }}
    >
      <Image src={after.src} alt={after.alt} fill sizes={SIZES} draggable={false} className="object-cover" />
      <div className="absolute inset-0" style={{ clipPath: "inset(0 " + (100 - pos) + "% 0 0)" }}>
        <Image src={before.src} alt={before.alt} fill sizes={SIZES} draggable={false} className="object-cover" />
      </div>

      <span className="pointer-events-none absolute left-3 top-3 rounded-full bg-black/60 px-3 py-1 text-xs text-white">Before</span>
      <span className="pointer-events-none absolute right-3 top-3 rounded-full bg-black/60 px-3 py-1 text-xs text-white">After</span>

      {/* キーボードとスクリーンリーダー用。マウスと指の操作は外側の div が受け取る */}
      <label htmlFor={inputId} className="sr-only">
        Before と After の境目の位置
      </label>
      <input
        id={inputId}
        type="range"
        min={0}
        max={100}
        value={pos}
        onChange={(e) => setPos(Number(e.target.value))}
        aria-valuetext={"Before を " + pos + "% 表示"}
        className="pointer-events-none absolute inset-0 h-full w-full opacity-0"
      />

      {/* 境目の線とつまみ(見た目だけ) */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-y-0 w-0.5 -translate-x-1/2 bg-white"
        style={{ left: pos + "%" }}
      >
        <span className="absolute left-1/2 top-1/2 flex size-11 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-white text-zinc-900 shadow-lg ring-sky-500 ring-offset-2 group-has-[:focus-visible]:ring-2">
          ↔
        </span>
      </div>
    </div>
  );
}

スマホで縦のスクロールを邪魔しない

外側の div に付けた touch-pan-y は、「縦の動きはページのスクロールに使う」という指定です。横のドラッグだけがスライダーに届き、縦に指を動かせば普通にスクロールできます。

setPointerCapture を呼ぶと、指が写真の外に出ても動きを追い続けます。ドラッグの途中で境目が止まることがありません。

つまみの大きさとフォーカスの見せ方

つまみは size-11(44px)にして、指で押しやすい大きさを確保します。写真の上ならどこを押しても動くので、つまみは目印の役目です。

キーボードで選ばれたときは、group-has-[:focus-visible] でつまみに青い輪を出します。透明な入力欄の代わりに、どこが操作中かを見せるためです。

Step3:next/image でビフォーアフタースライダーの画像を揃える

見比べで一番大切なのは、2枚の写真の構図が揃っていることです。縦横の比率が違うと、object-cover の切り取り方が変わり、境目の前後で写真がずれます。

2枚は同じ比率・同じ画角で書き出してから public に置きます。部品には fill を使い、外枠の aspect-ratio で高さを決めます。

app/page.tsx
import { BeforeAfterSlider } from "@/components/BeforeAfterSlider";

export default function Page() {
  return (
    <section className="mx-auto max-w-3xl px-6 py-24">
      <h2 className="text-3xl font-semibold">片付けサービスの施工例</h2>
      <p className="mt-3 text-zinc-600">境目をドラッグして、作業の前とあとを見比べられます。</p>
      <div className="mt-10">
        <BeforeAfterSlider
          before={{ src: "/ba/room-before.jpg", alt: "作業前のリビング。床に段ボールが積まれている" }}
          after={{ src: "/ba/room-after.jpg", alt: "作業後のリビング。床が見え、荷物は棚に収まっている" }}
          aspect="3 / 2"
        />
      </div>
    </section>
  );
}

sizes と alt を書く

fill を使うときは sizes を書きます。表示される幅に合った大きさの画像が選ばれ、スマホで大きすぎる画像を読み込まずに済みます。

alt には「何が写っているか」と「前か後か」を書きます。2枚とも「施工例」だけだと、読み上げでは違いが伝わりません。

page.tsx はサーバーのままで構いません。ブラウザで動くのは、スライダーのファイルだけです。

応用:スクロールでめくるビフォーアフタースライダー

LP の見せ場では、スクロールするだけで Before から After へ切り替わる演出もよく使われます。写真を画面に留めたまま、スクロール量に合わせて境目を動かします。

仕組みは、縦に長い外枠の中に sticky(スクロールしても画面に留まる配置)の写真を置く形です。外枠をどこまで進んだかを Motion の useScroll で読み、切り取る幅に変えます。

動きを減らす設定の人には、Step2 の手で動かすスライダーを出します。

components/BeforeAfterScroll.tsx
"use client";

import Image from "next/image";
import { useRef } from "react";
import { motion, useReducedMotion, useScroll, useTransform } from "motion/react";
import { BeforeAfterSlider } from "./BeforeAfterSlider";

type Img = { src: string; alt: string };
type Props = { before: Img; after: Img };

const SIZES = "(min-width: 1024px) 896px, 100vw";

export function BeforeAfterScroll(props: Props) {
  const reduce = useReducedMotion();
  // 動きを減らす設定なら、手で動かすスライダーを出す
  if (reduce) return <BeforeAfterSlider {...props} />;
  return <ScrollWipe {...props} />;
}

function ScrollWipe({ before, after }: Props) {
  const trackRef = useRef<HTMLDivElement>(null);
  const { scrollYProgress } = useScroll({ target: trackRef, offset: ["start start", "end end"] });
  // 進み具合 15%〜85% の間で、Before の右側を 0% から 100% まで切り取る
  const hidden = useTransform(scrollYProgress, [0.15, 0.85], [0, 100]);
  const clipPath = useTransform(hidden, (v) => "inset(0 " + v + "% 0 0)");
  const lineLeft = useTransform(hidden, (v) => 100 - v + "%");

  return (
    <div ref={trackRef} className="relative h-[250vh]">
      <div className="sticky top-0 flex h-svh items-center justify-center px-6">
        <div className="relative aspect-[3/2] w-full max-w-4xl overflow-hidden rounded-2xl bg-zinc-100">
          <Image src={after.src} alt={after.alt} fill sizes={SIZES} className="object-cover" />
          <motion.div className="absolute inset-0" style={{ clipPath }}>
            <Image src={before.src} alt={before.alt} fill sizes={SIZES} className="object-cover" />
          </motion.div>
          <motion.div
            aria-hidden="true"
            className="absolute inset-y-0 w-0.5 -translate-x-1/2 bg-white"
            style={{ left: lineLeft }}
          />
        </div>
      </div>
    </div>
  );
}

GSAP で作る場合

写真の切り替えのあとに文字や次の写真を続けて動かすなら、GSAP の ScrollTrigger で順番に並べる方が組みやすくなります。

Next.js での使い方は https://design-layer.com/ja/articles/nextjs-gsap-scrolltrigger-guide で解説しています。

ビフォーアフタースライダーでよくある失敗と公開前チェック

  • 2枚の比率や画角が違う。境目を動かすと建物や顔がずれて見える

  • 写真を押したときに、ブラウザの画像ドラッグが始まる。draggable={false} と select-none で止める

  • 外側の要素に overflow: hidden がある。sticky が効かず、応用の演出で写真が流れていく

  • 境目の初期位置が端にある。最初は50%前後にして、2枚あることがすぐ分かるようにする

  • Before と After のラベルがない。どちらが前かを写真の上に小さく書く

  • 動きを減らす設定を確かめていない。OS の設定を変えて、手で動かす表示に切り替わるか見る

写真を何枚も横に送って見せたい場合は、スライダーよりカルーセルが向いています。作り方は https://design-layer.com/ja/articles/nextjs-image-carousel-guide で解説しています。

完成したビフォーアフタースライダーをコピーして使う方法

DesignLayer の「ビフォーアフター・スクロール」の詳細ページ。左でライブプレビューを確かめ、右の「Cursor 用にコピー」でソースをコピーできる
DesignLayer の「ビフォーアフター・スクロール」の詳細ページ。左でライブプレビューを確かめ、右の「Cursor 用にコピー」でソースをコピーできる

スクロールで切り替わる演出は、写真の重ね方や留まる長さの調整に時間がかかります。完成したセクションを動かして確かめてから、自分のサイトに載せる方法もあります。

DesignLayer の「ビフォーアフター・スクロール」は、スクロールに合わせて Before から After へ写真がめくれるセクションです。続けて次の写真が広がり、仕上がりまでを3段階で見せます。

カタログのライブプレビューで動きを見てから、詳細ページの「Cursor 用にコピー」を押します。統合の指示・ライブプレビューの URL・本体の TSX・npm 依存がまとめてコピーされるので、Cursor や Claude に貼って使います。

閲覧とライブプレビューは無料で、アカウントもカードも要りません。無料のソースコピーは日本時間で1日3回までです。

Plus(月 990 円)と Pro(月 2,980 円)では、コンポーネントのコピーが無制限になります。カタログは https://design-layer.com/ja/components から開けます。

ビフォーアフタースライダーについてよくある質問

  • 必要ありません。CSS の clip-path と range 入力だけで作れます。

    スクロールに合わせて切り替える応用だけ、Motion や GSAP を使うと書きやすくなります。

まとめ:Next.js でビフォーアフタースライダーを作る手順

  • 写真を2枚重ね、上の Before を clip-path で切り取る

  • 境目の位置は range 入力で持ち、キーボードと読み上げに対応する

  • 指とマウスは Pointer Events で受け取り、touch-pan-y で縦のスクロールを残す

  • 2枚は同じ比率・同じ画角で用意し、next/image の fill と sizes で表示する

  • スクロールで切り替える演出は sticky と useScroll で作り、動きを減らす設定では手動に戻す

まずは Step1 のコードに、自分の写真を2枚入れてみてください。スクロールで要素を現す基本は https://design-layer.com/ja/articles/nextjs-scroll-reveal-guide で解説しています。

【Next.js】ビフォーアフタースライダーの作り方をドラッグとスクロール演出まで解説 | DesignLayer