Curved Carousel

An infinite scrolling carousel that appears curved

Slide 1
Slide 2
Slide 3
Slide 4
Slide 5
Slide 6
Slide 7
Slide 8
Slide 9
Slide 10
Slide 11
Slide 12
Slide 13
Slide 14
Slide 15

Installation

1

Install dependencies

npm install framer-motion tailwind-merge clsx
2

Add util file

lib/utils.ts

import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
3

Add the following code in the tailwind.config.js file

import type { Config } from "tailwindcss";

export default {
  darkMode: ["class"],
  content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      animation: {
        carousel: "carousel var(--duration) linear infinite",
      },
      keyframes: {
        carousel: {
          from: { transform: "translateX(0)" },
          to: { transform: "translateX(calc(-100% - var(--gap)))" },
        },
      },
      // other code
    }
  },
  plugins: [require("tailwindcss-animate")],
} satisfies Config;
4

Copy the source code

components/ui/curved-carousel.tsx
'use client';
import Image from 'next/image';
import React from 'react';

type CurvedCarouselProps = {
  imageSrcs: string[];
  repeat?: number;
}

export function CurvedCarousel ({ 
    imageSrcs, 
    repeat = 3 
}: CurvedCarouselProps) {
  const images = Array(repeat).fill(imageSrcs).flat();

  return (
    <div className="w-full h-full flex justify-center items-center bg-background">
      <div className="relative w-full h-full overflow-hidden">
        <div className="absolute z-10 left-1/2 w-[500%] h-[500%] bg-background rounded-[50%] -translate-x-1/2 -top-[490%]" />
        
        <div className="absolute left-0 top-0 w-32 h-full z-20 backdrop-blur-[1px] dark:bg-gradient-to-r from-background to-transparent pointer-events-none" />
        <div className="absolute right-0 top-0 w-32 h-full z-20 backdrop-blur-[1px] dark:bg-gradient-to-l from-background to-transparent pointer-events-none" />
        
        <div className="flex [--duration:40s] [--gap:0px] [gap:var(--gap)]">
          <div className="flex animate-carousel">
            {images.map((src, index) => (
              <div key={`slide-${index}`} className="relative flex-none w-1/3">
                <div className="w-full h-full border-8 border-background">
                  <Image
                    src={src}
                    alt={`Slide ${index + 1}`}
                    width={400}
                    height={900}
                    className="w-full h-full object-cover"
                  />
                </div>
              </div>
            ))}
          </div>
        </div>
        
        <div className="absolute z-10 left-1/2 w-[500%] h-[500%] bg-background rounded-[50%] -translate-x-1/2 -bottom-[490%]" />
      </div>
    </div>
  );
};

Props

PropTypeDescriptionDefault Value
imageSrcsstring[]The array of image sources that the carousel will displayundefined
repeatnumberNumber of times to repeat the array of images or elements for more seamless scrolling3

Inspired by kyu