【Next.js】画像スライダー・カルーセルの作り方を紹介|CSS と Embla
公開日: 2026年9月12日 · 読了目安 13 分
Next.js で画像スライダー(カルーセル)を作る方法を、ライブラリなしの CSS スクロールスナップと Embla Carousel の2通りのコードで解説。前へ・次へボタン、ドット、自動再生の停止、next/image での表示速度まで扱います。
画像スライダーは CSS のスクロールスナップから始める
Next.js の LP や商品ページに、写真を横に送る画像スライダーを置きたい。ライブラリを入れるべきか、自分で書くべきかで手が止まっている。
そんな人に向けた記事です。
画像スライダー(カルーセル)は、まず CSS のスクロールスナップで作るのがおすすめです。横スクロールに snap を付けるだけで、スワイプで1枚ずつ止まる動きが手に入ります。
ループや慣性のあるドラッグが必要になったら、そのときに Embla Carousel などのライブラリへ移ります。
この記事で分かること:
ライブラリなしで作る、CSS スクロールスナップの画像スライダー
前へ・次へボタンとドットの付け方
自動再生と、その止め方
Embla Carousel でループするカルーセルを作るコード
next/image で1枚目を速く表示する設定
カルーセルにするべきかを先に決める
カルーセルは、見えている1枚以外が隠れる部品です。2枚目以降は見られない前提で考えた方が安全です。
写真が3枚までなら、グリッドで並べた方が全部見てもらえます。
カルーセルが向いているのは、次のような場面です。
4枚以上の写真を、同じ大きさの枠に収めたい
商品の写真を、ユーザーが自分のペースで送って見たい
スマホで縦に長くしたくない
一方、スクロールに合わせて作品が切り替わる演出は、カルーセルとは別の部品です。そちらは https://design-layer.com/ja/articles/nextjs-scroll-gallery-guide で解説しています。
使いにくい画像スライダーになる3つの理由
見た目が整っていても、操作でつまずくスライダーは少なくありません。よくある原因は次の3つです。
前へ・次へのボタンが写真に埋もれて見えない、またはキーボードで押せない
スマホで指のスワイプが効かず、小さなボタンしか操作手段がない
写真ごとに高さが違い、送るたびにページ全体が上下にずれる
CSS のスクロールスナップは、2つ目の問題を最初から解決します。横スクロールはブラウザの機能なので、スワイプもトラックパッドもそのまま動きます。
残りの2つは、ボタンの作り方と枠の比率の固定で防ぎます。
CSS スクロールスナップで画像スライダーを作る
横に並べた写真の列には、横スクロールを許す overflow-x-auto と、止まる位置をそろえる snap-x snap-mandatory を付けます。
各写真には、中央で止める snap-center を付けます。これだけで、スワイプすると1枚ずつ中央で止まります。
ボタンとドットのために、今何枚目かを状態として覚えておきます。スクロールの位置を幅で割れば、何枚目かが分かります。
"use client";
import Image from "next/image";
import { useRef, useState } from "react";
const slides = [
{ src: "/carousel/01.jpg", alt: "木の机に置かれた白いマグカップ" },
{ src: "/carousel/02.jpg", alt: "マグカップを横から見た写真" },
{ src: "/carousel/03.jpg", alt: "マグカップの持ち手の拡大" },
{ src: "/carousel/04.jpg", alt: "色違いのマグカップが3つ並んだ写真" },
];
export function ImageCarousel() {
const trackRef = useRef<HTMLDivElement>(null);
const [index, setIndex] = useState(0);
const scrollToIndex = (i: number) => {
const track = trackRef.current;
if (!track) return;
const next = Math.max(0, Math.min(slides.length - 1, i));
track.scrollTo({ left: next * track.clientWidth, behavior: "smooth" });
};
const onScroll = () => {
const track = trackRef.current;
if (!track) return;
setIndex(Math.round(track.scrollLeft / track.clientWidth));
};
return (
<section
aria-roledescription="carousel"
aria-label="商品の写真"
className="mx-auto max-w-4xl px-6 py-24"
>
<div
ref={trackRef}
onScroll={onScroll}
className="flex snap-x snap-mandatory overflow-x-auto rounded-2xl [scrollbar-width:none]"
>
{slides.map((slide, i) => (
<div
key={slide.src}
role="group"
aria-roledescription="slide"
aria-label={`${i + 1} / ${slides.length}`}
className="relative aspect-[3/2] w-full shrink-0 snap-center"
>
<Image
src={slide.src}
alt={slide.alt}
fill
sizes="(min-width: 896px) 848px, 100vw"
className="object-cover"
priority={i === 0}
/>
</div>
))}
</div>
<div className="mt-4 flex items-center justify-between">
<button
type="button"
onClick={() => scrollToIndex(index - 1)}
disabled={index === 0}
aria-label="前の写真"
className="grid h-10 w-10 place-items-center rounded-full border border-zinc-300 disabled:opacity-30"
>
←
</button>
<div className="flex gap-2">
{slides.map((slide, i) => (
<button
key={slide.src}
type="button"
aria-label={`${i + 1} 枚目を表示`}
aria-current={i === index}
onClick={() => scrollToIndex(i)}
className={`h-2 w-2 rounded-full transition-colors ${i === index ? "bg-zinc-900" : "bg-zinc-300"}`}
/>
))}
</div>
<button
type="button"
onClick={() => scrollToIndex(index + 1)}
disabled={index === slides.length - 1}
aria-label="次の写真"
className="grid h-10 w-10 place-items-center rounded-full border border-zinc-300 disabled:opacity-30"
>
→
</button>
</div>
</section>
);
}スライダーの枠の比率を固定してページのずれを防ぐ
各スライドに aspect-[3/2] を付け、写真は fill と object-cover で枠いっぱいに表示します。元の写真の比率がばらばらでも、枠の高さは変わりません。
送るたびにページが上下にずれる問題は、これで起きなくなります。
前へ・次へボタンは写真の外に置く
前へ・次へのボタンを写真の上に重ねると、明るい写真では見えなくなります。このコードでは、写真の下にボタンとドットを並べています。
写真の上に置きたいときは、白い丸の背景と影を付けます。どの写真の上でも見分けられるようにするためです。
最初と最後の1枚では、ボタンに disabled を付けて薄く表示します。押しても何も起きないボタンを残すより、押せないことが分かる方が親切です。
カルーセルのドットは今の位置を aria-current で伝える
ドットは見た目だけでなく、読み上げソフトにも今何枚目かを伝えます。aria-current を今の1枚に付け、aria-label に「3 枚目を表示」のような文言を入れます。
カルーセルを自動再生するなら停止ボタンを先に作る
画像スライダーを自動で送りたいという要望はよくあります。ただ、読んでいる途中で写真が変わると、ユーザーは操作を奪われたと感じます。
自動再生を付けるなら、次の3つを必ず揃えます。
停止・再生のボタンを置く
マウスを乗せている間と、キーボードのフォーカスがある間は止める
端末で動きを減らす設定をしている人には、最初から自動再生しない
商品写真なら、自動で送るよりユーザーが自分で送る方が、じっくり見てもらえます。自動再生は、ブランドの雰囲気を見せる写真など、どこで止めても問題ない場面に絞ります。
const [playing, setPlaying] = useState(true);
const [hovered, setHovered] = useState(false);
useEffect(() => {
if (!playing || hovered) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const id = window.setInterval(() => {
const track = trackRef.current;
if (!track) return;
const current = Math.round(track.scrollLeft / track.clientWidth);
const next = (current + 1) % slides.length;
track.scrollTo({ left: next * track.clientWidth, behavior: "smooth" });
}, 5000);
return () => window.clearInterval(id);
}, [playing, hovered]);
// section に付ける
// onMouseEnter={() => setHovered(true)}
// onMouseLeave={() => setHovered(false)}
// onFocus={() => setHovered(true)}
// onBlur={() => setHovered(false)}
// ボタンの並びに足す
<button
type="button"
onClick={() => setPlaying((p) => !p)}
className="rounded-full border border-zinc-300 px-4 py-2 text-sm"
>
{playing ? "自動再生を止める" : "自動再生する"}
</button>スライダーの送る間隔は 5 秒以上にする
写真に短い説明文が付いている場合、3 秒では読み終わりません。5 秒以上を目安にし、説明文が長いならさらに延ばします。
なお、ファイル先頭の import に useEffect を足すのを忘れないでください。
ループするカルーセルは Embla Carousel で作る
次のような動きが必要になったら、ライブラリを使った方が早く確実です。
最後の1枚から最初に戻るループ
指を離した後も少し滑る慣性
サムネイルとの連動
Embla Carousel は見た目を持たない軽いライブラリで、Tailwind CSS で自由に見た目を付けられます。shadcn/ui の Carousel も Embla を土台にしています。
インストールは npm i embla-carousel-react です。外側の箱に ref を渡し、中に flex の列を置く、という形を守ります。
"use client";
import Image from "next/image";
import useEmblaCarousel from "embla-carousel-react";
type Slide = { src: string; alt: string };
export function EmblaCarousel({ slides }: { slides: Slide[] }) {
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true });
return (
<section aria-roledescription="carousel" aria-label="作品の写真" className="mx-auto max-w-4xl px-6 py-24">
<div ref={emblaRef} className="overflow-hidden rounded-2xl">
<div className="flex">
{slides.map((slide, i) => (
<div key={slide.src} className="relative aspect-[3/2] min-w-0 flex-[0_0_100%]">
<Image
src={slide.src}
alt={slide.alt}
fill
sizes="(min-width: 896px) 848px, 100vw"
className="object-cover"
priority={i === 0}
/>
</div>
))}
</div>
</div>
<div className="mt-4 flex justify-between">
<button type="button" aria-label="前の写真" onClick={() => emblaApi?.scrollPrev()}>
←
</button>
<button type="button" aria-label="次の写真" onClick={() => emblaApi?.scrollNext()}>
→
</button>
</div>
</section>
);
}Embla のカルーセルにドットを付けるには
写真が切り替わったことは、emblaApi.on("select", ...) で受け取ります。
続けて emblaApi.selectedScrollSnap() で今の位置を取り出し、状態として覚えます。
ドットの描画は、CSS スナップの例と同じ形で書けます。
カルーセルだけをブラウザで動く部品にする
Embla は、ブラウザ側で状態を扱う React の仕組み(hooks)を使います。そのため、ファイルの先頭に "use client"(この部品はブラウザで動かす、という印)が必要です。
ページ本体の page.tsx は、サーバーで組み立てる部品のままにします。写真の配列は、部品に渡す値(props)として渡します。
ページ全体をブラウザで動かさないことで、他のセクションの読み込みが重くなりません。
CSS スナップと Embla、カルーセルの選び方
どちらを使うかは、必要な動きで決めます。迷ったら CSS スナップで作り、足りなくなってから移っても遅くありません。
CSS スナップが向いている: 1枚ずつ送るだけ、依存を増やしたくない、スワイプはブラウザ任せでよい
Embla が向いている: ループさせたい、ドラッグの慣性がほしい、サムネイルや自動再生のプラグインを使いたい
どちらでもない: スクロールに合わせて写真が切り替わる演出は、スクロール連動のギャラリーで作る
完成したカルーセル・ギャラリーをコピーして使う方法

画面にピン留めされたまま写真が切り替わる作品の列や、ホバーで写真が広がる並びは、通常のカルーセルでは再現できません。
まず実際の動きを見て、どの種類が合うかを決めるのが近道です。
DesignLayer は、人が組んだ React + Tailwind のモーション付きセクションを、ブラウザでライブプレビューしてからコピーできるカタログです。
閲覧はアカウントもカードも不要です。https://design-layer.com/ja/components
無料サンプル10個を無料アカウントで回数制限なくコピーでき、動画プロンプトは月1回です。Free では MCP に接続できません。カタログ全体は30日ビルドパス($19の1回払い、自動更新なし)、Lifetime Access(先着100名の$79の1回払い)、Plus(月額 $9、いつでも解約可)です。コピーした後は、写真のパスと alt だけを差し替えます。
画像スライダー・カルーセルについてよくある質問
Web 制作ではほぼ同じ意味で使われます。どちらも、決まった枠の中で画像を横に送って切り替える部品を指します。
この記事では両方の呼び方を同じものとして扱っています。
まとめ:Next.js で画像スライダーを作る手順
写真が3枚までならグリッド、4枚以上を1つの枠に収めたいときにカルーセルを使う
まずは CSS のスクロールスナップで作り、スワイプはブラウザに任せる
枠の比率を固定し、ボタンは写真の外か見分けやすい背景付きにする
自動再生には停止ボタン・ホバー中の停止・動きを減らす設定への対応を揃える
ループや慣性が必要になったら Embla Carousel に移る
まずはこの記事の ImageCarousel を貼り、自分の写真に差し替えてスマホで送ってみてください。
スクロールで切り替わる見せ方は https://design-layer.com/ja/articles/nextjs-scroll-gallery-guide 、写真の上にホバーで情報を出すなら https://design-layer.com/ja/articles/nextjs-hover-card-guide が参考になります。


