MC Carousel
A component for cycling through a series of content.
Overview
The mc-carousel is a flexible carousel component for the MicroClub UI library. Built on embla-carousel-react, it provides smooth, performant scrolling through slides with navigation controls.
Preview
Loading...
import { McCarousel, McCarouselContent, McCarouselItem, McCarouselNext, McCarouselPrevious,} from '../ui/mc-carousel';export default function McCarouselDemo() { return ( <McCarousel className="w-full max-w-xs"> <McCarouselContent> {Array.from({ length: 5 }).map((_, index) => ( <McCarouselItem key={index}> <div className="flex aspect-square items-center justify-center p-6"> <span className="text-4xl font-semibold">{index + 1}</span> </div> </McCarouselItem> ))} </McCarouselContent> <McCarouselPrevious /> <McCarouselNext /> </McCarousel> );}Add to Your Project
Deploy the mc-carousel directly to your components/ui directory:
npx mcoli-ui@latest add mc-carousel
pnpm dlx mcoli-ui@latest add mc-carousel
yarn dlx mcoli-ui@latest add mc-carousel
bunx mcoli-ui@latest add mc-carousel
Manual Setup
If you prefer manual control:
Install the core dependencies
npm install embla-carousel-react lucide-react
pnpm add embla-carousel-react lucide-react
yarn add embla-carousel-react lucide-react
bun add embla-carousel-react lucide-react
Copy the source code
'use client';import * as React from 'react';import useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react';import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';import { cn } from '@/lib/utils';import { Button } from '@/components/ui/button';type McCarouselApi = UseEmblaCarouselType[1];type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;type CarouselOptions = UseCarouselParameters[0];type CarouselPlugin = UseCarouselParameters[1];type CarouselProps = { opts?: CarouselOptions; plugins?: CarouselPlugin; orientation?: 'horizontal' | 'vertical'; setApi?: (api: McCarouselApi) => void;};type CarouselContextProps = { carouselRef: ReturnType<typeof useEmblaCarousel>[0]; api: ReturnType<typeof useEmblaCarousel>[1]; scrollPrev: () => void; scrollNext: () => void; canScrollPrev: boolean; canScrollNext: boolean;} & CarouselProps;const McCarouselContext = React.createContext<CarouselContextProps | null>(null);function useMcCarousel() { const context = React.useContext(McCarouselContext); if (!context) { throw new Error('useMcCarousel must be used within a <McCarousel />'); } return context;}function McCarousel({ orientation = 'horizontal', opts, setApi, plugins, className, children, ...props}: React.ComponentProps<'div'> & CarouselProps) { const [carouselRef, api] = useEmblaCarousel( { ...opts, axis: orientation === 'horizontal' ? 'x' : 'y', }, plugins ); const [canScrollPrev, setCanScrollPrev] = React.useState(false); const [canScrollNext, setCanScrollNext] = React.useState(false); const onSelect = React.useCallback((api: McCarouselApi) => { if (!api) return; setCanScrollPrev(api.canScrollPrev()); setCanScrollNext(api.canScrollNext()); }, []); const scrollPrev = React.useCallback(() => { api?.scrollPrev(); }, [api]); const scrollNext = React.useCallback(() => { api?.scrollNext(); }, [api]); const handleKeyDown = React.useCallback( (event: React.KeyboardEvent<HTMLDivElement>) => { if (event.key === 'ArrowLeft') { event.preventDefault(); scrollPrev(); } else if (event.key === 'ArrowRight') { event.preventDefault(); scrollNext(); } }, [scrollPrev, scrollNext] ); React.useEffect(() => { if (!api || !setApi) return; setApi(api); }, [api, setApi]); React.useEffect(() => { if (!api) return; onSelect(api); api.on('reInit', onSelect); api.on('select', onSelect); return () => { api?.off('select', onSelect); }; }, [api, onSelect]); return ( <McCarouselContext.Provider value={{ carouselRef, api: api, opts, orientation: orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'), scrollPrev, scrollNext, canScrollPrev, canScrollNext, }} > <div onKeyDownCapture={handleKeyDown} className={cn('relative', className)} role="region" aria-roledescription="carousel" data-slot="carousel" {...props} > {children} </div> </McCarouselContext.Provider> );}function McCarouselContent({ className, ...props }: React.ComponentProps<'div'>) { const { carouselRef, orientation } = useMcCarousel(); return ( <div ref={carouselRef} className="overflow-hidden " data-slot="carousel-content"> <div className={cn('flex', orientation === 'horizontal' ? '' : '-mt-4 flex-col', className)} {...props} /> </div> );}function McCarouselItem({ className, children, ...props }: React.ComponentProps<'div'>) { const { orientation } = useMcCarousel(); return ( <div role="group" aria-roledescription="slide" data-slot="carousel-item" className={cn( ' shrink-0 grow-0 basis-full', orientation === 'horizontal' ? ' ' : '', className )} {...props} > {/* Card shell matching Figma "Wrapper" spec exactly */} <div data-slot="carousel-item-card" className="flex h-full w-full flex-col items-center justify-center gap-[8.14px] rounded-xl border border-border bg-card text-card-foreground shadow" > {children} </div> </div> );}function McCarouselPrevious({ className, variant = 'outline', size = 'icon-sm', ...props}: React.ComponentProps<typeof Button>) { const { orientation, scrollPrev, canScrollPrev } = useMcCarousel(); return ( <Button data-slot="carousel-previous" variant={variant} size={size} className={cn( 'absolute size-8.5 touch-manipulation rounded-full border border-border bg-card p-2.5 shadow-[0px_0.81px_2.44px_0px_rgba(0,0,0,0.1)]', orientation === 'horizontal' ? 'inset-y-0 -left-12 my-auto' : '-top-12 -right-[-140px] rotate-90', className )} disabled={!canScrollPrev} onClick={scrollPrev} {...props} > <ChevronLeftIcon className="cn-rtl-flip" /> <span className="sr-only">Previous slide</span> </Button> );}function McCarouselNext({ className, variant = 'outline', size = 'icon-sm', ...props}: React.ComponentProps<typeof Button>) { const { orientation, scrollNext, canScrollNext } = useMcCarousel(); return ( <Button data-slot="carousel-next" variant={variant} size={size} className={cn( 'absolute size-[34px] touch-manipulation rounded-full border-[1px] border-border bg-card p-[10px] ', orientation === 'horizontal' ? 'inset-y-0 -right-12 my-auto' : '-bottom-12 -right-[-140px] rotate-90', className )} disabled={!canScrollNext} onClick={scrollNext} {...props} > <ChevronRightIcon className="cn-rtl-flip" /> <span className="sr-only">Next slide</span> </Button> );}export { type McCarouselApi, McCarousel, McCarouselContent, McCarouselItem, McCarouselPrevious, McCarouselNext, useMcCarousel,};Copy the example
import { McCarousel, McCarouselContent, McCarouselItem, McCarouselNext, McCarouselPrevious,} from '../ui/mc-carousel';export default function McCarouselDemo() { return ( <McCarousel className="w-full max-w-xs"> <McCarouselContent> {Array.from({ length: 5 }).map((_, index) => ( <McCarouselItem key={index}> <div className="flex aspect-square items-center justify-center p-6"> <span className="text-4xl font-semibold">{index + 1}</span> </div> </McCarouselItem> ))} </McCarouselContent> <McCarouselPrevious /> <McCarouselNext /> </McCarousel> );}Usage
import {
McCarousel,
McCarouselContent,
McCarouselItem,
McCarouselPrevious,
McCarouselNext,
} from '@/components/ui/mc-carousel';
export default function Example() {
return (
<McCarousel className="w-full max-w-xs">
<McCarouselContent>
{Array.from({ length: 5 }).map((_, index) => (
<McCarouselItem key={index}>
<div className="flex aspect-square items-center justify-center p-6">
<span className="text-4xl font-semibold">{index + 1}</span>
</div>
</McCarouselItem>
))}
</McCarouselContent>
<McCarouselPrevious />
<McCarouselNext />
</McCarousel>
);
}Examples
Basic Carousel
<McCarousel className="w-full max-w-xs">
<McCarouselContent>
{Array.from({ length: 5 }).map((_, index) => (
<McCarouselItem key={index}>
<div className="flex aspect-square items-center justify-center p-6">
<span className="text-4xl font-semibold">{index + 1}</span>
</div>
</McCarouselItem>
))}
</McCarouselContent>
<McCarouselPrevious />
<McCarouselNext />
</McCarousel>Vertical Orientation
<McCarousel orientation="vertical" className="w-full max-w-xs">
<McCarouselContent>
{Array.from({ length: 5 }).map((_, index) => (
<McCarouselItem key={index}>
<div className="flex aspect-square items-center justify-center p-6">
<span className="text-4xl font-semibold">{index + 1}</span>
</div>
</McCarouselItem>
))}
</McCarouselContent>
<McCarouselPrevious />
<McCarouselNext />
</McCarousel>API Reference
McCarousel Props
McCarousel accepts all props from a standard div, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" | The direction of the carousel |
opts | EmblaOptionsType | undefined | Options passed to the Embla carousel instance |
plugins | EmblaPluginsType | undefined | Plugins passed to the Embla carousel instance |
setApi | (api: McCarouselApi) => void | undefined | Callback to access the Embla carousel API |
className | string | undefined | Additional CSS classes |
McCarouselContent
A container for the carousel slides. Wraps the scrollable area.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
McCarouselItem
A single slide within the carousel. Each item fills the full viewport width by default.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional CSS classes |
McCarouselPrevious / McCarouselNext
Navigation buttons to scroll to the previous or next slide.
Accepts all props from Button, with the following defaults:
| Prop | Type | Default | Description |
|---|---|---|---|
variant | string | "outline" | Button variant |
size | string | "icon-sm" | Button size |
className | string | undefined | Additional CSS classes |