
お好きな金額でサポートしていただけると嬉しいです!
(50円〜10,000円[税込]まで)
作成日時:2026年5月8日 02:15
更新日時:2026年5月8日 04:05
React
Next.js
昔々、カルーセルの実装には Splide.js を使っていました(いたことがありました笑)。
その当時はメンテナンスが続いており、特に不満もなかったのですが、時は過ぎたり、最終リリースが 2024年9月(v4.1.3) で止まっているではありませんか(浦島太郎)。
そこで今回は、改めて他にどんなのがあるのかな?と思い、そんなに新しくもないのですが、Embla Carousel を触ってみましたので共有です。
カルーセルライブラリはいくつか候補がありますが、代表的なものを比較すると以下のようになるかと思います。
Splide.js | Swiper | Embla Carousel | |
|---|---|---|---|
サイズ(min+gzip) | ~12 KB | ~45 KB | ~7 KB |
React対応 | ラッパー型 | ラッパー型 | フック型 |
デフォルトCSS | あり | あり | なし |
SSR対応 | △ | ○ | ○ |
メンテナンス状況 | 停滞中 | 活発 | 活発 |
プラグイン | 豊富 | 豊富 | 豊富 |
Embla Carouselを触ってみようと選んだ理由はシンプルで、バンドルサイズが最小クラス、useEmblaCarousel フック1つで実装できて、過去触っていたラッパー型のSplide.jsよりもNext.jsやReactとの統合が楽そうだったため。emblaApi を通じたAPI操作で自然に統合できます。
とは言っても、フレームワークに依存しないライブラリになっていて、「動くものをすぐ用意してくれる」タイプのライブラリではなく、「スクロールの挙動だけ提供するので、UIは自分で作る」というタイプのライブラリにあたります。
なので、デフォルトCSSがなくスタイルの競合が起きず、既存のスタイリング方法をそのまま使えるのも魅力的。TypeScriptにも対応しています。
pnpm add embla-carousel-react
# 自動再生が必要な場合は以下も
pnpm add embla-carousel-autoplay
Embla Carouselは 3階層の構造 が前提です。
最外層ラッパー
└── ビューポート(overflow: hidden)← ref を渡す
└── コンテナ(display: flex)
└── スライド × n
ビューポートに overflow: hidden を当てるのがポイント。最外層に当ててしまうとナビゲーションボタンまでクリッピングされるので注意してください。
/* 最低限必要なCSS */
.viewport {
overflow: hidden;
}
.container {
display: flex;
touch-action: pan-y pinch-zoom; /* モバイルでの縦スクロールと競合させないように */
}
.slide {
flex: 0 0 100%;
min-width: 0;
}
各サンプルは、すべて以下の型を例として前提においてまとめます。
type Slide = {
id: number;
src: string;
alt: string;
};
type Props = {
slides: Slide[];
};
もっともシンプルな実装。前後ボタンだけのカルーセルです。
"use client";
import useEmblaCarousel from "embla-carousel-react";
export const Carousel = ({ slides }: Props) => {
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true });
return (
<div className="relative">
<div ref={emblaRef} className="overflow-hidden">
<div className="flex touch-pan-y">
{slides.map((slide) => (
<div key={slide.id} className="flex-[0_0_100%] min-w-0">
<img src={slide.src} alt={slide.alt} className="w-full" />
</div>
))}
</div>
</div>
<button
className="absolute left-2 top-1/2 -translate-y-1/2"
onClick={() => emblaApi?.goToPrev()}
>
←
</button>
<button
className="absolute right-2 top-1/2 -translate-y-1/2"
onClick={() => emblaApi?.goToNext()}
>
→
</button>
</div>
);
};
現在のスライド位置をドットで示すパターンです。snapList() でスナップ位置の数を取得し、selectedSnap() で現在位置を取得できます。
"use client";
import { useCallback, useEffect, useState } from "react";
import useEmblaCarousel from "embla-carousel-react";
export const CarouselWithDots = ({ slides }: Props) => {
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true });
const [selectedIndex, setSelectedIndex] = useState(0);
const [scrollSnaps, setScrollSnaps] = useState<number[]>([]);
const onInit = useCallback(() => {
if (!emblaApi) return;
setScrollSnaps(emblaApi.snapList());
}, [emblaApi]);
const onSelect = useCallback(() => {
if (!emblaApi) return;
setSelectedIndex(emblaApi.selectedSnap());
}, [emblaApi]);
useEffect(() => {
if (!emblaApi) return;
onInit();
onSelect();
emblaApi.on("reinit", onInit);
emblaApi.on("reinit", onSelect);
emblaApi.on("select", onSelect);
}, [emblaApi, onInit, onSelect]);
return (
<div className="relative">
<div ref={emblaRef} className="overflow-hidden">
<div className="flex touch-pan-y">
{slides.map((slide) => (
<div key={slide.id} className="flex-[0_0_100%] min-w-0">
<img src={slide.src} alt={slide.alt} className="w-full" />
</div>
))}
</div>
</div>
<button
className="absolute left-2 top-1/2 -translate-y-1/2"
onClick={() => emblaApi?.goToPrev()}
>
←
</button>
<button
className="absolute right-2 top-1/2 -translate-y-1/2"
onClick={() => emblaApi?.goToNext()}
>
→
</button>
<div className="flex justify-center gap-2 mt-4">
{scrollSnaps.map((_, index) => (
<button
key={index}
className={`w-2 h-2 rounded-full transition-colors ${
index === selectedIndex ? "bg-gray-800" : "bg-gray-300"
}`}
onClick={() => emblaApi?.goTo(index)}
/>
))}
</div>
</div>
);
};
embla-carousel-autoplay プラグインを第2引数に渡すだけで自動再生もできます。delay で間隔(ms)を指定します。
"use client";
import useEmblaCarousel from "embla-carousel-react";
import Autoplay from "embla-carousel-autoplay";
export const AutoplayCarousel = ({ slides }: Props) => {
const [emblaRef] = useEmblaCarousel({ loop: true }, [
Autoplay({ delay: 3000 })
]);
return (
<div className="relative">
<div ref={emblaRef} className="overflow-hidden">
<div className="flex touch-pan-y">
{slides.map((slide) => (
<div key={slide.id} className="flex-[0_0_100%] min-w-0">
<img src={slide.src} alt={slide.alt} className="w-full" />
</div>
))}
</div>
</div>
</div>
);
};
slidesToScroll と breakpoints を組み合わせると、画面幅に応じて表示枚数を変えられます。
const [emblaRef] = useEmblaCarousel({
loop: true,
slidesToScroll: 1,
breakpoints: {
"(min-width: 768px)": { slidesToScroll: 2 },
"(min-width: 1024px)": { slidesToScroll: 3 }
}
});
スライドの幅はCSSで指定。
{/* 1枚表示 → md:2枚 → lg:3枚 */}
<div className="flex-[0_0_100%] md:flex-[0_0_50%] lg:flex-[0_0_33.33%] min-w-0">
制限というほどでもなくあるあるですが、useEmblaCarousel は内部で useRef を使っています。
そのため、必ずクライアントコンポーネントになります。カルーセルのコンポーネントだけを "use client" に閉じて切り出して使うようにましょう。
// app/page.tsx(Server Component)
import { Carousel } from "@/components/Carousel";
export default async function Page() {
const slides = await fetchSlides();
return <Carousel slides={slides} />;
}
// components/Carousel.tsx(Client Component)
"use client";
import useEmblaCarousel from "embla-carousel-react";
// ...
公式ドキュメントはこちら。
それでは、また。
HTNCode