← Back
UI

Carousel

Drag-to-swipe slider with auto-play, arrow buttons, dot indicators, and spring ease.

01 / 05

Design Systems

Consistent, scalable UI

02 / 05

Motion Design

Bring interfaces to life

03 / 05

Data Visualization

Make data beautiful

04 / 05

Micro-interactions

Delight in the details

05 / 05

3D on the Web

CSS & WebGL techniques

drag to swipe · auto-play · dot nav · spring ease

Carousel.tsx105 lines
"use client";

import { useEffect, useState } from "react";

const SLIDES = [
  { bg: "from-violet-600 to-indigo-700", label: "01", title: "Design Systems",  sub: "Consistent, scalable UI" },
  { bg: "from-sky-600   to-blue-700",    label: "02", title: "Motion Design",    sub: "Bring interfaces to life" },
  { bg: "from-emerald-600 to-teal-700",  label: "03", title: "Data Visualization", sub: "Make data beautiful" },
  { bg: "from-amber-500  to-orange-600", label: "04", title: "Micro-interactions", sub: "Delight in the details" },
  { bg: "from-pink-500   to-rose-600",   label: "05", title: "3D on the Web",    sub: "CSS & WebGL techniques" },
];

export default function Carousel() {
  const [current, setCurrent] = useState(0);
  const [dragging, setDragging] = useState(false);
  const [startX, setStartX] = useState(0);
  const [autoplay, setAutoplay] = useState(true);

  const n = SLIDES.length;
  const prev = () => setCurrent((c) => (c - 1 + n) % n);
  const next = () => setCurrent((c) => (c + 1) % n);

  useEffect(() => {
    if (!autoplay) return;
    const id = setInterval(next, 3000);
    return () => clearInterval(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [autoplay, current]);

  function onPointerDown(e: React.PointerEvent) {
    setDragging(true);
    setStartX(e.clientX);
    setAutoplay(false);
    (e.target as HTMLElement).setPointerCapture(e.pointerId);
  }

  function onPointerUp(e: React.PointerEvent) {
    if (!dragging) return;
    setDragging(false);
    const dx = e.clientX - startX;
    if (dx < -40) next();
    else if (dx > 40) prev();
  }

  return (
    <div className="w-full flex flex-col gap-5 py-4">
      {/* slider */}
      <div
        className="relative overflow-hidden rounded-2xl cursor-grab active:cursor-grabbing"
        style={{ height: 220 }}
        onPointerDown={onPointerDown}
        onPointerUp={onPointerUp}
      >
        <div
          className="flex h-full transition-transform duration-500 ease-[cubic-bezier(0.16,1,0.3,1)]"
          style={{ transform: `translateX(-${current * 100}%)`, width: `${n * 100}%` }}
        >
          {SLIDES.map((s) => (
            <div
              key={s.label}
              className={`h-full bg-gradient-to-br ${s.bg} flex flex-col items-start justify-end p-8 select-none`}
              style={{ width: `${100 / n}%` }}
            >
              <span className="text-white/30 text-xs font-mono mb-1">{s.label} / 0{n}</span>
              <h2 className="text-2xl font-black text-white">{s.title}</h2>
              <p className="text-white/60 text-sm mt-1">{s.sub}</p>
            </div>
          ))}
        </div>

        {/* arrows */}
        <button
          onClick={(e) => { e.stopPropagation(); prev(); setAutoplay(false); }}
          className="absolute left-3 top-1/2 -translate-y-1/2 w-9 h-9 rounded-full bg-black/30 backdrop-blur-sm text-white flex items-center justify-center hover:bg-black/50 transition-colors text-sm"
        >

        </button>
        <button
          onClick={(e) => { e.stopPropagation(); next(); setAutoplay(false); }}
          className="absolute right-3 top-1/2 -translate-y-1/2 w-9 h-9 rounded-full bg-black/30 backdrop-blur-sm text-white flex items-center justify-center hover:bg-black/50 transition-colors text-sm"
        >

        </button>
      </div>

      {/* dots */}
      <div className="flex justify-center gap-2">
        {SLIDES.map((_, i) => (
          <button
            key={i}
            onClick={() => { setCurrent(i); setAutoplay(false); }}
            className={`h-1.5 rounded-full transition-all duration-300 ${
              i === current ? "w-6 bg-violet-500" : "w-1.5 bg-zinc-300 dark:bg-zinc-700"
            }`}
          />
        ))}
      </div>

      <p className="text-center text-[10px] font-mono text-zinc-400 dark:text-zinc-600">
        drag to swipe · auto-play · dot nav · spring ease
      </p>
    </div>
  );
}

More in UI