Carousel
Slide container on embla-carousel-svelte. Each snap settles with embla's own spring feel; arrow keys advance one slide at a time and the edge buttons disable as the rail hits either end.
Usage
<script lang="ts">
import * as Carousel from '$lib/components/ui/carousel';
</script>
<Carousel.Root>
<Carousel.Content>
{#each [1, 2, 3, 4, 5] as n (n)}
<Carousel.Item>
<div class="flex aspect-square items-center justify-center">
{n}
</div>
</Carousel.Item>
{/each}
</Carousel.Content>
<Carousel.Previous />
<Carousel.Next />
</Carousel.Root>Multiple per view
Set each item's basis-* to show more than one slide at once.
The embla rail still snaps one slide per interaction.
<script lang="ts">
import {
type CarouselAPI,
type CarouselProps,
type EmblaContext,
setEmblaContext
} from './context.js';
import { cn, type WithElementRef } from '$lib/utils.js';
let {
ref = $bindable(null),
opts = {},
plugins = [],
setApi = () => {},
orientation = 'horizontal',
ariaLabel = 'Carousel',
class: className,
children,
...restProps
}: WithElementRef<CarouselProps> & { ariaLabel?: string } = $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', className)}
role="region"
aria-roledescription="carousel"
aria-label={ariaLabel}
{...restProps}
>
{@render children?.()}
</div>
Vertical
Pass orientation="vertical" to pivot the rail; the nav buttons
reposition and rotate automatically.
<Carousel.Root orientation="vertical">
<Carousel.Content class="h-60">
{#each [1, 2, 3] as n (n)}
<Carousel.Item class="basis-1/2">…</Carousel.Item>
{/each}
</Carousel.Content>
<Carousel.Previous />
<Carousel.Next />
</Carousel.Root>With dots
Carousel.Dots renders one button per snap, exposing role="tablist" with a roving tabindex. The visual stays at
8px while a 44pt pseudo-element widens the hit area.
<Carousel.Root>
<Carousel.Content>
{#each [1, 2, 3, 4, 5] as n (n)}
<Carousel.Item>…</Carousel.Item>
{/each}
</Carousel.Content>
<Carousel.Previous />
<Carousel.Next />
<Carousel.Dots class="mt-4" />
</Carousel.Root>Carousel.Root props
Inherits every native HTML div attribute via spread. Wraps embla-carousel-svelte — `opts` and `plugins` pass straight through to embla.
| Prop | Type | Default | Description |
|---|---|---|---|
'horizontal' | 'vertical' | 'horizontal' | Rail axis. Flips arrow placement and swaps embla`s `axis` option. | |
CarouselOptions | {} | Embla options — `align`, `loop`, `dragFree`, `skipSnaps`, etc. See the embla docs. | |
CarouselPlugins | [] | Embla plugins (Autoplay, ClassNames, WheelGestures…). Pass-through to embla. | |
(api: CarouselAPI | undefined) => void | — | Receives the embla instance on init. Use for custom controls, dot indicators, or progress readouts. | |
string | — | Merged onto the root container via tailwind-merge. |
Carousel.Content & nav props
Content forwards HTMLDivAttributes; Previous and Next forward Button props (variant, size, class, …) and auto-disable when the rail hits an edge.
| Prop | Type | Default | Description |
|---|---|---|---|
string | — | Merged onto the embla viewport container. Useful for constraining height in vertical mode. | |
string | — | Merged onto each slide. Override `basis-*` to display more than one slide per view. | |
ButtonVariant | 'outline' | Button variant applied to the prev-slide affordance. | |
ButtonSize | 'icon-sm' | Button size applied to the prev-slide affordance. | |
ButtonVariant | 'outline' | Button variant applied to the next-slide affordance. | |
ButtonSize | 'icon-sm' | Button size applied to the next-slide affordance. |