All-Access · 22 templates$1270.97$99/year — save 92%Get access
  • HomeHome
  • TemplatesTemplates23
  • ComponentsComponents78
  • PricingPricing
  • Need a Website?Need a Website?New
Unlock All AccessUnlock All Access

Our latest template collection

Ready-to-launch sites for real estate agents, service businesses, and local brands — built to rank, load fast, and convert.

Explore All TemplatesExplore All Templates

Orvaine - Luxury Equestrian & Horse Breeding Website Template

Orvaine is a premium website template for stud farms, sport horse breeders and equestrian estates: horse pages with pedigrees, events, films. Next.js, Astro, Sanity.

Download Template
Orvaine - Luxury Equestrian & Horse Breeding Website Template

Maressa - Luxury Yacht Charter & Sales Website Template

Maressa is a premium website template for yacht charter, brokerage and superyacht brands: 21 editable sections, full-screen films, Next.js and Astro versions. Sanity.

Download Template
Maressa - Luxury Yacht Charter & Sales Website Template

Krishi – Agriculture & Farm Website Template

Krishi is a premium website template for farms, agritech and food brands: 17 editable sections, cinematic GSAP motion and both Next.js and Astro versions. Built with Sanity.

Download Template
Krishi – Agriculture & Farm Website Template
ComponentsCarousel

Curved 3D Carousel

View code
Full screen

A 3D curved carousel where cards grow and turn away from the centre, wrapping around with autoplay, keys and swipe. React and Tailwind CSS.

Curved3DCarousel.tsx
"use client";

// 3D curved carousel: cards grow and turn away the further they sit from the
// centre, the row wraps around, it autoplays (pausing on hover), and you can
// click a card, use the arrow keys or swipe.
// Pure Tailwind + React, no carousel library. Works with Tailwind v3 and v4.

import { useEffect, useRef, useState } from "react";

type Props = {
  heading?: string;
  images?: { src: string; alt: string }[];
  interval?: number;
};

const img = (id: string) => `https://images.unsplash.com/photo-${id}?w=900&q=80&auto=format&fit=crop`;

const DEFAULT_IMAGES = [
  { src: img("1574695272842-52dfdb7989d0"), alt: "Show jumping" },
  { src: img("1658298208155-ab71765747a1"), alt: "Free diver underwater" },
  { src: img("1695133996129-017af80651fc"), alt: "Horse clearing a fence" },
  { src: img("1664922114319-4700c0ef74b1"), alt: "Snorkelling over a reef" },
  { src: img("1559295928-6964f905e9c7"), alt: "Rider at a competition" },
  { src: img("1695133994223-02698c56f100"), alt: "Grand prix jump" },
  { src: img("1602016992941-311664c0306c"), alt: "Training in the arena" },
];

// Per distance from the centre: card height (% of the row) and how far it turns away
const SHAPES = [
  { height: 68, rotate: 0 },
  { height: 84, rotate: 16 },
  { height: 100, rotate: 24 },
];

export default function CurvedCarousel({ heading = "Life at the club", images = DEFAULT_IMAGES, interval = 3500 }: Props) {
  const count = images.length;
  const [active, setActive] = useState(Math.floor(count / 2));
  const [paused, setPaused] = useState(false);
  const touchX = useRef<number | null>(null);

  useEffect(() => {
    if (paused || count < 2 || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    const id = setInterval(() => setActive((i) => (i + 1) % count), interval);
    return () => clearInterval(id);
  }, [paused, count, interval]);

  const go = (step: number) => setActive((i) => (i + step + count) % count);

  // Shortest signed distance from the active card, so the row wraps around
  const offsetOf = (i: number) => {
    let d = i - active;
    if (d > count / 2) d -= count;
    if (d < -count / 2) d += count;
    return d;
  };

  return (
    <section className="overflow-hidden bg-[#f3f3f3] py-20 font-['Inter',ui-sans-serif,system-ui,sans-serif] text-black antialiased md:py-28">
      <h2 className="mx-auto max-w-7xl px-4 text-4xl font-medium md:px-8 md:text-5xl">{heading}</h2>

      <div
        className="relative mt-12 aspect-[4/3] cursor-grab touch-pan-y select-none [perspective:1200px] active:cursor-grabbing sm:aspect-[2/1] lg:aspect-[1440/560]"
        onMouseEnter={() => setPaused(true)}
        onMouseLeave={() => setPaused(false)}
        onKeyDown={(e) => {
          if (e.key === "ArrowRight") go(1);
          if (e.key === "ArrowLeft") go(-1);
        }}
        onTouchStart={(e) => (touchX.current = e.touches[0].clientX)}
        onTouchEnd={(e) => {
          if (touchX.current === null) return;
          const dx = e.changedTouches[0].clientX - touchX.current;
          if (Math.abs(dx) > 40) go(dx < 0 ? 1 : -1);
          touchX.current = null;
        }}
        role="region"
        aria-roledescription="carousel"
        aria-label="Photos"
      >
        {images.map((image, i) => {
          const offset = offsetOf(i);
          const distance = Math.abs(offset);
          const shape = SHAPES[Math.min(distance, 2)];
          const hidden = distance > 2;
          return (
            <button
              key={image.src}
              type="button"
              onClick={() => setActive(i)}
              tabIndex={hidden ? -1 : 0}
              aria-label={`Show photo ${i + 1}: ${image.alt}`}
              aria-current={offset === 0}
              className={`absolute left-1/2 top-1/2 w-[62%] overflow-hidden rounded-2xl transition-all duration-[900ms] ease-[cubic-bezier(0.16,1,0.3,1)] sm:w-[40%] lg:w-[26.4%] ${hidden ? "pointer-events-none opacity-0" : "opacity-100"}`}
              style={{
                height: `${shape.height}%`,
                transform: `translate(calc(-50% + ${offset} * 109%), -50%) rotateY(${-Math.sign(offset) * shape.rotate}deg)`,
                zIndex: 10 - distance,
              }}
            >
              <img src={image.src} alt="" draggable={false} className="pointer-events-none h-full w-full object-cover" />
            </button>
          );
        })}
      </div>
    </section>
  );
}

How to use it

  1. Make sure your Next.js or React project has Tailwind CSS set up (v3 or v4 both work).
  2. Create components/Curved3DCarousel.tsx and paste the code.
  3. Import it into a page and replace the sample text and images with your own.
Spoty - Tennis Club & Sports Academy Template

From the full template

Spoty$79

Every page, Sanity CMS and SEO already set up. This section is one of 3 you can try free.

Get the full template Get the full template Live preview Live preview
Share
Category
CarouselGalleryAnimation
Template
Spoty
Dependencies
None, just React
Works with
Next.js, React, Tailwind CSS v3 & v4
Added
Oct 29, 2026

More from Spoty

See the full template
Inverting Stat Cards
Feature

Inverting Stat Cards

Gallery with Next Preview
Gallery

Gallery with Next Preview

More carousel

View all
Arched Portrait Carousel
Carousel

Arched Portrait Carousel

Centered Loop Slider
Carousel

Centered Loop Slider

Fanned Photo Statement
Animation

Fanned Photo Statement

Ready for a faster site that's easier to edit?

Book a free 30-minute call. We'll review your current site and send a fixed-price plan. No pressure, no pitch.

Book Free Call Book Free Call No commitment required

Fast Next.js + Sanity websites, built for you or ready to launch with our templates.

Book a Free Call Book a Free Call
Services
  • Web Design
  • UI/UX Design
  • Framer Dev
  • Webflow Dev
  • Custom Dev
  • Sanity CMS
Resources
  • Templates
  • Pricing
  • License
  • Components
  • Case Studies
Company
  • About Us
  • Contact
  • Privacy Policy
© 2026 rayso.studio — All rights reservedDesigned & built in-house Remote-first agency
Let's TalkLet's Talk