Carousel
Swipeable slides built on Embla, with arrows and position dots.
import { Carousel, CarouselContent, CarouselDots, CarouselItem, CarouselNext, CarouselPrevious,} from "@/components/ui/carousel";
const slides = [ ["Magnificent Four", "from-blue-500 to-emerald-500"], ["Pre-IPO Basket", "from-amber-500 to-red-500"], ["AI Index", "from-violet-500 to-indigo-500"], ["Energy", "from-lime-500 to-teal-500"],];
export default function Demo() { return ( <div className="mx-12 max-w-md"> <Carousel opts={{ align: "start" }}> <CarouselContent> {slides.map(([name, g]) => ( <CarouselItem key={name} className="basis-1/2"> <div className={`flex h-32 items-end rounded-xl bg-linear-to-br p-3 text-sm font-semibold text-white ${g}`} > {name} </div> </CarouselItem> ))} </CarouselContent> <CarouselPrevious /> <CarouselNext /> <CarouselDots /> </Carousel> </div> );}<script setup lang="ts">import { Carousel, CarouselContent, CarouselDots, CarouselItem, CarouselNext, CarouselPrevious,} from "@/components/ui/carousel";
const slides = [ ["Magnificent Four", "from-blue-500 to-emerald-500"], ["Pre-IPO Basket", "from-amber-500 to-red-500"], ["AI Index", "from-violet-500 to-indigo-500"], ["Energy", "from-lime-500 to-teal-500"],];</script>
<template> <div class="mx-12 max-w-md"> <Carousel :opts="{ align: 'start' }"> <CarouselContent> <CarouselItem v-for="[name, g] in slides" :key="name" class="basis-1/2"> <div :class="`flex h-32 items-end rounded-xl bg-linear-to-br p-3 text-sm font-semibold text-white ${g}`" > {{ name }} </div> </CarouselItem> </CarouselContent> <CarouselPrevious /> <CarouselNext /> <CarouselDots /> </Carousel> </div></template><script lang="ts"> import * as Carousel from "$lib/components/ui/carousel";
const slides = [ ["Magnificent Four", "from-blue-500 to-emerald-500"], ["Pre-IPO Basket", "from-amber-500 to-red-500"], ["AI Index", "from-violet-500 to-indigo-500"], ["Energy", "from-lime-500 to-teal-500"], ];</script>
<div class="mx-12 max-w-md"> <Carousel.Root opts={{ align: "start" }}> <Carousel.Content> {#each slides as [name, g] (name)} <Carousel.Item class="basis-1/2"> <div class="flex h-32 items-end rounded-xl bg-linear-to-br p-3 text-sm font-semibold text-white {g}" > {name} </div> </Carousel.Item> {/each} </Carousel.Content> <Carousel.Previous /> <Carousel.Next /> <Carousel.Dots /> </Carousel.Root></div>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/carouselpnpm dlx shadcn@latest add @edmi-ui/carouselyarn dlx shadcn@latest add @edmi-ui/carouselbunx shadcn@latest add @edmi-ui/carouselPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install embla-carousel-reactTerminal window pnpm add embla-carousel-reactTerminal window yarn add embla-carousel-reactTerminal window bun add embla-carousel-reactAdd the Edmi components this one builds on:
button.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/carouselpnpm dlx shadcn-vue@latest add @edmi-ui/carouselyarn dlx shadcn-vue@latest add @edmi-ui/carouselbunx shadcn-vue@latest add @edmi-ui/carouselPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install embla-carousel-vue @vueuse/coreTerminal window pnpm add embla-carousel-vue @vueuse/coreTerminal window yarn add embla-carousel-vue @vueuse/coreTerminal window bun add embla-carousel-vue @vueuse/coreAdd the Edmi components this one builds on:
button.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/carousel.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/carousel.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/carousel.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/carousel.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
button.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Carousel, CarouselContent, CarouselDots, CarouselNext, CarouselPrevious,} from "@/components/ui/carousel";
<div className="mx-12 max-w-md"> <Carousel opts={{ align: "start" }}> <CarouselContent> </CarouselContent> <CarouselPrevious /> <CarouselNext /> <CarouselDots /> </Carousel></div><script setup lang="ts">import { Carousel, CarouselContent, CarouselDots, CarouselItem, CarouselNext, CarouselPrevious,} from "@/components/ui/carousel";</script>
<template> <div class="mx-12 max-w-md"> <Carousel :opts="{ align: 'start' }"> <CarouselContent> <CarouselItem :key="name" class="basis-1/2"> <div :class="`flex h-32 items-end rounded-xl bg-linear-to-br p-3 text-sm font-semibold text-white ${g}`" > {{ name }} </div> </CarouselItem> </CarouselContent> <CarouselPrevious /> <CarouselNext /> <CarouselDots /> </Carousel> </div></template><script lang="ts"> import * as Carousel from "$lib/components/ui/carousel";</script>
<div class="mx-12 max-w-md"> <Carousel.Root opts={{ align: "start" }}> <Carousel.Content> {#each slides as [name, g] (name)} <Carousel.Item class="basis-1/2"> <div class="flex h-32 items-end rounded-xl bg-linear-to-br p-3 text-sm font-semibold text-white {g}" > {name} </div> </Carousel.Item> {/each} </Carousel.Content> <Carousel.Previous /> <Carousel.Next /> <Carousel.Dots /> </Carousel.Root></div>Component source
Section titled “Component source”import { cn } from "cn";import useEmblaCarousel, { type UseEmblaCarouselType,} from "embla-carousel-react";import * as React from "react";import { CaretLeftIcon, CaretRightIcon } from "@phosphor-icons/react";
import { Button } from "@/registry/edmi/ui/button";
type CarouselApi = 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: CarouselApi) => void;};
type CarouselContextProps = { carouselRef: ReturnType<typeof useEmblaCarousel>[0]; api: ReturnType<typeof useEmblaCarousel>[1]; scrollPrev: () => void; scrollNext: () => void; canScrollPrev: boolean; canScrollNext: boolean;} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() { const context = React.useContext(CarouselContext);
if (!context) { throw new Error("useCarousel must be used within a <Carousel />"); }
return context;}
function Carousel({ 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: CarouselApi) => { 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 ( <CarouselContext.Provider value={{ carouselRef, api: api, opts, orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"), scrollPrev, scrollNext, canScrollPrev, canScrollNext, }} > {/* biome-ignore lint/a11y/useSemanticElements: matches stock shadcn carousel */} <div onKeyDownCapture={handleKeyDown} className={cn("relative", className)} role="region" aria-roledescription="carousel" data-slot="carousel" {...props} > {children} </div> </CarouselContext.Provider> );}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) { const { carouselRef, orientation } = useCarousel();
return ( <div ref={carouselRef} className="overflow-hidden" data-slot="carousel-content" > <div className={cn( "flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", className, )} {...props} /> </div> );}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) { const { orientation } = useCarousel();
return ( // biome-ignore lint/a11y/useSemanticElements: matches stock shadcn carousel <div role="group" aria-roledescription="slide" data-slot="carousel-item" className={cn( "min-w-0 shrink-0 grow-0 basis-full", orientation === "horizontal" ? "pl-4" : "pt-4", className, )} {...props} /> );}
function CarouselPrevious({ className, variant = "outline", size = "icon-sm", ...props}: React.ComponentProps<typeof Button>) { const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return ( <Button data-slot="carousel-previous" variant={variant} size={size} className={cn( "absolute touch-manipulation rounded-full", orientation === "horizontal" ? "inset-y-0 -left-12 my-auto" : "-top-12 left-1/2 -translate-x-1/2 rotate-90", className, )} disabled={!canScrollPrev} onClick={scrollPrev} {...props} > <CaretLeftIcon className="rtl:rotate-180" /> <span className="sr-only">Previous slide</span> </Button> );}
function CarouselNext({ className, variant = "outline", size = "icon-sm", ...props}: React.ComponentProps<typeof Button>) { const { orientation, scrollNext, canScrollNext } = useCarousel();
return ( <Button data-slot="carousel-next" variant={variant} size={size} className={cn( "absolute touch-manipulation rounded-full", orientation === "horizontal" ? "inset-y-0 -right-12 my-auto" : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", className, )} disabled={!canScrollNext} onClick={scrollNext} {...props} > <CaretRightIcon className="rtl:rotate-180" /> <span className="sr-only">Next slide</span> </Button> );}
// ✦ Position dots: the active slide is a wider --foreground pill.function CarouselDots({ className, ...props }: React.ComponentProps<"div">) { const { api } = useCarousel(); const [count, setCount] = React.useState(0); const [selected, setSelected] = React.useState(0);
React.useEffect(() => { if (!api) return; const update = () => { setCount(api.scrollSnapList().length); setSelected(api.selectedScrollSnap()); }; update(); api.on("reInit", update); api.on("select", update); return () => { api.off("reInit", update); api.off("select", update); }; }, [api]);
return ( <div data-slot="carousel-dots" className={cn("mt-3 flex items-center justify-center gap-1.5", className)} {...props} > {Array.from({ length: count }, (_, i) => ( <button // biome-ignore lint/suspicious/noArrayIndexKey: dots are positional key={i} type="button" aria-label={`Go to slide ${i + 1}`} aria-current={i === selected} onClick={() => api?.scrollTo(i)} className={cn( "h-1.5 rounded-full outline-none transition-all focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", i === selected ? "w-6 bg-foreground" : "w-1.5 bg-input", )} /> ))} </div> );}
export { Carousel, type CarouselApi, CarouselContent, CarouselDots, CarouselItem, CarouselNext, CarouselPrevious, useCarousel,};<script setup lang="ts">import type { CarouselEmits, CarouselProps, WithClassAsProps } from "./interface"import { cn } from "@/registry/edmi/lib/utils"import { useProvideCarousel } from "./useCarousel"
const props = withDefaults(defineProps<CarouselProps & WithClassAsProps>(), { orientation: "horizontal",})
const emits = defineEmits<CarouselEmits>()
const { canScrollNext, canScrollPrev, carouselApi, carouselRef, orientation, scrollNext, scrollPrev } = useProvideCarousel(props, emits)
defineExpose({ canScrollNext, canScrollPrev, carouselApi, carouselRef, orientation, scrollNext, scrollPrev,})
function onKeyDown(event: KeyboardEvent) { const prevKey = props.orientation === "vertical" ? "ArrowUp" : "ArrowLeft" const nextKey = props.orientation === "vertical" ? "ArrowDown" : "ArrowRight"
if (event.key === prevKey) { event.preventDefault() scrollPrev()
return }
if (event.key === nextKey) { event.preventDefault() scrollNext() }}</script>
<template> <div data-slot="carousel" :class="cn('relative outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring', props.class)" role="region" aria-roledescription="carousel" tabindex="0" @keydown="onKeyDown" > <slot :can-scroll-next :can-scroll-prev :carousel-api :carousel-ref :orientation :scroll-next :scroll-prev /> </div></template><script setup lang="ts">import type { WithClassAsProps } from "./interface"import { cn } from "@/registry/edmi/lib/utils"import { useCarousel } from "./useCarousel"
defineOptions({ inheritAttrs: false,})
const props = defineProps<WithClassAsProps>()
// `carouselRef` is bound through the template ref below.const { carouselRef, orientation } = useCarousel()void carouselRef</script>
<template> <div ref="carouselRef" data-slot="carousel-content" class="overflow-hidden" > <div :class=" cn( 'flex', orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col', props.class, )" v-bind="$attrs" > <slot /> </div> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import type { UnwrapRefCarouselApi } from './interface'import { computed, onBeforeUnmount, ref, watch } from 'vue'import { cn } from '@/registry/edmi/lib/utils'import { useCarousel } from './useCarousel'
// ✦ Position dots: the active slide is a wider --foreground pill.const props = defineProps<{ class?: HTMLAttributes['class'] }>()
const { carouselApi } = useCarousel()const count = ref(0)const selected = ref(0)const slides = computed(() => Array.from({ length: count.value }, (_, i) => i))
function update(api: UnwrapRefCarouselApi) { if (!api) return count.value = api.scrollSnapList().length selected.value = api.selectedScrollSnap()}
let off: (() => void) | undefinedwatch( carouselApi, (api) => { off?.() off = undefined if (!api) return const handler = () => update(api) handler() api.on('reInit', handler) api.on('select', handler) off = () => { api.off('reInit', handler) api.off('select', handler) } }, { immediate: true },)onBeforeUnmount(() => off?.())</script>
<template> <div data-slot="carousel-dots" :class="cn('mt-3 flex items-center justify-center gap-1.5', props.class)" > <button v-for="i in slides" :key="i" type="button" :aria-label="`Go to slide ${i + 1}`" :aria-current="i === selected" :class="cn( 'h-1.5 rounded-full outline-none transition-all focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring', i === selected ? 'w-6 bg-foreground' : 'w-1.5 bg-input', )" @click="carouselApi?.scrollTo(i)" /> </div></template><script setup lang="ts">import type { WithClassAsProps } from "./interface"import { cn } from "@/registry/edmi/lib/utils"import { useCarousel } from "./useCarousel"
const props = defineProps<WithClassAsProps>()
const { orientation } = useCarousel()</script>
<template> <div data-slot="carousel-item" role="group" aria-roledescription="slide" :class="cn( 'min-w-0 shrink-0 grow-0 basis-full', orientation === 'horizontal' ? 'pl-4' : 'pt-4', props.class, )" > <slot /> </div></template><script setup lang="ts">import type { WithClassAsProps } from "./interface"import type { ButtonVariants } from "@/registry/edmi/ui/button"import { ChevronRight } from "@lucide/vue"import { cn } from "@/registry/edmi/lib/utils"import { Button } from "@/registry/edmi/ui/button"import { useCarousel } from "./useCarousel"
const props = withDefaults(defineProps<{ variant?: ButtonVariants["variant"] size?: ButtonVariants["size"]}& WithClassAsProps>(), { variant: "outline", size: "icon-sm",})
const { orientation, canScrollNext, scrollNext } = useCarousel()</script>
<template> <Button data-slot="carousel-next" :disabled="!canScrollNext" :class="cn( 'absolute touch-manipulation rounded-full', orientation === 'horizontal' ? 'inset-y-0 -right-12 my-auto' : '-bottom-12 left-1/2 -translate-x-1/2 rotate-90', props.class, )" :variant="variant" :size="size" @click="scrollNext" > <slot> <ChevronRight class="rtl:rotate-180" /> <span class="sr-only">Next slide</span> </slot> </Button></template><script setup lang="ts">import type { WithClassAsProps } from "./interface"import type { ButtonVariants } from "@/registry/edmi/ui/button"import { ChevronLeft } from "@lucide/vue"import { cn } from "@/registry/edmi/lib/utils"import { Button } from "@/registry/edmi/ui/button"import { useCarousel } from "./useCarousel"
const props = withDefaults(defineProps<{ variant?: ButtonVariants["variant"] size?: ButtonVariants["size"]}& WithClassAsProps>(), { variant: "outline", size: "icon-sm",})
const { orientation, canScrollPrev, scrollPrev } = useCarousel()</script>
<template> <Button data-slot="carousel-previous" :disabled="!canScrollPrev" :class="cn( 'absolute touch-manipulation rounded-full', orientation === 'horizontal' ? 'inset-y-0 -left-12 my-auto' : '-top-12 left-1/2 -translate-x-1/2 rotate-90', props.class, )" :variant="variant" :size="size" @click="scrollPrev" > <slot> <ChevronLeft class="rtl:rotate-180" /> <span class="sr-only">Previous slide</span> </slot> </Button></template>export { default as Carousel } from "./Carousel.vue";export { default as CarouselContent } from "./CarouselContent.vue";export { default as CarouselDots } from "./CarouselDots.vue";export { default as CarouselItem } from "./CarouselItem.vue";export { default as CarouselNext } from "./CarouselNext.vue";export { default as CarouselPrevious } from "./CarouselPrevious.vue";export type { UnwrapRefCarouselApi as CarouselApi } from "./interface";
export { useCarousel } from "./useCarousel";import type useEmblaCarousel from "embla-carousel-vue";import type { EmblaCarouselVueType } from "embla-carousel-vue";import type { HTMLAttributes, UnwrapRef } from "vue";
type CarouselApi = EmblaCarouselVueType[1];type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;type CarouselOptions = UseCarouselParameters[0];type CarouselPlugin = UseCarouselParameters[1];
export type UnwrapRefCarouselApi = UnwrapRef<CarouselApi>;
export interface CarouselProps { opts?: CarouselOptions; plugins?: CarouselPlugin; orientation?: "horizontal" | "vertical";}
export type CarouselEmits = ( e: "init-api", payload: UnwrapRefCarouselApi,) => void;
export interface WithClassAsProps { class?: HTMLAttributes["class"];}import { createInjectionState } from "@vueuse/core";import emblaCarouselVue from "embla-carousel-vue";import { onMounted, ref } from "vue";import type { UnwrapRefCarouselApi as CarouselApi, CarouselEmits, CarouselProps,} from "./interface";
const [useProvideCarousel, useInjectCarousel] = createInjectionState( ({ opts, orientation, plugins }: CarouselProps, emits: CarouselEmits) => { const [emblaNode, emblaApi] = emblaCarouselVue( { ...opts, axis: orientation === "horizontal" ? "x" : "y", }, plugins, );
function scrollPrev() { emblaApi.value?.scrollPrev(); } function scrollNext() { emblaApi.value?.scrollNext(); }
const canScrollNext = ref(false); const canScrollPrev = ref(false);
function onSelect(api: CarouselApi) { canScrollNext.value = api?.canScrollNext() || false; canScrollPrev.value = api?.canScrollPrev() || false; }
onMounted(() => { if (!emblaApi.value) return;
emblaApi.value?.on("init", onSelect); emblaApi.value?.on("reInit", onSelect); emblaApi.value?.on("select", onSelect);
emits("init-api", emblaApi.value); });
return { carouselRef: emblaNode, carouselApi: emblaApi, canScrollPrev, canScrollNext, scrollPrev, scrollNext, orientation, }; },);
function useCarousel() { const carouselState = useInjectCarousel();
if (!carouselState) throw new Error("useCarousel must be used within a <Carousel />");
return carouselState;}
export { useCarousel, useProvideCarousel };<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import { type CarouselAPI, type CarouselProps, type EmblaContext, setEmblaContext, } from "./context.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", className)} role="region" aria-roledescription="carousel" {...restProps}> {@render children?.()}</div><script lang="ts"> import emblaCarouselSvelte from "embla-carousel-svelte"; import { cn, type WithElementRef } from "$lib/utils.js"; import { getEmblaContext } from "./context.js"; import type { HTMLAttributes } from "svelte/elements";
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" 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-4" : "-mt-4 flex-col", className )} data-embla-container="" {...restProps} > {@render children?.()} </div></div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import { getEmblaContext } from "./context.js"; import type { HTMLAttributes } from "svelte/elements";
// ✦ Position dots: the active slide is a wider --foreground pill. let { ref = $bindable(null), class: className, ...restProps }: WithElementRef<Omit<HTMLAttributes<HTMLDivElement>, "children">> = $props();
const emblaCtx = getEmblaContext("<Carousel.Dots/>");</script>
<div bind:this={ref} data-slot="carousel-dots" class={cn("mt-3 flex items-center justify-center gap-1.5", className)} {...restProps}> {#each emblaCtx.scrollSnaps as _, i (i)} <button type="button" aria-label={`Go to slide ${i + 1}`} aria-current={i === emblaCtx.selectedIndex} onclick={() => emblaCtx.scrollTo(i)} class={cn( "h-1.5 rounded-full outline-none transition-all focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", i === emblaCtx.selectedIndex ? "w-6 bg-foreground" : "w-1.5 bg-input" )} ></button> {/each}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import { getEmblaContext } from "./context.js"; import type { HTMLAttributes } from "svelte/elements";
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", emblaCtx.orientation === "horizontal" ? "ps-4" : "pt-4", className )} data-embla-slide="" {...restProps}> {@render children?.()}</div><script lang="ts"> import CaretRightIcon from "phosphor-svelte/lib/CaretRight"; import { Button, type Props } from "$lib/registry/ui/button/index.js"; import { cn } from "$lib/utils.js"; import { getEmblaContext } from "./context.js"; import type { WithoutChildren } from "bits-ui";
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( "absolute touch-manipulation rounded-full", emblaCtx.orientation === "horizontal" ? "inset-y-0 -end-12 my-auto" : "start-1/2 -bottom-12 -translate-x-1/2 rotate-90", className )} onclick={emblaCtx.scrollNext} onkeydown={emblaCtx.handleKeyDown} bind:ref {...restProps}> <CaretRightIcon class="rtl:rotate-180" /> <span class="sr-only">Next slide</span></Button><script lang="ts"> import CaretLeftIcon from "phosphor-svelte/lib/CaretLeft"; import { Button, type Props } from "$lib/registry/ui/button/index.js"; import { cn } from "$lib/utils.js"; import { getEmblaContext } from "./context.js"; import type { WithoutChildren } from "bits-ui";
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( "absolute touch-manipulation rounded-full", emblaCtx.orientation === "horizontal" ? "inset-y-0 -start-12 my-auto" : "start-1/2 -top-12 -translate-x-1/2 rotate-90", className )} onclick={emblaCtx.scrollPrev} onkeydown={emblaCtx.handleKeyDown} {...restProps} bind:ref> <CaretLeftIcon class="rtl:rotate-180" /> <span class="sr-only">Previous slide</span></Button>import type { EmblaCarouselSvelteType, default as emblaCarouselSvelte,} from "embla-carousel-svelte";import { getContext, hasContext, setContext } from "svelte";import type { HTMLAttributes } from "svelte/elements";import type { WithElementRef } from "$lib/utils.js";
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 Dots from "./carousel-dots.svelte";import Item from "./carousel-item.svelte";import Next from "./carousel-next.svelte";import Previous from "./carousel-previous.svelte";
export { Content, Content as CarouselContent, Dots, Dots as CarouselDots, Item, Item as CarouselItem, Next, Next as CarouselNext, Previous, Previous as CarouselPrevious, Root, // Root as Carousel,};| Prop | Type | Default |
|---|---|---|
opts |
Embla options | — |
plugins |
Embla plugins | — |
orientation |
"horizontal" | "vertical" |
"horizontal" |
setApi |
(api: CarouselApi) => void |
— |
Anatomy: CarouselContent, CarouselItem, CarouselPrevious, CarouselNext.
✦ Edmi additions
Section titled “✦ Edmi additions”CarouselDots: position dots (active slide is a wider pill).
Vue / Svelte differences
Section titled “Vue / Svelte differences”CarouselDotsis shipped in all three frameworks.- Vue: the carousel emits
init-api, the equivalent of React’ssetApi. - Svelte: pass a
setApiprop, as in React.