【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 入力は透明にして、写真の上いっぱいに広げます。マウスでクリックやドラッグをすると、その位置まで境目が動きます。
"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 のファイルを、次のコードで置き換えます。
"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 で高さを決めます。
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 の手で動かすスライダーを出します。
"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 の「ビフォーアフター・スクロール」は、スクロールに合わせて 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 で解説しています。