Search for a command to run...
A carousel with motion and swipe built using Embla.
The carousel component is built using the Embla Carousel library.
npx shadcn@latest add @zaidan/carouselpnpx shadcn add @zaidan/carouselyarn dlx shadcn@latest add @zaidan/carouselbunx shadcn@latest add @zaidan/carouselInstall the following dependencies:
npm i embla-carousel embla-carousel-solidpnpm add embla-carousel embla-carousel-solidyarn add embla-carousel embla-carousel-solidbun add embla-carousel embla-carousel-solidCopy and paste the following code into your project.
1import type { EmblaCarouselType, EmblaOptionsType, EmblaPluginType } from "embla-carousel";2import createEmblaCarousel from "embla-carousel-solid";3import { ChevronLeft, ChevronRight } from "lucide-solid";4import type { Accessor, ComponentProps } from "solid-js";5import {6 createContext,7 createEffect,8 createSignal,9 mergeProps,10 onCleanup,11 splitProps,12 useContext,13} from "solid-js";14
15import { cn } from "~/lib/utils";16import { Button, type ButtonProps } from "~/components/ui/button";17
18type CarouselApi = EmblaCarouselType | undefined;19type CarouselOptions = EmblaOptionsType;20type CarouselPlugin = EmblaPluginType;21
22type CarouselProps = {23 opts?: CarouselOptions;24 plugins?: CarouselPlugin[];25 orientation?: "horizontal" | "vertical";26 setApi?: (api: CarouselApi) => void;27};28
29type CarouselContextProps = {30 carouselRef: ReturnType<typeof createEmblaCarousel>[0];31 api: ReturnType<typeof createEmblaCarousel>[1];32 scrollPrev: () => void;33 scrollNext: () => void;34 canScrollPrev: Accessor<boolean>;35 canScrollNext: Accessor<boolean>;36} & CarouselProps;37
38const CarouselContext = createContext<CarouselContextProps | null>(null);39
40function useCarousel() {41 const context = useContext(CarouselContext);42
43 if (!context) {44 throw new Error("useCarousel must be used within a <Carousel />");45 }46
47 return context;48}49
50type CarouselRootProps = ComponentProps<"div"> & CarouselProps;51
52const Carousel = (props: CarouselRootProps) => {53 const mergedProps = mergeProps({ orientation: "horizontal" as const }, props);54 const [local, others] = splitProps(mergedProps, [55 "class",56 "children",57 "opts",58 "plugins",59 "orientation",60 "setApi",61 ]);62
63 const [carouselRef, api] = createEmblaCarousel(64 () => ({65 ...local.opts,66 axis: local.orientation === "horizontal" ? "x" : "y",67 }),68 () => local.plugins ?? [],69 );70
71 const [canScrollPrev, setCanScrollPrev] = createSignal(false);72 const [canScrollNext, setCanScrollNext] = createSignal(false);73
74 const onSelect = (emblaApi: EmblaCarouselType) => {75 setCanScrollPrev(emblaApi.canScrollPrev());76 setCanScrollNext(emblaApi.canScrollNext());77 };78
79 const scrollPrev = () => {80 api()?.scrollPrev();81 };82
83 const scrollNext = () => {84 api()?.scrollNext();85 };86
87 const handleKeyDown = (event: KeyboardEvent) => {88 if (event.key === "ArrowLeft") {89 event.preventDefault();90 scrollPrev();91 } else if (event.key === "ArrowRight") {92 event.preventDefault();93 scrollNext();94 }95 };96
97 createEffect(() => {98 const emblaApi = api();99 if (!emblaApi || !local.setApi) return;100 local.setApi(emblaApi);101 });102
103 createEffect(() => {104 const emblaApi = api();105 if (!emblaApi) return;106
107 onSelect(emblaApi);108 emblaApi.on("reInit", onSelect);109 emblaApi.on("select", onSelect);110
111 onCleanup(() => {112 emblaApi.off("select", onSelect);113 });114 });115
116 return (117 <CarouselContext.Provider118 value={{119 carouselRef,120 api,121 opts: local.opts,122 orientation: local.orientation || (local.opts?.axis === "y" ? "vertical" : "horizontal"),123 scrollPrev,124 scrollNext,125 canScrollPrev,126 canScrollNext,127 }}128 >129 {/** biome-ignore lint/a11y/useSemanticElements: <exception for carousel> */}130 <div131 aria-roledescription="carousel"132 class={cn("relative", local.class)}133 data-slot="carousel"134 onKeyDown={handleKeyDown}135 role="region"136 {...others}137 >138 {local.children}139 </div>140 </CarouselContext.Provider>141 );142};143
144type CarouselContentProps = ComponentProps<"div">;145
146const CarouselContent = (props: CarouselContentProps) => {147 const [local, others] = splitProps(props, ["class"]);148 const { carouselRef, orientation } = useCarousel();149
150 return (151 <div class="overflow-hidden" data-slot="carousel-content" ref={carouselRef}>152 <div153 class={cn("flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", local.class)}154 {...others}155 />156 </div>157 );158};159
160type CarouselItemProps = ComponentProps<"div">;161
162const CarouselItem = (props: CarouselItemProps) => {163 const [local, others] = splitProps(props, ["class"]);164 const { orientation } = useCarousel();165
166 return (167 // biome-ignore lint/a11y/useSemanticElements: <exception for carousel item>168 <div169 aria-roledescription="slide"170 class={cn(171 "min-w-0 shrink-0 grow-0 basis-full",172 orientation === "horizontal" ? "pl-4" : "pt-4",173 local.class,174 )}175 data-slot="carousel-item"176 role="group"177 {...others}178 />179 );180};181
182type CarouselPreviousProps = ButtonProps;183
184const CarouselPrevious = (props: CarouselPreviousProps) => {185 const mergedProps = mergeProps(186 { variant: "outline", size: "icon-sm" } as CarouselPreviousProps,187 props,188 );189 const [local, others] = splitProps(mergedProps, ["class", "variant", "size"]);190 const { orientation, scrollPrev, canScrollPrev } = useCarousel();191
192 return (193 <Button194 class={cn(195 "absolute z-carousel-previous touch-manipulation",196 orientation === "horizontal"197 ? "top-1/2 -left-12 -translate-y-1/2"198 : "-top-12 left-1/2 -translate-x-1/2 rotate-90",199 local.class,200 )}201 data-slot="carousel-previous"202 disabled={!canScrollPrev()}203 onClick={scrollPrev}204 size={local.size}205 variant={local.variant}206 {...others}207 >208 <ChevronLeft />209 <span class="sr-only">Previous slide</span>210 </Button>211 );212};213
214type CarouselNextProps = ButtonProps;215
216const CarouselNext = (props: CarouselNextProps) => {217 const mergedProps = mergeProps(218 { variant: "outline", size: "icon-sm" } as CarouselNextProps,219 props,220 );221 const [local, others] = splitProps(mergedProps, ["class", "variant", "size"]);222 const { orientation, scrollNext, canScrollNext } = useCarousel();223
224 return (225 <Button226 class={cn(227 "absolute z-carousel-next touch-manipulation",228 orientation === "horizontal"229 ? "top-1/2 -right-12 -translate-y-1/2"230 : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",231 local.class,232 )}233 data-slot="carousel-next"234 disabled={!canScrollNext()}235 onClick={scrollNext}236 size={local.size}237 variant={local.variant}238 {...others}239 >240 <ChevronRight />241 <span class="sr-only">Next slide</span>242 </Button>243 );244};245
246export {247 type CarouselApi,248 Carousel,249 CarouselContent,250 CarouselItem,251 CarouselPrevious,252 CarouselNext,253 useCarousel,254};You can pass options to the carousel using the opts prop. See the Embla Carousel docs for more information.
1<Carousel2 opts={{3 align: "start",4 loop: true,5 }}6>7 <CarouselContent>8 <CarouselItem>...</CarouselItem>9 <CarouselItem>...</CarouselItem>10 <CarouselItem>...</CarouselItem>11 </CarouselContent>12</Carousel>Use a signal and the setApi prop to get an instance of the carousel API.
1import { createSignal, createEffect } from "solid-js";2import { type CarouselApi } from "~/components/ui/carousel";3
4export function Example() {5 const [api, setApi] = createSignal<CarouselApi>();6 const [current, setCurrent] = createSignal(0);7 const [count, setCount] = createSignal(0);8
9 createEffect(() => {10 const carouselApi = api();11 if (!carouselApi) return;12
13 setCount(carouselApi.scrollSnapList().length);14 setCurrent(carouselApi.selectedScrollSnap() + 1);15
16 carouselApi.on("select", () => {17 setCurrent(carouselApi.selectedScrollSnap() + 1);18 });19 });20
21 return (22 <Carousel setApi={setApi}>23 <CarouselContent>24 <CarouselItem>...</CarouselItem>25 <CarouselItem>...</CarouselItem>26 <CarouselItem>...</CarouselItem>27 </CarouselContent>28 </Carousel>29 );30}You can use the plugins prop to add plugins to the carousel.
1import Autoplay from "embla-carousel-autoplay";2
3export function Example() {4 return (5 <Carousel6 plugins={[7 Autoplay({8 delay: 2000,9 }),10 ]}11 >12 // ...13 </Carousel>14 );15}See the Embla Carousel docs for more information on using plugins.
Here are the source code of all the examples from the preview page:
import { For } from "solid-js";import { Card, CardContent } from "~/components/ui/card";import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious,} from "~/components/ui/carousel";function CarouselBasic() { return ( <Example title="Basic"> <Carousel class="mx-auto max-w-xs sm:max-w-sm"> <CarouselContent> <For each={Array.from({ length: 5 })}> {(_, index) => ( <CarouselItem> <div class="p-1"> <Card> <CardContent class="flex aspect-square items-center justify-center p-6"> <span class="font-semibold text-4xl">{index() + 1}</span> </CardContent> </Card> </div> </CarouselItem> )} </For> </CarouselContent> <CarouselPrevious class="hidden sm:inline-flex" /> <CarouselNext class="hidden sm:inline-flex" /> </Carousel> </Example> );}import { For } from "solid-js";import { Card, CardContent } from "~/components/ui/card";import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious,} from "~/components/ui/carousel";function CarouselMultiple() { return ( <Example title="Multiple"> <Carousel class="mx-auto max-w-xs sm:max-w-sm" opts={{ align: "start", }} > <CarouselContent> <For each={Array.from({ length: 5 })}> {(_, index) => ( <CarouselItem class="sm:basis-1/2 lg:basis-1/3"> <div class="p-1"> <Card> <CardContent class="flex aspect-square items-center justify-center p-6"> <span class="font-semibold text-3xl">{index() + 1}</span> </CardContent> </Card> </div> </CarouselItem> )} </For> </CarouselContent> <CarouselPrevious class="hidden sm:inline-flex" /> <CarouselNext class="hidden sm:inline-flex" /> </Carousel> </Example> );}import { For } from "solid-js";import { Card, CardContent } from "~/components/ui/card";import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious,} from "~/components/ui/carousel";function CarouselWithGap() { return ( <Example title="With Gap"> <Carousel class="mx-auto max-w-xs sm:max-w-sm"> <CarouselContent class="-ml-1"> <For each={Array.from({ length: 5 })}> {(_, index) => ( <CarouselItem class="pl-1 md:basis-1/2"> <div class="p-1"> <Card> <CardContent class="flex aspect-square items-center justify-center p-6"> <span class="font-semibold text-2xl">{index() + 1}</span> </CardContent> </Card> </div> </CarouselItem> )} </For> </CarouselContent> <CarouselPrevious class="hidden sm:inline-flex" /> <CarouselNext class="hidden sm:inline-flex" /> </Carousel> </Example> );}