Carousel
Carousel is a React and Tailwind CSS media component from Motion Primitives, licensed MIT. Copy it and paste it into your project.
Motion Primitives
MIT
media
What it is
Carousel is a React and Tailwind CSS media component from Motion Primitives, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i lucide-react motion
Licence
This component comes from Motion Primitives and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Carousel
'use client';
import {
Children,
ReactNode,
createContext,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { motion, Transition, useMotionValue } from 'motion/react';
import { cn } from '@/lib/utils';
import { ChevronLeft, ChevronRight } from 'lucide-react';
export type CarouselContextType = {
index: number;
setIndex: (newIndex: number) => void;
itemsCount: number;
setItemsCount: (newItemsCount: number) => void;
disableDrag: boolean;
};
const CarouselContext = createContext<CarouselContextType | undefined>(
undefined
);
function useCarousel() {
const context = useContext(CarouselContext);
if (!context) {
throw new Error('useCarousel must be used within an CarouselProvider');
}
return context;
}
export type CarouselProviderProps = {
children: ReactNode;
initialIndex?: number;
onIndexChange?: (newIndex: number) => void;
disableDrag?: boolean;
};
function CarouselProvider({
children,
initialIndex = 0,
onIndexChange,
disableDrag = false,
}: CarouselProviderProps) {
const [index, setIndex] = useState<number>(initialIndex);
const [itemsCount, setItemsCount] = useState<number>(0);
const handleSetIndex = (newIndex: number) => {
setIndex(newIndex);
onIndexChange?.(newIndex);
};
useEffect(() => {
setIndex(initialIndex);
}, [initialIndex]);
return (
<CarouselContext.Provider
value={{
index,
setIndex: handleSetIndex,
itemsCount,
setItemsCount,
disableDrag,
}}
>
{children}
</CarouselContext.Provider>
);
}
export type CarouselProps = {
children: ReactNode;
className?: string;
initialIndex?: number;
index?: number;
onIndexChange?: (newIndex: number) => void;
disableDrag?: boolean;
};
function Carousel({
children,
className,
initialIndex = 0,
index: externalIndex,
onIndexChange,
disableDrag = false,
}: CarouselProps) {
const [internalIndex, setInternalIndex] = useState<number>(initialIndex);
const isControlled = externalIndex !== undefined;
const currentIndex = isControlled ? externalIndex : internalIndex;
const handleIndexChange = (newIndex: number) => {
if (!isControlled) {
setInternalIndex(newIndex);
}
onIndexChange?.(newIndex);
};
return (
<CarouselProvider
initialIndex={currentIndex}
onIndexChange={handleIndexChange}
disableDrag={disableDrag}
>
<div className={cn('group/hover relative', className)}>
<div className='overflow-hidden'>{children}</div>
</div>
</CarouselProvider>
);
}
export type CarouselNavigationProps = {
className?: string;
classNameButton?: string;
alwaysShow?: boolean;
};
function CarouselNavigation({
className,
classNameButton,
alwaysShow,
}: CarouselNavigationProps) {
const { index, setIndex, itemsCount } = useCarousel();
return (
<div
className={cn(
'pointer-events-none absolute left-[-12.5%] top-1/2 flex w-[125%] -translate-y-1/2 justify-between px-2',
className
)}
>
<button
type='button'
aria-label='Previous slide'
className={cn(
'pointer-events-auto h-fit w-fit rounded-full bg-zinc-50 p-2 transition-opacity duration-300 dark:bg-zinc-950',
alwaysShow
? 'opacity-100'
: 'opacity-0 group-hover/hover:opacity-100',
alwaysShow
? 'disabled:opacity-40'
: 'group-hover/hover:disabled:opacity-40',
classNameButton
)}
disabled={index === 0}
onClick={() => {
if (index > 0) {
setIndex(index - 1);
}
}}
>
<ChevronLeft
className='stroke-zinc-600 dark:stroke-zinc-50'
size={16}
/>
</button>
<button
type='button'
className={cn(
'pointer-events-auto h-fit w-fit rounded-full bg-zinc-50 p-2 transition-opacity duration-300 dark:bg-zinc-950',
alwaysShow
? 'opacity-100'
: 'opacity-0 group-hover/hover:opacity-100',
alwaysShow
? 'disabled:opacity-40'
: 'group-hover/hover:disabled:opacity-40',
classNameButton
)}
aria-label='Next slide'
disabled={index + 1 === itemsCount}
onClick={() => {
if (index < itemsCount - 1) {
setIndex(index + 1);
}
}}
>
<ChevronRight
className='stroke-zinc-600 dark:stroke-zinc-50'
size={16}
/>
</button>
</div>
);
}
export type CarouselIndicatorProps = {
className?: string;
classNameButton?: string;
};
function CarouselIndicator({
className,
classNameButton,
}: CarouselIndicatorProps) {
const { index, itemsCount, setIndex } = useCarousel();
return (
<div
className={cn(
'absolute bottom-0 z-10 flex w-full items-center justify-center',
className
)}
>
<div className='flex space-x-2'>
{Array.from({ length: itemsCount }, (_, i) => (
<button
key={i}
type='button'
aria-label={`Go to slide ${i + 1}`}
onClick={() => setIndex(i)}
className={cn(
'h-2 w-2 rounded-full transition-opacity duration-300',
index === i
? 'bg-zinc-950 dark:bg-zinc-50'
: 'bg-zinc-900/50 dark:bg-zinc-100/50',
classNameButton
)}
/>
))}
</div>
</div>
);
}
export type CarouselContentProps = {
children: ReactNode;
className?: string;
transition?: Transition;
};
function CarouselContent({
children,
className,
transition,
}: CarouselContentProps) {
const { index, setIndex, setItemsCount, disableDrag } = useCarousel();
const [visibleItemsCount, setVisibleItemsCount] = useState(1);
const dragX = useMotionValue(0);
const containerRef = useRef<HTMLDivElement>(null);
const itemsLength = Children.count(children);
useEffect(() => {
if (!containerRef.current) {
return;
}
const options = {
root: containerRef.current,
threshold: 0.5,
};
const observer = new IntersectionObserver((entries) => {
const visibleCount = entries.filter(
(entry) => entry.isIntersecting
).length;
setVisibleItemsCount(visibleCount);
}, options);
const childNodes = containerRef.current.children;
Array.from(childNodes).forEach((child) => observer.observe(child));
return () => observer.disconnect();
}, [children, setItemsCount]);
useEffect(() => {
if (!itemsLength) {
return;
}
setItemsCount(itemsLength);
}, [itemsLength, setItemsCount]);
const onDragEnd = () => {
const x = dragX.get();
if (x <= -10 && index < itemsLength - 1) {
setIndex(index + 1);
} else if (x >= 10 && index > 0) {
setIndex(index - 1);
}
};
return (
<motion.div
drag={disableDrag ? false : 'x'}
dragConstraints={
disableDrag
? undefined
: {
left: 0,
right: 0,
}
}
dragMomentum={disableDrag ? undefined : false}
style={{
x: disableDrag ? undefined : dragX,
}}
animate={{
translateX: `-${index * (100 / visibleItemsCount)}%`,
}}
onDragEnd={disableDrag ? undefined : onDragEnd}
transition={
transition || {
damping: 18,
stiffness: 90,
type: 'spring',
duration: 0.2,
}
}
className={cn(
'flex items-center',
!disableDrag && 'cursor-grab active:cursor-grabbing',
className
)}
ref={containerRef}
>
{children}
</motion.div>
);
}
export type CarouselItemProps = {
children: ReactNode;
className?: string;
};
function CarouselItem({ children, className }: CarouselItemProps) {
return (
<motion.div
className={cn(
'w-full min-w-0 shrink-0 grow-0 overflow-hidden',
className
)}
>
{children}
</motion.div>
);
}
export {
Carousel,
CarouselContent,
CarouselNavigation,
CarouselIndicator,
CarouselItem,
useCarousel,
};