# Introduction Blossom Carousel is the first carousel library that builds on native browser scrolling instead of replacing it. It provides a small drag enhancement for pointer devices while maintaining the core interaction model, performance, and accessibility of a real scroll container. ## Why Blossom Carousel? Carousels have become a powerful and popular choice among the many ways to structure content hierarchy. They are one of the main means to create a spatial form of progressive disclosure by collapsing vertical real estate while inviting horizontal exploration. Their scrolling behavior makes them stand out especially on touch devices where swiping is such a natural gesture. Web standards have evolved to a point where these swipable carousels feel incredibly native. However, horizontal scrolling on desktop lacks the intuitive drag interaction that makes touch-based carousels feel so natural. Blossom Carousel fills that gap with a custom, lightweight physics-based drag engine that works seamlessly on top of native scrolling. ::note{color="neutral" icon="i-lucide-lightbulb"} Blossom's dragging engine syncs with your CSS configuration automatically. :: ::card-group :::card{color="neutral" icon="i-lucide-gauge" title="Native scrolling"} Full browser performance, platform scrolling behavior, and accessibility from a real horizontal scroll container. ::: :::card{color="neutral" icon="i-lucide-mouse-pointer-2" title="Dragging"} Custom physics-based dragging for pointer input, layered on top of native scrolling instead of replacing it. ::: :::card{color="neutral" icon="i-lucide-code-2" title="No abstraction"} Use native web APIs directly. Blossom keeps your carousel as real markup, real CSS, and real scroll state. ::: :::card --- color: neutral icon: i-lucide-sliders-horizontal title: Configure with CSS --- Works with native [scroll snap](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Scroll_snap){rel=""nofollow""} , [position: sticky](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/position#sticky){rel=""nofollow""} , [scroll-driven animations](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Scroll-driven_animations){rel=""nofollow""} , [scroll-state queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Conditional_rules/Container_scroll-state_queries){rel=""nofollow""} and more. ::: :::card{color="neutral" icon="i-lucide-feather" title="0 KB on touch devices"} Blossom only loads the drag enhancement when a fine pointer device is detected, so touch devices keep native scrolling untouched. ::: :::card --- color: neutral icon: i-lucide-blocks title: Works with major frameworks --- Components are available for React, Vue, Svelte, and Web Components, with a framework-agnostic core package underneath. ::: :: ## Next steps - [Installation](https://www.blossom-carousel.com/docs/getting-started/installation) - Add Blossom Carousel to your project. - [Examples](https://www.blossom-carousel.com/docs/examples/basic/simple) - Explore practical implementations. # Installation ::prompt --- actions: - copy - cursor description: Ask my Agent to install Blossom Carousel icon: i-lucide-square-chevron-right --- Install Blossom Carousel in my project. - Detect the framework I'm using (Vue or Nuxt, React or Next.js, Svelte or SvelteKit, Web Components, or standalone) - Install Blossom Carousel with my package manager: pnpm add @blossom-carousel/ :framework[ (or npm/yarn/bun)] - Wire up the framework-specific plugin or component Docs: {rel=""nofollow""} :: ## Install Install the package that matches your framework or use Blossom manually with the Core package. ::code-group{sync="migration-code-style"} ```bash [Vue] npm install @blossom-carousel/vue ``` ```bash [React] npm install @blossom-carousel/react ``` ```bash [Svelte] npm install @blossom-carousel/svelte ``` ```bash [Web] npm install @blossom-carousel/web ``` ```bash [Core] npm install @blossom-carousel/core ``` :: ## Using Agent Skills If you use an AI assistant (Claude Code, Cursor, etc.), install the blossom-carousel skill for guided setup and code review: ::code-block ```bash [Terminal] npx skills add https://www.blossom-carousel.com ``` ## Packages Blossom is available as framework wrappers and as a framework-agnostic core package. :::card-group ::::card --- color: neutral icon: i-simple-icons-vuedotjs title: Vue and Nuxt to: https://www.blossom-carousel.com/docs/framework-guides/vue-nuxt --- Start with `@blossom-carousel/vue` :::: ::::card --- color: neutral icon: i-simple-icons-react title: React and Next.js to: https://www.blossom-carousel.com/docs/framework-guides/react-nextjs --- Start with `@blossom-carousel/react` :::: ::::card --- color: neutral icon: i-simple-icons-svelte title: Svelte and SvelteKit to: https://www.blossom-carousel.com/docs/framework-guides/svelte-sveltekit --- Start with `@blossom-carousel/svelte` :::: ::::card --- color: neutral icon: i-simple-icons-webcomponentsdotorg title: Web Components to: https://www.blossom-carousel.com/docs/framework-guides/web-components --- Start with `@blossom-carousel/web` :::: ::::card --- color: neutral icon: i-lucide-braces title: Core to: https://www.blossom-carousel.com/docs/framework-guides/core --- Start with `@blossom-carousel/core` :::: ::: :: # Vue and Nuxt ## Install ::code-group ```bash [pnpm] pnpm add @blossom-carousel/vue ``` ```bash [bun] bun add @blossom-carousel/vue ``` ```bash [yarn] yarn add @blossom-carousel/vue ``` ```bash [npm] npm install @blossom-carousel/vue ``` :: ## Local component usage ```vue ``` ## Nuxt plugin Register the component globally to use it across the app. ```ts [plugins/blossom-carousel.ts] import { BlossomCarousel } from "@blossom-carousel/vue"; import "@blossom-carousel/vue/style.css"; export default defineNuxtPlugin((nuxtApp) => { nuxtApp.vueApp.component("BlossomCarousel", BlossomCarousel); }); ``` ## Semantic root element Use `as="ul"` for list-like carousels. ```html
  • Slide {{ i }}
  • ``` ## Navigation controls Place previous, next, and dot controls outside the carousel with ``, ``, and ``. Link them to the carousel with an `id` on ``, and a matching `for` prop on each control and mark slides with `data-blossom-slide`. ```html
    Slide {{ i }}
    ``` #### Prev/Next Buttons `` and `` are aware of configured scroll-snap and will navigate between snap points. When no scroll-snap is configured, they will slide they carousel proportionally. Slot your own content to replace the default button icon. ```html Previous ``` #### Dots `` renders one button per slide marked with `data-blossom-slide`. Default styles can be themed with CSS custom properties on the component or any ancestor: ```css /* defaults */ { --blossom-dot-size: 0.625rem; --blossom-dot-radius: 50%; --blossom-dot-color: currentColor; --blossom-dot-opacity: 0.35; --blossom-dot-hover-opacity: 0.6; --blossom-dot-active-opacity: 1; } ``` To bring your own dots, provide a default slot and render `` inside it. This will configure the dot as a ` ``` Or let `blossom-dot` create the button from its light-DOM children: ```html • ``` The prototype `` stays hidden; clones are rendered in the dots container. Existing `aria-label` and other attributes on your button are preserved. Since it's a single cloned template, every dot gets identical content — fine for a single active/inactive look, but not for content that varies per slide (e.g. per-slide thumbnails). For that, set `renderDot` on the `blossom-dots` element — a callback invoked once per slide with `(index, active, forId)`. It takes priority over any `` prototype and returns the element to render; navigation attributes are merged onto its ` ``` ```css [.css] .example { /* Gives the carousel and scrollbar a shared named scroll timeline. */ timeline-scope: --custom-scrollbar; } .carousel { /* The carousel's horizontal scroll position drives the animations below. */ scroll-timeline: --custom-scrollbar inline; } .scrollbar { position: relative; width: 300px; height: 16px; } .track .progress { /* pathLength="1" lets us animate the whole path with values from 1 to 0. */ animation: progress 1s linear both; animation-timeline: --custom-scrollbar; stroke-dasharray: 1; } .handle { position: absolute; top: 0; left: 0; /* Same path data as the SVG stroke, in CSS pixels. url(#id) follows the SVG geometry, which Chrome 155+ resolves in device pixels, so on a 2× display the handle travels half as far. */ offset-path: path("cosinePath"); offset-anchor: auto; animation: handle 1s linear both; animation-timeline: --custom-scrollbar; } @keyframes progress { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } } @keyframes handle { from { offset-distance: 0%; } to { offset-distance: 100%; } } ``` ```ts [.ts] let svgRect: DOMRect let offsetX: number let activeHandle: HTMLButtonElement | null = null const carousel = document.querySelector('.carousel')! const svg = document.querySelector('.track')! const handle = document.querySelector('.handle')! handle.addEventListener('pointerdown', onPointerDown) function onPointerDown(event: PointerEvent) { const handleRect = handle.getBoundingClientRect() // Keep receiving pointer events while dragging outside the handle. activeHandle = handle handle.setPointerCapture(event.pointerId) // Measure once at the start so pointer movement maps to track progress. svgRect = svg.getBoundingClientRect() offsetX = event.clientX - handleRect.left window.addEventListener('pointermove', onPointerMove) window.addEventListener('pointerup', onPointerUp) } function onPointerMove(event: PointerEvent) { const newX = event.clientX - offsetX const percentageX = ((newX - svgRect.left) / svgRect.width) * 100 // Convert track progress to the carousel's scroll range. carousel.scrollTo({ left: (percentageX / 100) * (carousel.scrollWidth - carousel.clientWidth), behavior: 'instant', }) } function onPointerUp(event: PointerEvent) { activeHandle?.releasePointerCapture(event.pointerId) activeHandle = null window.removeEventListener('pointermove', onPointerMove) window.removeEventListener('pointerup', onPointerUp) } ``` :::: ::: :::tabs-item{label="Tailwind"} ::::code-group ```html [.html]
    ...
    ``` ```css [.css] .group { /* Gives the carousel and scrollbar a shared named scroll timeline. */ timeline-scope: --custom-scrollbar; } .carousel { /* The carousel's horizontal scroll position drives the animations below. */ scroll-timeline: --custom-scrollbar inline; } svg path:last-child { /* pathLength="1" lets us animate the whole path with values from 1 to 0. */ animation: progress 1s linear both; animation-timeline: --custom-scrollbar; stroke-dasharray: 1; } .handle { /* Same path data as the SVG stroke, in CSS pixels. url(#id) follows the SVG geometry, which Chrome 155+ resolves in device pixels, so on a 2× display the handle travels half as far. */ offset-path: path("cosinePath"); offset-anchor: auto; animation: handle 1s linear both; animation-timeline: --custom-scrollbar; } @keyframes progress { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } } @keyframes handle { from { offset-distance: 0%; } to { offset-distance: 99%; } } ``` ```ts [.ts] let svgRect: DOMRect let offsetX: number let activeHandle: HTMLButtonElement | null = null const carousel = document.querySelector('.carousel')! const svg = document.querySelector('.track')! const handle = document.querySelector('.handle')! handle.addEventListener('pointerdown', onPointerDown) function onPointerDown(event: PointerEvent) { const handleRect = handle.getBoundingClientRect() // Keep receiving pointer events while dragging outside the handle. activeHandle = handle handle.setPointerCapture(event.pointerId) // Measure once at the start so pointer movement maps to track progress. svgRect = svg.getBoundingClientRect() offsetX = event.clientX - handleRect.left window.addEventListener('pointermove', onPointerMove) window.addEventListener('pointerup', onPointerUp) } function onPointerMove(event: PointerEvent) { const newX = event.clientX - offsetX const percentageX = ((newX - svgRect.left) / svgRect.width) * 100 // Convert track progress to the carousel's scroll range. carousel.scrollTo({ left: (percentageX / 100) * (carousel.scrollWidth - carousel.clientWidth), behavior: 'instant', }) } function onPointerUp(event: PointerEvent) { activeHandle?.releasePointerCapture(event.pointerId) activeHandle = null window.removeEventListener('pointermove', onPointerMove) window.removeEventListener('pointerup', onPointerUp) } ``` :::: ::: :: # Timeline :example-timeline ::tabs{sync="example-code-style"} :::tabs-item{label="CSS"} ::::code-group ```css [.css] .tracks { display: grid; width: max-content; gap: 0.125rem; } .track { display: flex; height: 3rem; gap: 0.125rem; padding-inline: 3rem; } .clip { padding: 0.25rem 0.5rem; border-radius: 0.5rem; } .clip-label { position: sticky; left: 0.5rem; right: 0.5rem; width: fit-content; } ``` ```html [.html]

    A quiet bloom begins

    Soft Blossom Preset

    ...
    ...
    ...
    ``` :::: ::: :::tabs-item{label="Tailwind"} ```html [.html]

    A quiet bloom begins

    Soft Blossom Preset

    ...
    ...
    ...
    ``` ::: :: # CSS Controls ::supports-scroll-marker-support :::caution css ::scroll-marker is not yet supported in this browser. Check browser compatibility at [caniuse.com](https://caniuse.com/#feat=css-container-queries){rel=""nofollow""} ::: :: :example-css-controls ::tabs{sync="example-code-style"} :::tabs-item{label="CSS"} ::::code-group ```css [.css] .container { anchor-name: --carousel; } .carousel { display: grid; grid-auto-flow: column; grid-auto-columns: 200px; gap: 1rem; scroll-marker-group: after; &::scroll-button(*) { position: fixed; position-anchor: --carousel; position-visibility: always; border: none; font-size: 1rem; background: white; color: cyan; aspect-ratio: 1; border-radius: 50%; cursor: pointer; box-shadow: 0 1px 8px 0 rgb(0 0 0 / 0.2); transition: 100ms scale ease; z-index: 1; } &::scroll-button(*):hover { scale: 1.1; } &::scroll-button(*):active { scale: 0.98; } &::scroll-button(left) { position-area: inline-start center; left: calc(anchor(left) + 2.5rem); padding-right: 0.55rem; content: "◄" / "Previous"; } &::scroll-button(right) { position-area: inline-end center; left: calc(anchor(right) - 2.5rem); padding-left: 0.55rem; content: "►" / "Next"; } &::scroll-marker-group { position: fixed; position-anchor: --carousel; position-area: block-end; top: calc(anchor(bottom) - 3rem); position-visibility: always; margin: 10px; display: grid; grid-auto-columns: 1rem; grid-auto-flow: column; gap: 0.5rem; } > .slide { width: 100%; aspect-ratio: 3 / 4; &::scroll-marker { content: ""; aspect-ratio: 1; border-radius: 50%; background: cyan; opacity: 0.3; } &::scroll-marker:hover { opacity: 0.8; } &::scroll-marker:target-current { opacity: 1; } } } ``` ```html [.html]
    ...
    ``` :::: ::: :::tabs-item{label="Tailwind"} ```html [.html]
    ...
    ``` ::: :: ::note{color="neutral" icon="i-lucide-lightbulb"} Learn more about [CSS Carousels](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Overflow/Carousels){rel=""nofollow""} . :: # Scroll State Query ::supports-scroll-state-support :::caution Scroll-state queries are not yet supported in this browser. Check browser compatibility at [caniuse.com](https://caniuse.com/#feat=css-container-queries){rel=""nofollow""} ::: :: :example-scroll-state ::tabs{sync="example-code-style"} :::tabs-item{label="CSS"} ::::code-group ```css [.css] .carousel { display: grid; grid-auto-flow: column; grid-auto-columns: 200px; gap: 1rem; scroll-snap-type: x mandatory; } .slide { width: 100%; aspect-ratio: 3 / 4; scroll-snap-align: center; container-type: scroll-state; container-name: snap-container; } .card { width: 100%; height: 100%; background: color-mix(in srgb, var(--color-primary) 50%, transparent); } @container snap-container scroll-state(snapped: x) { .card { background: var(--color-primary); } } ``` ```html [.html]
    ...
    ``` :::: ::: :::tabs-item{label="Tailwind"} ::::code-group ```css [.css] .slide { container-type: scroll-state; container-name: snap-container; } @container snap-container scroll-state(snapped: x) { .card { background: var(--color-primary); } } ``` ```html [.html]
    ...
    ``` :::: ::: :: # Scroll Snap Events ::supports-scroll-snap-event-support :::caution Scroll-snap events are not yet supported in this browser. Check browser compatibility at [caniuse.com](https://caniuse.com/?search=scroll-snap-change){rel=""nofollow""} ::: :: :example-scroll-snap-events ::code-group ```ts [.ts] const carousel = document.querySelector(".carousel") as HTMLElement; const videos = carousel.querySelectorAll("video"); carousel.addEventListener("scrollsnapchange", (event) => { (event.snapTargetInline as HTMLVideoElement)?.play(); }); carousel.addEventListener("scrollsnapchanging", () => { videos.forEach((video) => video.pause()); }); ``` ```css [.css] .carousel { display: grid; grid-auto-flow: column; grid-auto-columns: 400px; gap: 1rem; scroll-snap-type: x mandatory; } .slide { width: 100%; aspect-ratio: 16 / 9; scroll-snap-align: center; object-fit: cover; border-radius: 0.5rem; } ``` ```html [.html] ... ``` :: # Repeat ::warning This feature is experimental and might not work as expected. :: :example-repeat ```html [.html]
    ...
    ``` # Scroll Trigger ::supports-scroll-trigger-support :::caution Animation-trigger is not yet supported in this browser. Check browser compatibility at [caniuse.com](https://caniuse.com/?search=animation-trigger){rel=""nofollow""} ::: :: :example-scroll-trigger ::tabs{sync="example-code-style"} :::tabs-item{label="CSS"} ::::code-group ```css [.css] .carousel { display: grid; grid-auto-flow: column; grid-auto-columns: 200px; gap: 1rem; scroll-snap-type: x mandatory; } .slide { width: 100%; aspect-ratio: 3 / 4; scroll-snap-align: center; timeline-trigger-name: --t; timeline-trigger-source: view(inline); animation: unclip 0.35s ease-in-out both; animation-trigger: --t play-forwards play-backwards; } @keyframes unclip { from { clip-path: inset(100% 0 0 0 round 1rem); } to { clip-path: inset(0); } } ``` ```html [.html]
    ...
    ``` :::: ::: :::tabs-item{label="Tailwind"} ::::code-group ```css [.css] .slide { timeline-trigger-name: --t; timeline-trigger-source: view(inline); animation: unclip 0.35s ease-in-out both; animation-trigger: --t play-forwards play-backwards; } @keyframes unclip { from { clip-path: inset(100% 0 0 0 round 1rem); } to { clip-path: inset(0); } } ``` ```html [.html]
    ...
    ``` :::: ::: :: # Examples :examples-table # Accessibility Guide Accessible carousels are not created by adding a single ARIA attribute. They depend on ordinary web fundamentals working together: meaningful HTML, reachable controls, predictable focus, controllable motion, visible state, and tested assistive technology output. Blossom gives you a native scroll container and progressive drag support. The accessibility of the carousel still depends on the markup, controls, labels, and behavior you build around that container. ::note{color="neutral" icon="i-lucide-info"} WCAG is a baseline, not a guarantee of usability. A carousel can pass automated checks and still be confusing if controls are unnamed, focus moves into hidden content, or screen readers hear semantics that do not match the visual pattern. :: ## Start with the right pattern Not every horizontal scroller needs to be announced as a carousel. Choose the lightest pattern that matches the content and behavior. | Pattern | Use when | Recommended structure | | ----------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | Scrollable list | Several related items are visible and users can scroll through them naturally. | A list such as `
      ` with `
    • ` items, plus optional previous and next buttons. | | Carousel region | The component is a distinct page feature with slides and controls. | A labelled `
      ` or `role="region"` with `aria-roledescription="carousel"`. | | Tabbed carousel | Only one slide is displayed at a time and slide pickers behave exactly like tabs. | A real tabs pattern with `tablist`, `tab`, and `tabpanel`. | | Gallery or product rail | Multiple cards are visible at once and all cards remain normal page content. | A list of articles/cards with ordinary links and buttons. | Avoid giving a component a role that promises behavior you do not implement. If the user hears "tab", arrow keys, selected state, and tabpanel relationships need to work like a tabs widget. ## Use semantic slide markup When slides are a set of related items, use a list. This gives assistive technologies a familiar count and structure. ```html [List-based carousel]
    • ``` Use a carousel role description only when the component benefits from being identified as a carousel. ```html [Named carousel region]
      ``` The accessible label should not repeat the word "carousel" when `aria-roledescription="carousel"` is present. A screen reader may announce the role description after the label, so "Popular products carousel, carousel" is noisy. ## Provide real controls Dragging and swiping are not enough. WCAG 2.5.7 requires functionality that uses dragging to have a non-drag alternative unless dragging is essential. Use real buttons for carousel actions: - Previous slide or previous set. - Next slide or next set. - Pause and play, if the carousel auto-rotates. - Slide picker controls, if users can jump to a specific slide. ### Built-in navigation components Blossom ships navigation components that cover these requirements out of the box. ``, ``, and `` render native buttons, connect to the carousel through its `id`, and provide a keyboard-operable, non-drag alternative — even on touch devices where the drag engine is not loaded. ```html [Built-in controls]
      ``` Mark each slide with `data-blossom-slide` so the controls can track the slide count, the active slide, and the first and last positions. What the components expose to assistive technologies: - Every control is a native ` ``` If multiple items are visible at once, name controls for what they actually do. "Next products" or "Next articles" can be more honest than "Next slide". Do not disable previous and next buttons only visually. If a button cannot perform an action, use the native `disabled` attribute when it should be skipped in the tab order. ```html [Disabled edge control] ``` For slide picker buttons, `aria-disabled="true"` can be useful for the current slide picker when users benefit from encountering the current item in the tab sequence. ```html [Current slide picker]
      ``` ## Name every control clearly Every carousel control needs a unique accessible name. Repeated names like "Show slide" or "Read more" make the carousel hard to operate with screen readers and speech control. Good names tell the user what will happen: ```html [Specific control names] ``` If slide names are not available, use position and set size. ```html [Position-based names] ``` This is what the built-in components do: `` and `` default to "Previous slide" and "Next slide", and `` names each dot "Go to slide 1", "Go to slide 2", and so on. Override the prev and next labels with `aria-label` when more specific names fit the content better. When visible text exists, keep the accessible name aligned with it. This helps speech recognition users activate controls by the words they can see. ```html [Label in name] ``` Icon-only buttons need a name on the button. Hide the icon itself if it is decorative. ```html [Icon-only carousel button] ``` ## Keep keyboard behavior predictable Keyboard users should be able to reach the carousel, understand the controls, operate them repeatedly, and leave the component without surprise. Expected behavior: - `Tab` and `Shift + Tab` move through controls and focusable slide content in DOM order. - `Enter` and `Space` activate carousel buttons. - Previous and next buttons do not move focus after activation. - Focus is always visible. - Focus does not land on content that is visually hidden or intentionally unavailable. - Keyboard users can access the same functionality as pointer and touch users. You usually do not need custom scripting for `Tab`. Let the browser handle normal tab order unless you are implementing a real composite widget such as tabs. ```css [Visible focus] .carousel :focus-visible, .carousel-controls :focus-visible { outline: 3px solid currentColor; outline-offset: 3px; } ``` Avoid positive `tabindex` values. Put controls and slides in the DOM order users should experience. ## Do not trap focus in off-screen slides Native horizontal scrolling often keeps all slides in the document flow. That is good for simple scrollable lists, but it can be confusing when a carousel presents itself as showing only one slide. If all slides remain part of the normal content, it is acceptable for keyboard focus to move through them in order. In that case, do not claim the component behaves like tabs or like a single active panel. If only one slide should be available at a time, inactive slides need to be unavailable to both keyboard and assistive technology users. Depending on the implementation, use conditional rendering, `hidden`, `inert`, or another approach that removes inactive interactive content from the tab order and accessibility tree. ```html [Inactive slide removed]

      Canvas tote

      View product
      ``` Do not use `aria-hidden="true"` on a slide that still contains focusable links or buttons. That creates focusable controls that are hidden from assistive technologies. ## Handle motion and auto-rotation Auto-rotation is one of the easiest ways to make a carousel inaccessible. It can move content before users finish reading, disorient screen reader users, and distract people with attention or vestibular disabilities. If a carousel auto-rotates: - Provide a pause or stop button. - Put the rotation control before the rotating content in the tab order. - Stop rotation when keyboard focus enters the carousel. - Stop rotation when the pointer hovers over the carousel. - Do not restart rotation unless the user explicitly asks for it. - Keep the control label action-based, such as "Pause featured articles" and "Play featured articles". ```html [Rotation control] ``` Respect reduced motion preferences for scripted animation, transitions, and smooth scrolling. ```css [Reduced motion] @media (prefers-reduced-motion: reduce) { .carousel { scroll-behavior: auto; } .carousel *, .carousel *::before, .carousel *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } ``` ## Announce changes with care Not every carousel movement needs a live region. Over-announcing slide changes can be as disruptive as not announcing them. Use these defaults: - Use `aria-live="off"` while a carousel is auto-rotating. - Use `aria-live="polite"` for user-initiated slide changes when the changed content is not otherwise obvious to assistive technologies. - Do not move focus just to announce a slide change. - Keep announcements short, such as "Slide 2 of 5" or the slide title. ```html [Slide status]

      Showing wool scarf, slide 2 of 5

      ``` If focus moves to the newly shown slide content as part of a deliberate workflow, an additional live region may be redundant. ## Choose picker semantics carefully Slide pickers can be buttons or tabs. The choice matters. Use buttons when: - Multiple slides can be visible at once. - Slides are part of a scrollable list. - Picker controls jump the scroll position but do not hide every other slide. - You want simple `Tab` navigation through each picker. `` follows the button pattern: a labelled `role="group"` of native buttons where the active dot exposes `aria-current`. This matches Blossom's behavior, since dots move the scroll position without hiding other slides. Do not wrap `` in tab semantics. Use tabs only when: - One panel is presented at a time. - Each `tab` has a corresponding `tabpanel`. - The selected tab state is always correct. - Inactive tabpanels are hidden from users. - Arrow key behavior follows the tabs pattern. ```html [Tabbed carousel shape]

      Canvas tote

      ``` A scroller that merely moves content left and right is usually not a tabs widget. ## Keep visual states perceivable Carousel state should not depend on color alone. - Give active slide pickers a visible shape, outline, underline, icon, or text change in addition to color. - Ensure focus indicators have at least 3:1 contrast against adjacent colors. - Ensure icon buttons and disabled states remain understandable in forced colors mode. - Do not hide scrollbars unless an equally clear affordance and keyboard path remain. - Make touch targets at least 24 by 24 CSS pixels, and aim for 44 by 44 CSS pixels when space allows. ```css [Forced colors-friendly states] .carousel-picker[aria-current="true"], .carousel-picker[aria-disabled="true"] { outline: 2px solid currentColor; } @media (forced-colors: active) { .carousel-picker[aria-current="true"], .carousel-picker[aria-disabled="true"] { outline-color: Highlight; } } ``` ## Test the carousel as a component Automated checks are useful, but carousel accessibility depends heavily on behavior. Manual testing is essential. Carousel test checklist: - The carousel has a clear accessible name when it is exposed as a region or group. - Previous, next, pause, play, and picker controls are real buttons or fully implement button behavior. - Every control has a unique accessible name. - All pointer and drag functionality has a keyboard alternative. - `Tab` order follows the visual and reading order. - Focus remains visible and is not hidden by sticky controls or clipped slide containers. - Previous and next controls can be activated repeatedly without focus moving unexpectedly. - Disabled or unavailable controls expose the correct disabled state. - Auto-rotation pauses on focus and hover. - Auto-rotation does not restart without user action. - Reduced motion preferences are respected. - Screen reader output matches the visible component: roles, names, selected states, and announcements are not misleading. - Off-screen or inactive slides do not create unexpected tab stops for the chosen pattern. - Slide picker semantics match the behavior: buttons for simple picker controls, tabs only for real tabbed panels. - The component works at 200 percent zoom and narrow viewport sizes. - Active, focus, disabled, and selected states remain visible in forced colors mode. ## Carousel WCAG map The most relevant WCAG success criteria for carousels are usually these: | Criteria | Carousel check | | ---------------------------- | -------------------------------------------------------------------------------------------- | | 1.1.1 Non-text Content | Images, icon buttons, and thumbnails have appropriate text alternatives or accessible names. | | 1.3.1 Info and Relationships | Slide structure, control groups, labels, and selected states are programmatically exposed. | | 1.3.2 Meaningful Sequence | DOM order matches the reading and keyboard order. | | 1.4.1 Use of Color | Active, selected, disabled, and error states are not conveyed by color alone. | | 1.4.3 Contrast Minimum | Text inside slides and controls meets contrast requirements. | | 1.4.10 Reflow | The carousel works when zoomed or viewed in narrow layouts. | | 1.4.11 Non-text Contrast | Buttons, focus indicators, and active picker states have sufficient contrast. | | 2.1.1 Keyboard | All carousel functionality works with a keyboard. | | 2.1.2 No Keyboard Trap | Users can leave the carousel with normal keyboard navigation. | | 2.2.2 Pause, Stop, Hide | Auto-rotating or moving content can be paused, stopped, or hidden. | | 2.4.3 Focus Order | Focus moves through controls and slide content in a meaningful order. | | 2.4.7 Focus Visible | Keyboard focus is always visible. | | 2.4.11 Focus Not Obscured | Focused controls and slide links are not hidden by overlays or clipping. | | 2.5.3 Label in Name | Visible control text is included in the accessible name. | | 2.5.7 Dragging Movements | Dragging has a non-drag alternative. | | 2.5.8 Target Size Minimum | Controls and picker targets are large enough to operate. | | 3.2.1 On Focus | Focusing a control does not unexpectedly change context. | | 4.1.2 Name, Role, Value | Custom controls expose correct names, roles, values, and states. | | 4.1.3 Status Messages | Slide changes are announced when appropriate without moving focus. | ## Useful resources - [WAI Carousels Tutorial](https://www.w3.org/WAI/tutorials/carousels/){rel=""nofollow""} - accessible carousel structure, functionality, animation, and styling guidance. - [ARIA Authoring Practices Guide: Carousel Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/carousel/){rel=""nofollow""} - expected carousel roles, states, properties, and keyboard behavior. - [ARIA Authoring Practices Guide: Tabs Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/){rel=""nofollow""} - use this when slide pickers are truly tabs. - [WCAG 2.2 Quick Reference](https://www.w3.org/WAI/WCAG22/quickref/){rel=""nofollow""} - success criteria, techniques, and filters. # Agent Skills Blossom Carousel's Agent Skills help AI coding assistants give package-specific guidance. ## Install the skills Compatible agents (Cursor, Claude Code, etc.) can discover and use skills automatically. To manually install with the skills CLI: ::code-block ```bash [Terminal] npx skills add https://www.blossom-carousel.com ``` ## Available skills | Skill | Use it when | | ------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `blossom-carousel-core` | You are working with the framework-agnostic Core package, direct DOM setup, lazy loading, `init()`, or `destroy()` | | `blossom-carousel-vue` | You are using Blossom Carousel in Vue or Nuxt | | `blossom-carousel-react` | You are using Blossom Carousel in React or Next.js | | `blossom-carousel-svelte` | You are using Blossom Carousel in Svelte or SvelteKit | | `blossom-carousel-web` | You are using the Web Component package or CDN setup | ## How to use a skill When your AI coding agent supports skills, you can ask for the package or framework directly. The agent can then use the matching skill automatically. Good prompts name the framework, the desired carousel pattern, and any constraints that matter. :::prompt --- actions: - copy description: Build an accessible testimonial carousel in React. icon: i-lucide-square-chevron-right --- Use Blossom Carousel React to build an accessible testimonial carousel. Keep layout in CSS and use scroll snap. ::: :::prompt --- actions: - copy description: Create a product carousel with semantic list markup in Nuxt. icon: i-lucide-square-chevron-right --- Use Blossom Carousel Vue in Nuxt. Register the component globally and create a product carousel with semantic list markup. ::: :::prompt --- actions: - copy description: Create an RTL carousel with Web Components. icon: i-lucide-square-chevron-right --- Use Blossom Carousel Web Components from a CDN. Create an RTL carousel and include the required stylesheet link. ::: :::note{color="neutral" icon="i-lucide-lightbulb"} For implementation details, use the matching [framework guide](https://www.blossom-carousel.com/docs/framework-guides/vue-nuxt) . ::: :: ## What the skills help with The skills keep implementation guidance consistent across packages. - Use the right package import for the chosen framework. - Import the stylesheet from your framework package (for example `@blossom-carousel/vue/style.css`). - Use the framework component instead of the Web Component when working in Vue, React, or Svelte. - Use the `as` prop for semantic root elements in framework wrappers. - Use `Blossom(element)`, `init()`, and `destroy()` for direct Core usage. - Check common setup problems such as missing styles, invalid slide structure, and duplicate Core initialization. # Embla Embla creates a carousel engine around a viewport, container, options object, event system, and plugin API. Blossom keeps the carousel as a native scroll container. Layout, snapping, alignment, spacing, direction, and responsive behavior belong in CSS. ## Migration mindset When moving from Embla to Blossom, migrate the carousel in three steps: 1. Replace the Embla viewport and container structure with a single `` root. 2. Move Embla options such as alignment, axis, slide sizing, and containment into CSS. 3. Replace Embla API calls with Blossom methods, native scroll APIs, and browser observers. ::note{color="neutral" icon="i-lucide-lightbulb"} Most Blossom migrations replace abstract JavaScript configuration with native expressions using CSS scroll snap, grid, sticky positioning, or native scrol API's. :: ## Installation Remove the Embla package for your framework and install the matching Blossom package. ::code-group{sync="migration-code-style"} ```bash [Vue] npm uninstall embla-carousel-vue npm install @blossom-carousel/vue ``` ```bash [React] npm uninstall embla-carousel-react npm install @blossom-carousel/react ``` ```bash [Svelte] npm uninstall embla-carousel-svelte npm install @blossom-carousel/svelte ``` ```bash [Core] npm uninstall embla-carousel npm install @blossom-carousel/core ``` :: Import the stylesheet from your framework package once in your app. ::code-group ```js [Vue] import "@blossom-carousel/vue/style.css"; ``` ```js [React] import "@blossom-carousel/react/style.css"; ``` ```js [Svelte] import "@blossom-carousel/svelte/style.css"; ``` ```js [Core] import "@blossom-carousel/core/style.css"; ``` ```js [Web] import "@blossom-carousel/web/style.css"; ``` :: ## Component structure Embla usually needs a viewport element, a container element, and slide elements. With Blossom, the carousel root is the scroll container and the slides are direct children. ::code-group{sync="migration-code-style"} ```vue [Vue] ``` ```tsx [React] import useEmblaCarousel from "embla-carousel-react"; // [!code --] import { BlossomCarousel } from "@blossom-carousel/react"; // [!code ++] import "@blossom-carousel/react/style.css"; // [!code ++] export function Carousel() { const [emblaRef] = useEmblaCarousel({ align: "start" }); // [!code --] return (
      {/* [!code --] */}
      {/* [!code --] */}
      Slide 1
      {/* [!code --] */}
      Slide 2
      {/* [!code --] */}
      Slide 3
      {/* [!code --] */}
      {/* [!code --] */}
      {/* [!code --] */} {/* [!code ++] */}
      Slide 1
      {/* [!code ++] */}
      Slide 2
      {/* [!code ++] */}
      Slide 3
      {/* [!code ++] */}
      {/* [!code ++] */} ); } ``` ```svelte [Svelte]
      Slide 1
      Slide 2
      Slide 3
      Slide 1
      Slide 2
      Slide 3
      ``` ```html [Core]
      Slide 1
      Slide 2
      Slide 3
      ``` :: ## Options Embla options describe how the carousel engine should calculate slide positions. In Blossom, those same decisions are usually regular CSS on the scroll container and slides. | Embla concept | Blossom approach | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `axis: "x"` | Automatically handled by Blossom based on native overscroll. | | `align: "start"` | Use CSS `scroll-snap-align: start` on slides. | | `align: "center"` | Use CSS `scroll-snap-align: center` on slides. | | `containScroll` | Use native scroll bounds, padding, and `scroll-padding`. | | `slidesToScroll` | Use CSS `scroll-snap-align` within `:nth-child(xn)` or nest slides in groups. | | `dragFree` | Omit `scroll-snap-align` or use `scroll-snap-type: proximity`. | | `direction: "rtl"` | Use `dir="rtl"` or CSS `direction: rtl`. | | `breakpoints` | Use CSS media queries and container queries. | | `loop` | Prefer bounded native scrolling, or use the experimental repeat pattern when an infinite effect is required. See [Repeat example](https://www.blossom-carousel.com/docs/examples/experimental/repeat). | | `draggable` | Use CSS `overflow: hidden` on the carousel to disable dragging and scrolling. | For example, this Embla setup: ```js [Embla] useEmblaCarousel({ axis: "x", align: "center", containScroll: "trimSnaps", dragFree: false, breakpoints: { "(min-width: 768px)": { align: "start" }, }, }); ``` becomes CSS: ```css [Blossom] .carousel { scroll-snap-type: inline mandatory; scroll-padding-inline: 1rem; } .slide { scroll-snap-align: center; } @media (min-width: 768px) { .slide { scroll-snap-align: start; } } ``` ## Controls Embla controls call methods on the Embla API. Blossom provides ``, ``, and `` components that link to the carousel through matching `id` and `for` props. ```ts emblaApi.scrollPrev(); // [!code --] emblaApi.scrollNext(); // [!code --] ``` ```html
      Slide 1
      ...
      ``` For direct slide navigation, use native scrolling on the target slide. ```ts slides[index].scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest", }); ``` ## API Embla commonly uses `select` events and `selectedScrollSnap()` to track the active slide. With Blossom, the carousel is a real scroll container, so use browser APIs such as `IntersectionObserver`, `scrollsnapchange`, or `scroll` events depending on the browser support you need. ```ts emblaApi.on("select", () => { // [!code --] setSelectedIndex(emblaApi.selectedScrollSnap()); // [!code --] }); // [!code --] carousel.addEventListener("scrollsnapchange", (event) => { // [!code ++] setSelectedIndex(event.detail.index); // [!code ++] }); // [!code ++] Or use IntersectionObserver for wider browser support const slides = Array.from(carousel.children); // [!code ++] const observer = new IntersectionObserver( // [!code ++] (entries) => { // [!code ++] const activeEntry = entries.find((entry) => entry.isIntersecting); // [!code ++] if (activeEntry) { // [!code ++] const index = slides.indexOf(activeEntry.target); // [!code ++] setSelectedIndex(index); // [!code ++] } // [!code ++] }, // [!code ++] { root: carousel, threshold: 0.5 }, // [!code ++] ); // [!code ++] slides.forEach((slide) => observer.observe(slide)); // [!code ++] ``` ## Plugins Embla plugins usually become either small Blossom wrappers or native platform features. | Embla plugin pattern | Blossom replacement | | -------------------- | ------------------------------------------------------------------------ | | Autoplay | Use manual timer that scrolls the next slide into view. | | Fade | Use CSS scroll-driven animations or intersection state. | | Auto height | Let content define height, or update layout with `ResizeObserver`. | | Class names | Use `IntersectionObserver`, `scrollsnapchange`, or scroll-state queries. | | Wheel gestures | Use the browser's native scroll behavior where possible. | Keep plugin migrations small. Start from a working Blossom carousel, then add the behavior back only if the product still needs it. ## Lifecycle Embla's and Blossom's lifecycle methods are similar. ::note{color="neutral" icon="i-lucide-lightbulb"} Blossom initializes automatically when using framework components. :: ```ts const [emblaRef, emblaApi] = useEmblaCarousel(); // [!code --] emblaApi.reInit(); // [!code --] emblaApi.destroy(); // [!code --] const blossom = Blossom(carouselElement); // [!code ++] blossom.init(); // [!code ++] blossom.destroy(); // [!code ++] ``` ## Migration checklist - Remove Embla packages and install the matching Blossom package. - Import the stylesheet from your framework package once (for example `@blossom-carousel/vue/style.css`). - Replace Embla viewport and container markup with `` and direct slide children. - Move slide sizing, alignment, gaps, snapping, direction, and breakpoints into CSS. - Replace `scrollPrev()` and `scrollNext()` with `` and ``. - Replace `scrollTo()` with `scrollIntoView()` or native `scrollTo()` on the carousel element. - Replace `select` listeners with browser observers or scroll snap events. - Rebuild only the Embla plugins your carousel still needs. ## Next steps - [Installation](https://www.blossom-carousel.com/docs/getting-started/installation) - Pick the package for your framework. - [Snapping](https://www.blossom-carousel.com/docs/examples/basic/snap) - Configure scroll snap behavior with CSS. - [Buttons](https://www.blossom-carousel.com/docs/examples/basic/buttons) - Add previous and next controls. - [Dots](https://www.blossom-carousel.com/docs/examples/basic/dots) - Add dot markers with ``. - [Repeat](https://www.blossom-carousel.com/docs/examples/experimental/repeat) - Explore an experimental repeating carousel pattern. # Swiper Swiper provides a large carousel system with framework components, modules, configuration options, lifecycle events, and built-in effects. Blossom keeps the carousel as a native scroll container. Layout, snapping, spacing, breakpoints, and many effects move back into CSS, while Blossom adds dragging and small convenience methods. ## Migration mindset When moving from Swiper to Blossom, migrate the carousel in three steps: 1. Replace `` and `` with a single `` root and direct children. 2. Move Swiper options such as `slidesPerView`, `spaceBetween`, `direction`, and `breakpoints` into CSS. 3. Replace Swiper modules and events with Blossom methods, native scroll APIs, and browser observers. ::note{color="neutral" icon="i-lucide-lightbulb"} Most Swiper migrations become smaller when you start with the markup and CSS first, then add back only the behavior the product still needs. :: ## Installation Remove Swiper and install the matching Blossom package for your framework. ::code-group{sync="migration-code-style"} ```bash [Vue] npm uninstall swiper npm install @blossom-carousel/vue ``` ```bash [React] npm uninstall swiper npm install @blossom-carousel/react ``` ```bash [Svelte] npm uninstall swiper npm install @blossom-carousel/svelte ``` ```bash [Core] npm uninstall swiper npm install @blossom-carousel/core ``` :: Import the stylesheet from your framework package once in your app. ::code-group ```js [Vue] import "@blossom-carousel/vue/style.css"; ``` ```js [React] import "@blossom-carousel/react/style.css"; ``` ```js [Svelte] import "@blossom-carousel/svelte/style.css"; ``` ```js [Core] import "@blossom-carousel/core/style.css"; ``` ```js [Web] import "@blossom-carousel/web/style.css"; ``` :: ## Component structure Swiper wraps slides in framework-specific components. Blossom uses the carousel root as the scroll container and treats direct children as slides. ::code-group{sync="migration-code-style"} ```vue [Vue] ``` ```tsx [React] import { Swiper, SwiperSlide } from "swiper/react"; // [!code --] import "swiper/css"; // [!code --] import { BlossomCarousel } from "@blossom-carousel/react"; // [!code ++] import "@blossom-carousel/react/style.css"; // [!code ++] export function Carousel() { return ( {/* [!code --] */} Slide 1 {/* [!code --] */} Slide 2 {/* [!code --] */} Slide 3 {/* [!code --] */} {/* [!code --] */} {/* [!code ++] */}
      Slide 1
      {/* [!code ++] */}
      Slide 2
      {/* [!code ++] */}
      Slide 3
      {/* [!code ++] */}
      {/* [!code ++] */} ); } ``` ```svelte [Svelte] Slide 1 Slide 2 Slide 3
      Slide 1
      Slide 2
      Slide 3
      ``` ```html [Core]
      Slide 1
      Slide 2
      Slide 3
      ``` :: ## Options Swiper options describe how its engine should size slides, attach modules, and calculate movement. In Blossom, most of those choices become CSS or small browser APIs. | Swiper concept | Blossom approach | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `slidesPerView` | Use CSS `grid-auto-columns` to define the relative width of slides. | | `spaceBetween` | Use CSS `gap` in a `grid` or `flex` layout. | | `direction: "horizontal"` | Automatically handled by Blossom based on native overscroll. | | `centeredSlides` | Use CSS `scroll-snap-align: center` on slides. | | `breakpoints` | Use CSS media queries and container queries. | | `loop` | Prefer bounded native scrolling, or use the experimental repeat pattern when an infinite effect is required. See [Repeat example](https://www.blossom-carousel.com/docs/examples/experimental/repeat). | | `freeMode` | Omit `scroll-snap-align` or use `scroll-snap-type: proximity`. | | `allowTouchMove` | Use CSS `overflow: hidden` on the carousel to disable dragging and scrolling. | | `effect` modules | Use CSS transforms, scroll-driven animations, or intersection state. | For example, this Swiper setup: ```js [Swiper] const swiperOptions = { slidesPerView: 1, spaceBetween: 16, centeredSlides: false, breakpoints: { 768: { slidesPerView: 3, centeredSlides: true }, }, }; ``` becomes CSS: ```css [Blossom] .carousel { display: grid; grid-auto-flow: column; grid-auto-columns: 100%; gap: 1rem; scroll-snap-type: inline mandatory; } .slide { scroll-snap-align: start; } @media (min-width: 768px) { .carousel { grid-auto-columns: calc((100% - 2rem) / 3); } .slide { scroll-snap-align: center; } } ``` ## Controls Swiper controls call methods on a Swiper instance. Blossom provides ``, ``, and `` components that link to the carousel through matching `id` and `for` props. ```ts swiper.slidePrev(); // [!code --] swiper.slideNext(); // [!code --] ``` ```html
      Slide 1
      ...
      ``` For direct slide navigation, use native scrolling on the target slide. ```ts slides[index].scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest", }); ``` ## API Swiper commonly uses events such as `slideChange` and properties such as `activeIndex`. With Blossom, the carousel is a real scroll container, so use browser APIs such as `IntersectionObserver`, `scrollsnapchange`, or `scroll` events depending on the browser support you need. ```ts swiper.on("slideChange", () => { // [!code --] setSelectedIndex(swiper.activeIndex); // [!code --] }); // [!code --] carousel.addEventListener("scrollsnapchange", (event) => { // [!code ++] setSelectedIndex(event.detail.index); // [!code ++] }); // [!code ++] Or use IntersectionObserver for wider browser support const slides = Array.from(carousel.children); // [!code ++] const observer = new IntersectionObserver( // [!code ++] (entries) => { // [!code ++] const activeEntry = entries.find((entry) => entry.isIntersecting); // [!code ++] if (activeEntry) { // [!code ++] setSelectedIndex(slides.indexOf(activeEntry.target)); // [!code ++] } // [!code ++] }, // [!code ++] { root: carousel, threshold: 0.5 }, // [!code ++] ); // [!code ++] slides.forEach((slide) => observer.observe(slide)); // [!code ++] ``` ## Modules Swiper modules usually become either native browser features or small code that calls Blossom methods. | Swiper module pattern | Blossom replacement | | ------------------------------- | ----------------------------------------------------- | | Navigation | `` and `` components. | | Pagination | `` with `data-blossom-slide` on slides. | | Autoplay | A manual timer that scrolls the next slide into view. | | Keyboard | Handled natively by the browser. | | Mousewheel | Native scrolling where possible. | | Grid | CSS grid. | | Effect Fade / Cards / Coverflow | CSS scroll-driven animations or intersection state. | Start with a working native-scroll carousel before rebuilding effects. Many Swiper effects are visual polish, not required carousel mechanics. ## Lifecycle Swipers's and Blossom's lifecycle methods are similar. ::note{color="neutral" icon="i-lucide-lightbulb"} Blossom initializes automatically when using framework components. :: ```ts swiper.update(); // [!code --] swiper.destroy(); // [!code --] blossom.init(); // [!code ++] blossom.destroy(); // [!code ++] ``` ## Migration checklist - Remove Swiper packages and install the matching Blossom package. - Import the stylesheet from your framework package once (for example `@blossom-carousel/vue/style.css`). - Replace `` and `` with `` and direct slide children. - Move slide sizing, gaps, snapping, direction, and breakpoints into CSS. - Replace `slidePrev()` and `slideNext()` with `` and ``. - Replace `slideTo()` with `scrollIntoView()` or native `scrollTo()`. - Replace Swiper events with browser observers or scroll snap events. - Rebuild only the Swiper modules your carousel still needs. ## Next steps - [Installation](https://www.blossom-carousel.com/docs/getting-started/installation) - Pick the package for your framework. - [Snapping](https://www.blossom-carousel.com/docs/examples/basic/snap) - Configure scroll snap behavior with CSS. - [Buttons](https://www.blossom-carousel.com/docs/examples/basic/buttons) - Add previous and next controls. - [Dots](https://www.blossom-carousel.com/docs/examples/basic/dots) - Add dot markers with ``. - [Repeat](https://www.blossom-carousel.com/docs/examples/experimental/repeat) - Explore an experimental repeating carousel pattern. # Splide Splide builds a carousel from a root element, track, list, slide elements, options, extensions, and a JavaScript instance. Blossom keeps the carousel as a native scroll container. Layout, slide sizing, gaps, snapping, and responsive behavior belong in CSS, while Blossom adds dragging and small convenience methods. ## Migration mindset When moving from Splide to Blossom, migrate the carousel in three steps: 1. Replace the Splide root, track, and list structure with a single `` root. 2. Move Splide options such as `perPage`, `gap`, `focus`, `direction`, and `breakpoints` into CSS. 3. Replace Splide instance calls and events with Blossom methods, native scroll APIs, and browser observers. ::note{color="neutral" icon="i-lucide-lightbulb"} Splide migrations usually remove wrapper markup. The Blossom carousel root is the element that scrolls. :: ## Installation Remove Splide and install the matching Blossom package for your framework. ::code-group{sync="migration-code-style"} ```bash [Vue] npm uninstall @splidejs/vue-splide @splidejs/splide npm install @blossom-carousel/vue ``` ```bash [React] npm uninstall @splidejs/react-splide @splidejs/splide npm install @blossom-carousel/react ``` ```bash [Svelte] npm uninstall @splidejs/svelte-splide @splidejs/splide npm install @blossom-carousel/svelte ``` ```bash [Core] npm uninstall @splidejs/splide npm install @blossom-carousel/core ``` :: Import the stylesheet from your framework package once in your app. ::code-group ```js [Vue] import "@blossom-carousel/vue/style.css"; ``` ```js [React] import "@blossom-carousel/react/style.css"; ``` ```js [Svelte] import "@blossom-carousel/svelte/style.css"; ``` ```js [Core] import "@blossom-carousel/core/style.css"; ``` ```js [Web] import "@blossom-carousel/web/style.css"; ``` :: ## Component structure Splide usually needs a root, track, list, and slide elements. Blossom uses one scroll container with direct slide children. ::code-group{sync="migration-code-style"} ```vue [Vue] ``` ```tsx [React] import { Splide, SplideSlide } from "@splidejs/react-splide"; // [!code --] import "@splidejs/react-splide/css"; // [!code --] import { BlossomCarousel } from "@blossom-carousel/react"; // [!code ++] import "@blossom-carousel/react/style.css"; // [!code ++] export function Carousel() { return ( {/* [!code --] */} Slide 1 {/* [!code --] */} Slide 2 {/* [!code --] */} Slide 3 {/* [!code --] */} {/* [!code --] */} {/* [!code ++] */}
      Slide 1
      {/* [!code ++] */}
      Slide 2
      {/* [!code ++] */}
      Slide 3
      {/* [!code ++] */}
      {/* [!code ++] */} ); } ``` ```svelte [Svelte] Slide 1 Slide 2 Slide 3
      Slide 1
      Slide 2
      Slide 3
      ``` ```html [Core]
      • Slide 1
      • Slide 2
      • Slide 3
      Slide 1
      Slide 2
      Slide 3
      ``` :: ## Options Splide options describe slide movement, sizing, and generated controls. In Blossom, those choices usually become CSS and small browser APIs. | Splide concept | Blossom approach | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `perPage` | Use CSS `grid-auto-columns` to define the relative width of slides. | | `perMove` | Use CSS `scroll-snap-align` within `:nth-child(xn)` or nest slides in groups. | | `gap` | Use CSS `gap` in a `grid` or `flex` layout. | | `focus: "center"` | Use `scroll-snap-align: center`. | | `direction` | Use `dir="rtl"` or CSS `direction: rtl`. | | `breakpoints` | Use CSS media queries and container queries. | | `type: "loop"` | Prefer bounded native scrolling, or use the experimental repeat pattern when an infinite effect is required. See [Repeat example](https://www.blossom-carousel.com/docs/examples/experimental/repeat). | | `drag: "free"` | Omit `scroll-snap-align` or use `scroll-snap-type: proximity`. | | `arrows` | Use `` and ``. See [Buttons example](https://www.blossom-carousel.com/docs/examples/basic/buttons). | | `pagination` | Use `` and mark slides with `data-blossom-slide`. See [Dots example](https://www.blossom-carousel.com/docs/examples/basic/dots). | For example, this Splide setup: ```js [Splide] new Splide(".splide", { perPage: 1, gap: "1rem", focus: "center", breakpoints: { 768: { perPage: 3 }, }, }).mount(); ``` becomes CSS: ```css [Blossom] .carousel { display: grid; grid-auto-flow: column; grid-auto-columns: 100%; gap: 1rem; scroll-snap-type: inline mandatory; } .slide { scroll-snap-align: center; } @media (min-width: 768px) { .carousel { grid-auto-columns: calc((100% - 2rem) / 3); } } ``` ## Controls Splide controls call methods on the Splide instance. Blossom provides ``, ``, and `` components that link to the carousel through matching `id` and `for` props. ```ts splide.go("<"); // [!code --] splide.go(">"); // [!code --] ``` ```html
      Slide 1
      ...
      ``` For direct slide navigation, use native scrolling on the target slide. ```ts slides[index].scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest", }); ``` ## API Splide commonly uses `moved` or `active` events to track the current slide. With Blossom, the carousel is a real scroll container, so use browser APIs such as `IntersectionObserver`, `scrollsnapchange`, or `scroll` events depending on the browser support you need. ```ts splide.on("moved", (newIndex) => { // [!code --] setSelectedIndex(newIndex); // [!code --] }); // [!code --] carousel.addEventListener("scrollsnapchange", (event) => { // [!code ++] setSelectedIndex(event.detail.index); // [!code ++] }); // [!code ++] Or use IntersectionObserver for wider browser support const slides = Array.from(carousel.children); // [!code ++] const observer = new IntersectionObserver( // [!code ++] (entries) => { // [!code ++] const activeEntry = entries.find((entry) => entry.isIntersecting); // [!code ++] if (activeEntry) { // [!code ++] setSelectedIndex(slides.indexOf(activeEntry.target)); // [!code ++] } // [!code ++] }, // [!code ++] { root: carousel, threshold: 0.5 }, // [!code ++] ); // [!code ++] slides.forEach((slide) => observer.observe(slide)); // [!code ++] ``` ## Extensions Splide extensions and built-in UI usually become small Blossom wrappers or native platform features. | Splide pattern | Blossom replacement | | -------------- | ------------------------------------------------------------------ | | Arrows | `` and `` components. | | Pagination | `` with `data-blossom-slide` on slides. | | Autoplay | A manual timer that scrolls the next slide into view. | | Auto width | Let slide content size itself with CSS. | | Auto height | Let content define height, or update layout with `ResizeObserver`. | | Grid / rows | CSS grid. | | Lazy load | Native `loading="lazy"` and `content-visibility: auto`. | Keep extension migrations small. Start from a working Blossom carousel, then add behavior only if the product still needs it. ## Lifecycle Splide's and Blossom's lifecycle methods are similar. ::note{color="neutral" icon="i-lucide-lightbulb"} Blossom initializes automatically when using framework components. :: ```ts splide.refresh(); // [!code --] splide.destroy(); // [!code --] blossom.init(); // [!code ++] blossom.destroy(); // [!code ++] ``` ## Migration checklist - Remove Splide packages and install the matching Blossom package. - Import the stylesheet from your framework package once (for example `@blossom-carousel/vue/style.css`). - Replace Splide root, track, list, and slide wrappers with `` and direct slide children. - Move slide sizing, gaps, snapping, direction, focus, and breakpoints into CSS. - Replace `go("<")` and `go(">")` with `` and ``. - Replace `go(index)` with `scrollIntoView()` or native `scrollTo()`. - Replace Splide events with browser observers or scroll snap events. - Rebuild only the Splide extensions your carousel still needs. ## Next steps - [Installation](https://www.blossom-carousel.com/docs/getting-started/installation) - Pick the package for your framework. - [Snapping](https://www.blossom-carousel.com/docs/examples/basic/snap) - Configure scroll snap behavior with CSS. - [Buttons](https://www.blossom-carousel.com/docs/examples/basic/buttons) - Add previous and next controls. - [Dots](https://www.blossom-carousel.com/docs/examples/basic/dots) - Add dot markers with ``. - [Repeat](https://www.blossom-carousel.com/docs/examples/experimental/repeat) - Explore an experimental repeating carousel pattern. # Slick Slick Carousel creates a carousel around jQuery-managed markup, cloned slides, option objects, generated controls, and event callbacks. React Slick wraps the same model in React components. Blossom keeps the carousel as a native scroll container. Layout, slide sizing, gaps, breakpoints, and snapping belong in CSS, while Blossom adds dragging and small convenience methods. ## Migration mindset When moving from Slick Carousel to Blossom, migrate the carousel in three steps: 1. Replace Slick's generated track/list structure with a single `` root and direct children. 2. Move Slick options such as `slidesToShow`, `slidesToScroll`, `centerMode`, and `responsive` into CSS. 3. Replace Slick method calls and events with Blossom methods, native scroll APIs, and browser observers. ::note{color="neutral" icon="i-lucide-lightbulb"} Slick often adds hidden markup and cloned slides. With Blossom, your source markup is the carousel markup. :: ## Installation Remove Slick packages and install the matching Blossom package for your framework. ::code-group{sync="migration-code-style"} ```bash [Vue] npm uninstall vue-slick-carousel slick-carousel npm install @blossom-carousel/vue ``` ```bash [React] npm uninstall react-slick slick-carousel npm install @blossom-carousel/react ``` ```bash [Svelte] npm uninstall slick-carousel jquery npm install @blossom-carousel/svelte ``` ```bash [Core] npm uninstall slick-carousel jquery npm install @blossom-carousel/core ``` :: Import the stylesheet from your framework package once in your app. ::code-group ```js [Vue] import "@blossom-carousel/vue/style.css"; ``` ```js [React] import "@blossom-carousel/react/style.css"; ``` ```js [Svelte] import "@blossom-carousel/svelte/style.css"; ``` ```js [Core] import "@blossom-carousel/core/style.css"; ``` ```js [Web] import "@blossom-carousel/web/style.css"; ``` :: ## Component structure Slick and React Slick wrap your slides in a generated list and track. Blossom uses one scroll container with direct slide children. ::code-group{sync="migration-code-style"} ```vue [Vue] ``` ```tsx [React] import Slider from "react-slick"; // [!code --] import "slick-carousel/slick/slick.css"; // [!code --] import { BlossomCarousel } from "@blossom-carousel/react"; // [!code ++] import "@blossom-carousel/react/style.css"; // [!code ++] export function Carousel() { return ( {/* [!code --] */}
      Slide 1
      {/* [!code --] */}
      Slide 2
      {/* [!code --] */}
      Slide 3
      {/* [!code --] */}
      {/* [!code --] */} {/* [!code ++] */}
      Slide 1
      {/* [!code ++] */}
      Slide 2
      {/* [!code ++] */}
      Slide 3
      {/* [!code ++] */}
      {/* [!code ++] */} ); } ``` ```svelte [Svelte]
      Slide 1
      Slide 2
      Slide 3
      ``` ```html [jQuery]
      Slide 1
      Slide 2
      Slide 3
      ``` :: ## Options Slick options describe how its track should move, clone slides, and generate UI. In Blossom, most of those choices become CSS or small browser APIs. | Slick concept | Blossom approach | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `slidesToShow` | Use CSS `grid-auto-columns` to define the relative width of slides. | | `slidesToScroll` | Use CSS `scroll-snap-align` within `:nth-child(xn)` or nest slides in groups. | | `centerMode` | Use `scroll-snap-align: center`. | | `variableWidth` | Let slides size themselves with CSS. | | `adaptiveHeight` | Let content define height, or update layout with `ResizeObserver`. | | `responsive` | Use CSS media queries and container queries. | | `infinite` | Prefer bounded native scrolling, or use the experimental repeat pattern when an infinite effect is required. See [Repeat example](https://www.blossom-carousel.com/docs/examples/experimental/repeat). | | `draggable` / `swipe` | Use CSS `overflow: hidden` on the carousel to disable dragging and scrolling. | | `arrows` | Use `` and ``. See [Buttons example](https://www.blossom-carousel.com/docs/examples/basic/buttons). | | `dots` | Use `` and mark slides with `data-blossom-slide`. See [Dots example](https://www.blossom-carousel.com/docs/examples/basic/dots). | For example, this Slick setup: ```js [Slick] $(".carousel").slick({ slidesToShow: 1, slidesToScroll: 1, centerMode: true, responsive: [{ breakpoint: 768, settings: { slidesToShow: 3 } }], }); ``` becomes CSS: ```css [Blossom] .carousel { display: grid; grid-auto-flow: column; grid-auto-columns: 100%; gap: 1rem; scroll-snap-type: inline mandatory; } .slide { scroll-snap-align: center; } @media (min-width: 768px) { .carousel { grid-auto-columns: calc((100% - 2rem) / 3); } } ``` ## Controls Slick controls call methods on the Slick instance. Blossom provides ``, ``, and `` components that link to the carousel through matching `id` and `for` props. ```ts $(".carousel").slick("slickPrev"); // [!code --] $(".carousel").slick("slickNext"); // [!code --] ``` ```html
      Slide 1
      ...
      ``` For direct slide navigation, use native scrolling on the target slide. ```ts slides[index].scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest", }); ``` ## API Slick commonly uses `afterChange` to track the current slide. With Blossom, the carousel is a real scroll container, so use browser APIs such as `IntersectionObserver`, `scrollsnapchange`, or `scroll` events depending on the browser support you need. ```ts $(".carousel").on("afterChange", (_event, _slick, currentSlide) => { // [!code --] setSelectedIndex(currentSlide); // [!code --] }); // [!code --] carousel.addEventListener("scrollsnapchange", (event) => { // [!code ++] setSelectedIndex(event.detail.index); // [!code ++] }); // [!code ++] Or use IntersectionObserver for wider browser support const slides = Array.from(carousel.children); // [!code ++] const observer = new IntersectionObserver( // [!code ++] (entries) => { // [!code ++] const activeEntry = entries.find((entry) => entry.isIntersecting); // [!code ++] if (activeEntry) { // [!code ++] setSelectedIndex(slides.indexOf(activeEntry.target)); // [!code ++] } // [!code ++] }, // [!code ++] { root: carousel, threshold: 0.5 }, // [!code ++] ); // [!code ++] slides.forEach((slide) => observer.observe(slide)); // [!code ++] ``` ## Features Slick features usually become either small Blossom wrappers or native platform features. | Slick feature | Blossom replacement | | --------------- | ------------------------------------------------------ | | Arrows | `` and `` components. | | Dots | `` with `data-blossom-slide` on slides. | | Autoplay | A manual timer that scrolls the next slide into view. | | Fade | CSS scroll-driven animations or intersection state. | | Lazy load | Native `loading="lazy"` or framework image components. | | Rows | CSS grid. | | Syncing sliders | Shared selected state plus `scrollIntoView()`. | Rebuild Slick features deliberately. Native scrolling often replaces more of Slick than expected. ## Lifecycle Slicks's and Blossom's lifecycle methods are similar. ::note{color="neutral" icon="i-lucide-lightbulb"} Blossom initializes automatically when using framework components. :: ```ts $(".carousel").slick("refresh"); // [!code --] $(".carousel").slick("unslick"); // [!code --] blossom.init(); // [!code ++] blossom.destroy(); // [!code ++] ``` ## Migration checklist - Remove Slick packages and install the matching Blossom package. - Import the stylesheet from your framework package once (for example `@blossom-carousel/vue/style.css`). - Replace Slick markup and generated track assumptions with `` and direct slide children. - Move slide sizing, gaps, snapping, center mode, and responsive behavior into CSS. - Replace `slickPrev` and `slickNext` with `` and ``. - Replace `slickGoTo` with `scrollIntoView()` or native `scrollTo()`. - Replace Slick events with browser observers or scroll snap events. - Rebuild only the Slick features your carousel still needs. ## Next steps - [Installation](https://www.blossom-carousel.com/docs/getting-started/installation) - Pick the package for your framework. - [Snapping](https://www.blossom-carousel.com/docs/examples/basic/snap) - Configure scroll snap behavior with CSS. - [Buttons](https://www.blossom-carousel.com/docs/examples/basic/buttons) - Add previous and next controls. - [Dots](https://www.blossom-carousel.com/docs/examples/basic/dots) - Add dot markers with ``. - [Repeat](https://www.blossom-carousel.com/docs/examples/experimental/repeat) - Explore an experimental repeating carousel pattern. # Flickity Flickity creates a carousel from cell elements, option objects, generated controls, selected-cell state, and a JavaScript instance. Blossom keeps the carousel as a native scroll container. Layout, slide sizing, cell alignment, spacing, and responsive behavior belong in CSS, while Blossom adds dragging and small convenience methods. ## Migration mindset When moving from Flickity to Blossom, migrate the carousel in three steps: 1. Replace Flickity initialization with a single `` root and direct children. 2. Move Flickity options such as `cellAlign`, `contain`, `wrapAround`, and `groupCells` into CSS or native scrolling. 3. Replace Flickity methods and events with Blossom methods, native scroll APIs, and browser observers. ::note{color="neutral" icon="i-lucide-lightbulb"} Flickity migrations work best when you treat each cell as a normal scroll-snap child instead of a measured item in a carousel engine. :: ## Installation Remove Flickity and install the matching Blossom package for your framework. ::code-group{sync="migration-code-style"} ```bash [Vue] npm uninstall vue-flickity flickity npm install @blossom-carousel/vue ``` ```bash [React] npm uninstall react-flickity-component flickity npm install @blossom-carousel/react ``` ```bash [Svelte] npm uninstall flickity npm install @blossom-carousel/svelte ``` ```bash [Core] npm uninstall flickity npm install @blossom-carousel/core ``` :: Import the stylesheet from your framework package once in your app. ::code-group ```js [Vue] import "@blossom-carousel/vue/style.css"; ``` ```js [React] import "@blossom-carousel/react/style.css"; ``` ```js [Svelte] import "@blossom-carousel/svelte/style.css"; ``` ```js [Core] import "@blossom-carousel/core/style.css"; ``` ```js [Web] import "@blossom-carousel/web/style.css"; ``` :: ## Component structure Flickity initializes behavior on an element and treats its children as cells. Blossom keeps that direct-child model, but the root itself is the native scroll container. ::code-group{sync="migration-code-style"} ```vue [Vue] ``` ```tsx [React] import Flickity from "react-flickity-component"; // [!code --] import "flickity/css/flickity.css"; // [!code --] import { BlossomCarousel } from "@blossom-carousel/react"; // [!code ++] import "@blossom-carousel/react/style.css"; // [!code ++] export function Carousel() { return ( {/* [!code --] */}
      Slide 1
      {/* [!code --] */}
      Slide 2
      {/* [!code --] */}
      Slide 3
      {/* [!code --] */}
      {/* [!code --] */} {/* [!code ++] */}
      Slide 1
      {/* [!code ++] */}
      Slide 2
      {/* [!code ++] */}
      Slide 3
      {/* [!code ++] */}
      {/* [!code ++] */} ); } ``` ```svelte [Svelte]
      Slide 1
      Slide 2
      Slide 3
      ``` ```html [Core]
      Slide 1
      Slide 2
      Slide 3
      ``` :: ## Options Flickity options describe how its engine measures and aligns cells. In Blossom, most of those choices become CSS and native scroll behavior. | Flickity concept | Blossom approach | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `cellAlign: "left"` | Use `scroll-snap-align: start`. | | `cellAlign: "center"` | Use `scroll-snap-align: center`. | | `contain` | Use native scroll bounds, padding, and `scroll-padding`. | | `groupCells` | Create grouped slide elements or use native scrolling to target every nth slide. | | `wrapAround` | Prefer bounded native scrolling, or use the experimental repeat pattern when an infinite effect is required. See [Repeat example](https://www.blossom-carousel.com/docs/examples/experimental/repeat). | | `freeScroll` | Omit `scroll-snap-align` or use `scroll-snap-type: proximity`. | | `rightToLeft` | Use `dir="rtl"` or CSS `direction: rtl`. | | `adaptiveHeight` | Let content define height, or update layout with `ResizeObserver`. | | `prevNextButtons` | Use `` and ``. See [Buttons example](https://www.blossom-carousel.com/docs/examples/basic/buttons). | | `pageDots` | Use `` and mark slides with `data-blossom-slide`. See [Dots example](https://www.blossom-carousel.com/docs/examples/basic/dots). | For example, this Flickity setup: ```js [Flickity] const flkty = new Flickity(".carousel", { cellAlign: "center", contain: true, groupCells: true, wrapAround: false, }); ``` becomes CSS: ```css [Blossom] .carousel { display: grid; grid-auto-flow: column; grid-auto-columns: 25%; gap: 1rem; scroll-snap-type: inline mandatory; scroll-padding-inline: 10%; } .slide { scroll-snap-align: center; } ``` ## Controls Flickity controls call methods on the Flickity instance. Blossom provides ``, ``, and `` components that link to the carousel through matching `id` and `for` props. ```ts flkty.previous(); // [!code --] flkty.next(); // [!code --] ``` ```html
      Slide 1
      ...
      ``` For direct slide navigation, use native scrolling on the target slide. ```ts slides[index].scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest", }); ``` ## API Flickity commonly uses the `select` event and `selectedIndex` to track the active cell. With Blossom, the carousel is a real scroll container, so use browser APIs such as `IntersectionObserver`, `scrollsnapchange`, or `scroll` events depending on the browser support you need. ```ts flkty.on("select", () => { // [!code --] setSelectedIndex(flkty.selectedIndex); // [!code --] }); // [!code --] carousel.addEventListener("scrollsnapchange", (event) => { // [!code ++] setSelectedIndex(event.detail.index); // [!code ++] }); // [!code ++] Or use IntersectionObserver for wider browser support const slides = Array.from(carousel.children); // [!code ++] const observer = new IntersectionObserver( // [!code ++] (entries) => { // [!code ++] const activeEntry = entries.find((entry) => entry.isIntersecting); // [!code ++] if (activeEntry) { // [!code ++] setSelectedIndex(slides.indexOf(activeEntry.target)); // [!code ++] } // [!code ++] }, // [!code ++] { root: carousel, threshold: 0.5 }, // [!code ++] ); // [!code ++] slides.forEach((slide) => observer.observe(slide)); // [!code ++] ``` ## Features Flickity features usually become either small Blossom wrappers or native platform features. | Flickity feature | Blossom replacement | | ----------------------- | -------------------------------------------------------------------------------- | | Previous / next buttons | `` and `` components. | | Page dots | `` with `data-blossom-slide` on slides. | | Auto play | A manual timer that scrolls the next slide into view. | | Lazy load | Native `loading="lazy"` and `content-visibility: auto`. | | As nav for | Shared selected state plus `scrollIntoView()`. | | Images loaded | Native image loading events or `ResizeObserver` if layout depends on image size. | | Fade | CSS scroll-driven animations or intersection state. | Keep feature migrations small. Start from a working Blossom carousel, then add behavior back only if the product still needs it. ## Lifecycle Flickity's and Blossom's lifecycle methods are similar. ::note{color="neutral" icon="i-lucide-lightbulb"} Blossom initializes automatically when using framework components. :: ```ts flkty.resize(); // [!code --] flkty.destroy(); // [!code --] blossom.init(); // [!code ++] blossom.destroy(); // [!code ++] ``` ## Migration checklist - Remove Flickity packages and install the matching Blossom package. - Import the stylesheet from your framework package once (for example `@blossom-carousel/vue/style.css`). - Replace Flickity initialization with `` and direct slide children. - Move cell sizing, alignment, gaps, snapping, direction, and breakpoints into CSS. - Replace `previous()` and `next()` with `` and ``. - Replace `select(index)` with `scrollIntoView()` or native `scrollTo()`. - Replace Flickity events with browser observers or scroll snap events. - Rebuild only the Flickity features your carousel still needs. ## Next steps - [Installation](https://www.blossom-carousel.com/docs/getting-started/installation) - Pick the package for your framework. - [Snapping](https://www.blossom-carousel.com/docs/examples/basic/snap) - Configure scroll snap behavior with CSS. - [Buttons](https://www.blossom-carousel.com/docs/examples/basic/buttons) - Add previous and next controls. - [Dots](https://www.blossom-carousel.com/docs/examples/basic/dots) - Add dot markers with ``. - [Repeat](https://www.blossom-carousel.com/docs/examples/experimental/repeat) - Explore an experimental repeating carousel pattern. # Navigation controls with the Invoker Commands API Blossom Carousel now ships first-class navigation controls through ``, ``, and `` — with `` for fully custom dot buttons. These components work across the Vue, React, Svelte, and Web Components packages, and they can live anywhere in your layout. :blog-navigation-controls Link them to a carousel with a simple `id` / `for` pairing and Blossom handles the rest. No template ref, no layout constraints and no imperative API. ```html
      Slide 01
      Slide 02
      Slide 03
      ...
      ``` Mark each slide with `data-blossom-slide` so dot navigation knows which elements to track. You choose how you want to build your slides markup—flat, nested or grouped—and you decide which elements you want to be tracked. ## How they work Under the hood, each control is a native `