<script lang="ts">
import * as Card from "$lib/components/ui/card/index.js";
import * as Carousel from "$lib/components/ui/carousel/index.js";
</script>
<Carousel.Root class="w-full max-w-xs">
<Carousel.Content>
{#each Array(5), i}
<Carousel.Item>
<div class="p-1">
<Card.Root>
<Card.Content
class="flex aspect-square items-center justify-center p-6"
>
<span class="text-4xl font-semibold">{i + 1}</span>
</Card.Content>
</Card.Root>
</div>
</Carousel.Item>
{/each}
</Carousel.Content>
<Carousel.Previous />
<Carousel.Next />
</Carousel.Root> About
The carousel component is built using the Embla Carousel library.
Installation
pnpm dlx shadcn-svelte@latest add carousel Install embla-carousel-svelte:
pnpm add embla-carousel-svelte -D Copy and paste the following code into your project.
<script lang="ts">
import emblaCarouselSvelte from 'embla-carousel-svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { getEmblaContext } from './context.js';
import { cn, type WithElementRef } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
const emblaCtx = getEmblaContext('<Carousel.Content/>');
</script>
<div
data-slot="carousel-content"
class="overflow-hidden bg-background"
use:emblaCarouselSvelte={{
options: {
container: '[data-embla-container]',
slides: '[data-embla-slide]',
...emblaCtx.options,
axis: emblaCtx.orientation === 'horizontal' ? 'x' : 'y'
},
plugins: emblaCtx.plugins
}}
onemblaInit={emblaCtx.onInit}
>
<div
bind:this={ref}
class={cn(
'flex',
emblaCtx.orientation === 'horizontal' ? '-ms-px' : '-mt-px flex-col',
className
)}
data-embla-container=""
{...restProps}
>
{@render children?.()}
</div>
</div>
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { getEmblaContext } from './context.js';
import { cn, type WithElementRef } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
const emblaCtx = getEmblaContext('<Carousel.Item/>');
</script>
<div
bind:this={ref}
data-slot="carousel-item"
role="group"
aria-roledescription="slide"
class={cn(
'min-w-0 shrink-0 grow-0 basis-full border-zinc-900',
emblaCtx.orientation === 'horizontal' ? 'border-l ps-px' : 'border-t pt-px',
className
)}
data-embla-slide=""
{...restProps}
>
{@render children?.()}
</div>
<script lang="ts">
import type { WithoutChildren } from 'bits-ui';
import { getEmblaContext } from './context.js';
import { cn } from '$UTILS$.js';
import { Button, type Props } from '$UI$/button/index.js';
import CaretRightIcon from 'phosphor-svelte/lib/CaretRight';
let {
ref = $bindable(null),
class: className,
variant = 'outline',
size = 'icon-sm',
...restProps
}: WithoutChildren<Props> = $props();
const emblaCtx = getEmblaContext('<Carousel.Next/>');
</script>
<Button
data-slot="carousel-next"
{variant}
{size}
aria-disabled={!emblaCtx.canScrollNext}
disabled={!emblaCtx.canScrollNext}
class={cn(
'cn-carousel-next absolute rounded-full border-zinc-800 bg-background text-zinc-300 touch-manipulation hover:border-[#d0e891] hover:bg-[#b9d765] hover:text-[#101207]',
emblaCtx.orientation === 'horizontal'
? '-end-12 top-1/2 -translate-y-1/2'
: 'start-1/2 -bottom-12 -translate-x-1/2 rotate-90',
className
)}
onclick={emblaCtx.scrollNext}
onkeydown={emblaCtx.handleKeyDown}
bind:ref
{...restProps}
>
<CaretRightIcon />
<span class="sr-only">Next slide</span>
</Button>
<script lang="ts">
import type { WithoutChildren } from 'bits-ui';
import { getEmblaContext } from './context.js';
import { cn } from '$UTILS$.js';
import { Button, type Props } from '$UI$/button/index.js';
import CaretLeftIcon from 'phosphor-svelte/lib/CaretLeft';
let {
ref = $bindable(null),
class: className,
variant = 'outline',
size = 'icon-sm',
...restProps
}: WithoutChildren<Props> = $props();
const emblaCtx = getEmblaContext('<Carousel.Previous/>');
</script>
<Button
data-slot="carousel-previous"
{variant}
{size}
aria-disabled={!emblaCtx.canScrollPrev}
disabled={!emblaCtx.canScrollPrev}
class={cn(
'cn-carousel-previous absolute rounded-full border-zinc-800 bg-background text-zinc-300 touch-manipulation hover:border-[#d0e891] hover:bg-[#b9d765] hover:text-[#101207]',
emblaCtx.orientation === 'horizontal'
? '-start-12 top-1/2 -translate-y-1/2'
: 'start-1/2 -top-12 -translate-x-1/2 rotate-90',
className
)}
onclick={emblaCtx.scrollPrev}
onkeydown={emblaCtx.handleKeyDown}
{...restProps}
bind:ref
>
<CaretLeftIcon />
<span class="sr-only">Previous slide</span>
</Button>
<script lang="ts">
import {
type CarouselAPI,
type CarouselProps,
type EmblaContext,
setEmblaContext
} from './context.js';
import { cn, type WithElementRef } from '$UTILS$.js';
let {
ref = $bindable(null),
opts = {},
plugins = [],
setApi = () => {},
orientation = 'horizontal',
class: className,
children,
...restProps
}: WithElementRef<CarouselProps> = $props();
// svelte-ignore state_referenced_locally
let carouselState = $state<EmblaContext>({
api: undefined,
scrollPrev,
scrollNext,
orientation,
canScrollNext: false,
canScrollPrev: false,
handleKeyDown,
options: opts,
plugins,
onInit,
scrollSnaps: [],
selectedIndex: 0,
scrollTo
});
setEmblaContext(carouselState);
function scrollPrev() {
carouselState.api?.scrollPrev();
}
function scrollNext() {
carouselState.api?.scrollNext();
}
function scrollTo(index: number, jump?: boolean) {
carouselState.api?.scrollTo(index, jump);
}
function onSelect() {
if (!carouselState.api) return;
carouselState.selectedIndex = carouselState.api.selectedScrollSnap();
carouselState.canScrollNext = carouselState.api.canScrollNext();
carouselState.canScrollPrev = carouselState.api.canScrollPrev();
}
function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'ArrowLeft') {
e.preventDefault();
scrollPrev();
} else if (e.key === 'ArrowRight') {
e.preventDefault();
scrollNext();
}
}
function onInit(event: CustomEvent<CarouselAPI>) {
carouselState.api = event.detail;
setApi(carouselState.api);
carouselState.scrollSnaps = carouselState.api.scrollSnapList();
carouselState.api.on('select', onSelect);
onSelect();
}
$effect(() => {
return () => {
carouselState.api?.off('select', onSelect);
};
});
</script>
<div
bind:this={ref}
data-slot="carousel"
class={cn('relative border border-zinc-800 bg-background text-zinc-100', className)}
role="region"
aria-roledescription="carousel"
{...restProps}
>
{@render children?.()}
</div>
import type { WithElementRef } from '$UTILS$.js';
import type {
EmblaCarouselSvelteType,
default as emblaCarouselSvelte
} from 'embla-carousel-svelte';
import { getContext, hasContext, setContext } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
export type CarouselAPI =
NonNullable<NonNullable<EmblaCarouselSvelteType['$$_attributes']>['on:emblaInit']> extends (
evt: CustomEvent<infer CarouselAPI>
) => void
? CarouselAPI
: never;
type EmblaCarouselConfig = NonNullable<Parameters<typeof emblaCarouselSvelte>[1]>;
export type CarouselOptions = EmblaCarouselConfig['options'];
export type CarouselPlugins = EmblaCarouselConfig['plugins'];
////
export type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugins;
setApi?: (api: CarouselAPI | undefined) => void;
orientation?: 'horizontal' | 'vertical';
} & WithElementRef<HTMLAttributes<HTMLDivElement>>;
const EMBLA_CAROUSEL_CONTEXT = Symbol('EMBLA_CAROUSEL_CONTEXT');
export type EmblaContext = {
api: CarouselAPI | undefined;
orientation: 'horizontal' | 'vertical';
scrollNext: () => void;
scrollPrev: () => void;
canScrollNext: boolean;
canScrollPrev: boolean;
handleKeyDown: (e: KeyboardEvent) => void;
options: CarouselOptions;
plugins: CarouselPlugins;
onInit: (e: CustomEvent<CarouselAPI>) => void;
scrollTo: (index: number, jump?: boolean) => void;
scrollSnaps: number[];
selectedIndex: number;
};
export function setEmblaContext(config: EmblaContext): EmblaContext {
setContext(EMBLA_CAROUSEL_CONTEXT, config);
return config;
}
export function getEmblaContext(name = 'This component') {
if (!hasContext(EMBLA_CAROUSEL_CONTEXT)) {
throw new Error(`${name} must be used within a <Carousel.Root> component`);
}
return getContext<ReturnType<typeof setEmblaContext>>(EMBLA_CAROUSEL_CONTEXT);
}
import Root from './carousel.svelte';
import Content from './carousel-content.svelte';
import Item from './carousel-item.svelte';
import Previous from './carousel-previous.svelte';
import Next from './carousel-next.svelte';
export {
Root,
Content,
Item,
Previous,
Next,
//
Root as Carousel,
Content as CarouselContent,
Item as CarouselItem,
Previous as CarouselPrevious,
Next as CarouselNext
};
Usage
<script lang="ts">
import * as Carousel from '$lib/components/ui/carousel/index.js';
</script> <Carousel.Root>
<Carousel.Content>
<Carousel.Item>...</Carousel.Item>
<Carousel.Item>...</Carousel.Item>
<Carousel.Item>...</Carousel.Item>
</Carousel.Content>
<Carousel.Previous />
<Carousel.Next />
</Carousel.Root> Examples
Sizes
To set the size of the items, you can use the basis utility class on the <Carousel.Item />.
<script lang="ts">
import * as Card from "$lib/components/ui/card/index.js";
import * as Carousel from "$lib/components/ui/carousel/index.js";
const SLIDES = [1, 2, 3, 4, 5];
</script>
<Carousel.Root
opts={{
align: "start"
}}
class="w-full max-w-sm"
>
<Carousel.Content>
{#each SLIDES as slide (slide)}
<Carousel.Item class="md:basis-1/2 lg:basis-1/3">
<div class="p-1">
<Card.Root>
<Card.Content
class="flex aspect-square items-center justify-center p-6"
>
<span class="text-3xl font-semibold">{slide}</span>
</Card.Content>
</Card.Root>
</div>
</Carousel.Item>
{/each}
</Carousel.Content>
<Carousel.Previous />
<Carousel.Next />
</Carousel.Root> <!-- 33% of the carousel width. -->
<Carousel.Root>
<Carousel.Content>
<Carousel.Item class="basis-1/3">...</Carousel.Item>
<Carousel.Item class="basis-1/3">...</Carousel.Item>
<Carousel.Item class="basis-1/3">...</Carousel.Item>
</Carousel.Content>
</Carousel.Root> <!-- 50% on small screens and 33% on larger screens. -->
<Carousel.Root>
<Carousel.Content>
<Carousel.Item class="md:basis-1/2 lg:basis-1/3">...</Carousel.Item>
<Carousel.Item class="md:basis-1/2 lg:basis-1/3">...</Carousel.Item>
<Carousel.Item class="md:basis-1/2 lg:basis-1/3">...</Carousel.Item>
</Carousel.Content>
</Carousel.Root> Spacing
To set the spacing between the items, we use a ps-[VALUE] utility on the <Carousel.Item /> and a negative -ms-[VALUE] on the <Carousel.Content />.
<script lang="ts">
import * as Card from "$lib/components/ui/card/index.js";
import * as Carousel from "$lib/components/ui/carousel/index.js";
const SLIDES = [1, 2, 3, 4, 5];
</script>
<Carousel.Root class="w-full max-w-sm">
<Carousel.Content class="-ms-1">
{#each SLIDES as slide (slide)}
<Carousel.Item class="ps-1 md:basis-1/2 lg:basis-1/3">
<div class="p-1">
<Card.Root>
<Card.Content
class="flex aspect-square items-center justify-center p-6"
>
<span class="text-2xl font-semibold">{slide}</span>
</Card.Content>
</Card.Root>
</div>
</Carousel.Item>
{/each}
</Carousel.Content>
<Carousel.Previous />
<Carousel.Next />
</Carousel.Root> <Carousel.Root>
<Carousel.Content class="-ms-4">
<Carousel.Item class="ps-4">...</Carousel.Item>
<Carousel.Item class="ps-4">...</Carousel.Item>
<Carousel.Item class="ps-4">...</Carousel.Item>
</Carousel.Content>
</Carousel.Root> <Carousel.Root>
<Carousel.Content class="-ms-2 md:-ms-4">
<Carousel.Item class="ps-2 md:ps-4">...</Carousel.Item>
<Carousel.Item class="ps-2 md:ps-4">...</Carousel.Item>
<Carousel.Item class="ps-2 md:ps-4">...</Carousel.Item>
</Carousel.Content>
</Carousel.Root> Orientation
Use the orientation prop to set the orientation of the carousel.
<script lang="ts">
import * as Card from "$lib/components/ui/card/index.js";
import * as Carousel from "$lib/components/ui/carousel/index.js";
const SLIDES = [1, 2, 3, 4, 5];
</script>
<Carousel.Root
opts={{
align: "start"
}}
orientation="vertical"
class="w-full max-w-xs"
>
<Carousel.Content class="-mt-1 h-[200px]">
{#each SLIDES as slide (slide)}
<Carousel.Item class="pt-1 md:basis-1/2">
<div class="p-1">
<Card.Root>
<Card.Content class="flex items-center justify-center p-6">
<span class="text-3xl font-semibold">{slide}</span>
</Card.Content>
</Card.Root>
</div>
</Carousel.Item>
{/each}
</Carousel.Content>
<Carousel.Previous />
<Carousel.Next />
</Carousel.Root> <Carousel.Root orientation="vertical | horizontal">
<Carousel.Content>
<Carousel.Item>...</Carousel.Item>
<Carousel.Item>...</Carousel.Item>
<Carousel.Item>...</Carousel.Item>
</Carousel.Content>
</Carousel.Root> Options
You can pass options to the carousel using the opts prop. See the Embla Carousel docs for more information.
<Carousel.Root
opts={{
align: 'start',
loop: true
}}
>
<Carousel.Content>
<Carousel.Item>...</Carousel.Item>
<Carousel.Item>...</Carousel.Item>
<Carousel.Item>...</Carousel.Item>
</Carousel.Content>
</Carousel.Root> API
Use reactive state and the setApi callback to get an instance of the carousel API.
<script lang="ts">
import * as Card from "$lib/components/ui/card/index.js";
import * as Carousel from "$lib/components/ui/carousel/index.js";
import type { CarouselAPI } from "$lib/components/ui/carousel/context.js";
let api = $state<CarouselAPI>();
const count = $derived(api ? api.scrollSnapList().length : 0);
let current = $state(0);
const SLIDES = [1, 2, 3, 4, 5];
$effect(() => {
if (api) {
current = api.selectedScrollSnap() + 1;
api.on("select", () => {
current = api!.selectedScrollSnap() + 1;
});
}
});
</script>
<div>
<Carousel.Root
setApi={(emblaApi) => (api = emblaApi)}
class="w-full max-w-xs"
>
<Carousel.Content>
{#each SLIDES as slide (slide)}
<Carousel.Item>
<Card.Root>
<Card.Content
class="flex aspect-square items-center justify-center p-6"
>
<span class="text-4xl font-semibold">{slide}</span>
</Card.Content>
</Card.Root>
</Carousel.Item>
{/each}
</Carousel.Content>
<Carousel.Previous />
<Carousel.Next />
</Carousel.Root>
<div class="text-muted-foreground py-2 text-center text-sm">
Slide {current} of {count}
</div>
</div> <script lang="ts">
import { type CarouselAPI } from '$lib/components/ui/carousel/context.js';
import * as Carousel from '$lib/components/ui/carousel/index.js';
let api = $state<CarouselAPI>();
let current = $state(0);
const count = $derived(api ? api.scrollSnapList().length : 0);
$effect(() => {
if (api) {
current = api.selectedScrollSnap() + 1;
api.on('select', () => {
current = api!.selectedScrollSnap() + 1;
});
}
});
</script>
<Carousel.Root setApi={(emblaApi) => (api = emblaApi)}>
<Carousel.Content>
<Carousel.Item>...</Carousel.Item>
<Carousel.Item>...</Carousel.Item>
<Carousel.Item>...</Carousel.Item>
</Carousel.Content>
</Carousel.Root> Events
You can listen to events using the api instance from bind:api.
<script lang="ts">
import { type CarouselAPI } from '$lib/components/ui/carousel/context.js';
import * as Carousel from '$lib/components/ui/carousel/index.js';
let api = $state<CarouselAPI>();
$effect(() => {
if (api) {
api.on('select', () => {
// do something
});
}
});
</script>
<Carousel.Root setApi={(emblaApi) => (api = emblaApi)}>
<Carousel.Content>
<Carousel.Item>...</Carousel.Item>
<Carousel.Item>...</Carousel.Item>
<Carousel.Item>...</Carousel.Item>
</Carousel.Content>
</Carousel.Root> Plugins
You can use the plugins prop to add plugins to the carousel.
<script lang="ts">
import Autoplay from 'embla-carousel-autoplay';
import * as Carousel from '$lib/components/ui/carousel/index.js';
</script>
<Carousel.Root
plugins={[
Autoplay({
delay: 2000
})
]}
>
<!-- ... -->
</Carousel.Root> <script lang="ts">
import Autoplay from "embla-carousel-autoplay";
import * as Card from "$lib/components/ui/card/index.js";
import * as Carousel from "$lib/components/ui/carousel/index.js";
const plugin = Autoplay({ delay: 2000, stopOnInteraction: true });
const SLIDES = [1, 2, 3, 4, 5];
</script>
<Carousel.Root
plugins={[plugin]}
class="w-full max-w-xs"
onmouseenter={plugin.stop}
onmouseleave={plugin.reset}
>
<Carousel.Content>
{#each SLIDES as slide (slide)}
<Carousel.Item>
<div class="p-1">
<Card.Root>
<Card.Content
class="flex aspect-square items-center justify-center p-6"
>
<span class="text-4xl font-semibold">{slide}</span>
</Card.Content>
</Card.Root>
</div>
</Carousel.Item>
{/each}
</Carousel.Content>
<Carousel.Previous />
<Carousel.Next />
</Carousel.Root> See the Embla Carousel docs for more information on using plugins.