Zaidan

Command Palette

Search for a command to run...

GitHub43

Carousel

A carousel with motion and swipe built using Embla.

About

The carousel component is built using the Embla Carousel library.

Installation

CLI

Manual

Install the following dependencies:

Copy and paste the following code into your project.

import type { EmblaCarouselType, EmblaOptionsType, EmblaPluginType } from "embla-carousel";
import createEmblaCarousel from "embla-carousel-solid";
import { ChevronLeft, ChevronRight } from "lucide-solid";
import type { Accessor, ComponentProps } from "solid-js";
import {
createContext,
createEffect,
createSignal,
mergeProps,
onCleanup,
splitProps,
useContext,
} from "solid-js";
import { cn } from "~/lib/utils";
import { Button, type ButtonProps } from "~/components/ui/button";
type CarouselApi = EmblaCarouselType | undefined;
type CarouselOptions = EmblaOptionsType;
type CarouselPlugin = EmblaPluginType;
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin[];
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof createEmblaCarousel>[0];
api: ReturnType<typeof createEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: Accessor<boolean>;
canScrollNext: Accessor<boolean>;
} & CarouselProps;
const CarouselContext = createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}
type CarouselRootProps = ComponentProps<"div"> & CarouselProps;
const Carousel = (props: CarouselRootProps) => {
const mergedProps = mergeProps({ orientation: "horizontal" as const }, props);
const [local, others] = splitProps(mergedProps, [
"class",
"children",
"opts",
"plugins",
"orientation",
"setApi",
]);
const [carouselRef, api] = createEmblaCarousel(
() => ({
...local.opts,
axis: local.orientation === "horizontal" ? "x" : "y",
}),
() => local.plugins ?? [],
);
const [canScrollPrev, setCanScrollPrev] = createSignal(false);
const [canScrollNext, setCanScrollNext] = createSignal(false);
const onSelect = (emblaApi: EmblaCarouselType) => {
setCanScrollPrev(emblaApi.canScrollPrev());
setCanScrollNext(emblaApi.canScrollNext());
};
const scrollPrev = () => {
api()?.scrollPrev();
};
const scrollNext = () => {
api()?.scrollNext();
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
};
createEffect(() => {
const emblaApi = api();
if (!emblaApi || !local.setApi) return;
local.setApi(emblaApi);
});
createEffect(() => {
const emblaApi = api();
if (!emblaApi) return;
onSelect(emblaApi);
emblaApi.on("reInit", onSelect);
emblaApi.on("select", onSelect);
onCleanup(() => {
emblaApi.off("select", onSelect);
});
});
return (
<CarouselContext.Provider
value={{
carouselRef,
api,
opts: local.opts,
orientation: local.orientation || (local.opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
{/** biome-ignore lint/a11y/useSemanticElements: <exception for carousel> */}
<div
aria-roledescription="carousel"
class={cn("relative", local.class)}
data-slot="carousel"
onKeyDown={handleKeyDown}
role="region"
{...others}
>
{local.children}
</div>
</CarouselContext.Provider>
);
};
type CarouselContentProps = ComponentProps<"div">;
const CarouselContent = (props: CarouselContentProps) => {
const [local, others] = splitProps(props, ["class"]);
const { carouselRef, orientation } = useCarousel();
return (
<div class="overflow-hidden" data-slot="carousel-content" ref={carouselRef}>
<div
class={cn("flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", local.class)}
{...others}
/>
</div>
);
};
type CarouselItemProps = ComponentProps<"div">;
const CarouselItem = (props: CarouselItemProps) => {
const [local, others] = splitProps(props, ["class"]);
const { orientation } = useCarousel();
return (
// biome-ignore lint/a11y/useSemanticElements: <exception for carousel item>
<div
aria-roledescription="slide"
class={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
local.class,
)}
data-slot="carousel-item"
role="group"
{...others}
/>
);
};
type CarouselPreviousProps = ButtonProps;
const CarouselPrevious = (props: CarouselPreviousProps) => {
const mergedProps = mergeProps(
{ variant: "outline", size: "icon-sm" } as CarouselPreviousProps,
props,
);
const [local, others] = splitProps(mergedProps, ["class", "variant", "size"]);
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
class={cn(
"absolute z-carousel-previous touch-manipulation",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
local.class,
)}
data-slot="carousel-previous"
disabled={!canScrollPrev()}
onClick={scrollPrev}
size={local.size}
variant={local.variant}
{...others}
>
<ChevronLeft />
<span class="sr-only">Previous slide</span>
</Button>
);
};
type CarouselNextProps = ButtonProps;
const CarouselNext = (props: CarouselNextProps) => {
const mergedProps = mergeProps(
{ variant: "outline", size: "icon-sm" } as CarouselNextProps,
props,
);
const [local, others] = splitProps(mergedProps, ["class", "variant", "size"]);
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
class={cn(
"absolute z-carousel-next touch-manipulation",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
local.class,
)}
data-slot="carousel-next"
disabled={!canScrollNext()}
onClick={scrollNext}
size={local.size}
variant={local.variant}
{...others}
>
<ChevronRight />
<span class="sr-only">Next slide</span>
</Button>
);
};
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
useCarousel,
};

Options

You can pass options to the carousel using the opts prop. See the Embla Carousel docs for more information.

<Carousel
opts={{
align: "start",
loop: true,
}}
>
<CarouselContent>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
</CarouselContent>
</Carousel>

API

Use a signal and the setApi prop to get an instance of the carousel API.

import { createSignal, createEffect } from "solid-js";
import { type CarouselApi } from "~/components/ui/carousel";
export function Example() {
const [api, setApi] = createSignal<CarouselApi>();
const [current, setCurrent] = createSignal(0);
const [count, setCount] = createSignal(0);
createEffect(() => {
const carouselApi = api();
if (!carouselApi) return;
setCount(carouselApi.scrollSnapList().length);
setCurrent(carouselApi.selectedScrollSnap() + 1);
carouselApi.on("select", () => {
setCurrent(carouselApi.selectedScrollSnap() + 1);
});
});
return (
<Carousel setApi={setApi}>
<CarouselContent>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
</CarouselContent>
</Carousel>
);
}

Plugins

You can use the plugins prop to add plugins to the carousel.

import Autoplay from "embla-carousel-autoplay";
export function Example() {
return (
<Carousel
plugins={[
Autoplay({
delay: 2000,
}),
]}
>
// ...
</Carousel>
);
}

See the Embla Carousel docs for more information on using plugins.

Examples

Here are the source code of all the examples from the preview page:

Basic

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>
);
}

Multiple

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>
);
}

With Gap

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>
);
}