Native Scroll Carousel Web Component with Drag – Blossom Carousel

Category: Javascript , Recommended , Slider | August 6, 2026
Authorjespervo
Last UpdateAugust 6, 2026
LicenseMIT
Views37 views
Native Scroll Carousel Web Component with Drag – Blossom Carousel

Blossom Carousel is a dependency-free JavaScript carousel library that adds mouse-drag support to a horizontally scrolling container and keeps the browser’s own scroll behavior in place.

Native scroll containers already handle touch swiping, keyboard scrolling, and screen reader compatibility well.

Blossom Carousel builds on that and adds a physics-based drag functionality for pointer devices, the one interaction native scrolling does not provide on desktop.

Features:

  • Native horizontal scrolling preserves normal browser scroll behavior and DOM state.
  • Physics-based dragging adds mouse and fine-pointer interaction.
  • CSS controls slide width, gaps, breakpoints, scroll snap, sticky elements, and visual effects.
  • Previous and next controls follow configured snap positions.
  • Dot navigation tracks elements marked with data-blossom-slide.
  • Custom dot templates support numbered controls, icons, and thumbnails.
  • Web Component, React, Vue, Svelte, and framework-agnostic Core packages.
  • Right-to-left layouts follow the carousel’s CSS direction.
  • A cancelable overscroll event supports custom edge effects.
  • Core instances expose initialization, navigation, and cleanup methods.
  • Conditional Core imports can limit the drag enhancement to fine-pointer devices.
  • Experimental repeat mode creates a cyclical scrolling effect.

How To Use It:

Installation

Choose the package that matches your app architecture.

EnvironmentPackage
Framework-free Web Component@blossom-carousel/web
Direct JavaScript Core API@blossom-carousel/core
React and Next.js@blossom-carousel/react
Vue and Nuxt@blossom-carousel/vue
Svelte and SvelteKit@blossom-carousel/svelte

The Web Component package supports both NPM projects and static pages.

npm install @blossom-carousel/web

Import the component registration and its base stylesheet from the application entry point.

import "@blossom-carousel/web";
import "@blossom-carousel/web/style.css";

Static pages can load the UMD build from a CDN.

<link
  rel="stylesheet"
  href="https://unpkg.com/@blossom-carousel/web/dist/blossom-carousel-web.css"
/>
<script
  defer
  src="https://unpkg.com/@blossom-carousel/web/dist/blossom-carousel-web.umd.js"
></script>

Basic Usage

The carousel needs a horizontal layout and overflowing content. Add data-blossom-slide to each tracked slide when the interface uses dot navigation.

The example below creates a responsive product rail with scroll snapping, previous and next controls, and automatic dot markers.

<section
  class="featured-products"
  aria-labelledby="featured-products-title"
>
  <h2 id="featured-products-title">Featured products</h2>
  <blossom-carousel
    id="featured-products"
    class="product-carousel"
  >
    <article class="product-card" data-blossom-slide>
      <img src="product-1.jpg" alt="Canvas travel bag" />
      <h3>Canvas Travel Bag</h3>
      <a href="/products/canvas-travel-bag">View product</a>
    </article>
    <article class="product-card" data-blossom-slide>
      <img src="product-2.jpg" alt="Ceramic desk lamp" />
      <h3>Ceramic Desk Lamp</h3>
      <a href="/products/ceramic-desk-lamp">View product</a>
    </article>
    <article class="product-card" data-blossom-slide>
      <img src="product-3.jpg" alt="Oak storage tray" />
      <h3>Oak Storage Tray</h3>
      <a href="/products/oak-storage-tray">View product</a>
    </article>
  </blossom-carousel>
  <div class="carousel-controls">
    <blossom-prev
      for="featured-products"
      aria-label="Previous products"
    >
      Previous
    </blossom-prev>
    <blossom-dots for="featured-products"></blossom-dots>
    <blossom-next
      for="featured-products"
      aria-label="Next products"
    >
      Next
    </blossom-next>
  </div>
</section>
.product-carousel {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(16rem, 75%);
  gap: 1rem;
  overflow-x: auto;
  padding: 0.5rem 1rem 1rem;
  scroll-padding-inline: 1rem;
  scroll-snap-type: inline mandatory;
}
.product-card {
  scroll-snap-align: start;
  border: 1px solid #d7d7d7;
  border-radius: 0.75rem;
  padding: 1rem;
}
.product-card img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 0.5rem;
}
.carousel-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  margin-top: 1rem;
}
@media (min-width: 48rem) {
  .product-carousel {
    grid-auto-columns: calc((100% - 2rem) / 3);
  }
}

Navigation Controls

<blossom-prev>, <blossom-next>, and <blossom-dots> can be placed outside the scrolling element. Their for attribute must match the carousel’s id.

Previous and next controls search for the next valid snap point. A carousel without scroll snap moves by a proportional distance. The controls also account for scroll padding and right-to-left direction.

Dot navigation counts elements marked with data-blossom-slide. The current dot receives aria-current, while previous and next buttons become disabled at the start and end of a bounded carousel.

<blossom-carousel id="article-list" class="carousel">
  <article data-blossom-slide>Article 1</article>
  <article data-blossom-slide>Article 2</article>
  <article data-blossom-slide>Article 3</article>
</blossom-carousel>
<blossom-prev for="article-list">Previous articles</blossom-prev>
<blossom-dots for="article-list"></blossom-dots>
<blossom-next for="article-list">Next articles</blossom-next>

Styling The Dot Controls

Default dots use CSS custom properties placed on <blossom-dots> or an ancestor.

  • --blossom-dot-size: Width and height of each marker.
  • --blossom-dot-radius: Corner radius of each marker.
  • --blossom-dot-color: Marker color.
  • --blossom-dot-opacity: Default marker opacity.
  • --blossom-dot-hover-opacity: Marker opacity on hover.
  • --blossom-dot-active-opacity: Opacity of the current marker.
.carousel-controls {
  --blossom-dot-size: 0.75rem;
  --blossom-dot-radius: 0.2rem;
  --blossom-dot-color: #222;
  --blossom-dot-opacity: 0.25;
  --blossom-dot-hover-opacity: 0.65;
  --blossom-dot-active-opacity: 1;
}

Custom Thumbnail Dots

Set the renderDot property when each navigation marker needs different content. The callback receives the slide index, current state, and target carousel ID.

Return a button for each slide. Blossom adds the navigation command and target relationship.

<blossom-dots
  id="product-thumbnails"
  for="featured-products"
></blossom-dots>
<script type="module">
  const dots = document.getElementById("product-thumbnails");
  dots.renderDot = (index, active) => {
    const button = document.createElement("button");
    button.type = "button";
    button.className = "thumbnail-dot";
    button.dataset.active = String(active);
    button.setAttribute("data-blossom-dot", "");
    button.setAttribute(
      "aria-label",
      `Show product ${index + 1}`
    );
    button.innerHTML = `
      <img
        src="/images/product-thumb-${index + 1}.jpg"
        alt=""
      />
    `;
    return button;
  };
</script>

Direct Core API

Use @blossom-carousel/core when a custom element or framework component does not fit your project. Pass the scrolling element to Blossom(), then call init().

The returned object provides these public methods:

  • init(): Adds drag behavior, observers, scroll handling, and snap detection.
  • destroy(): Removes listeners and observers associated with the instance.
  • prev(options): Scrolls to the previous slide or snap position.
  • next(options): Scrolls to the next slide or snap position.

The optional navigation alignment accepts start, center, or end.

npm install @blossom-carousel/core
import { Blossom } from "@blossom-carousel/core";
import "@blossom-carousel/core/style.css";
const track = document.querySelector("#core-carousel");
const previousButton = document.querySelector("[data-carousel-prev]");
const nextButton = document.querySelector("[data-carousel-next]");
if (track instanceof HTMLElement) {
  const carousel = Blossom(track);
  carousel.init();
  previousButton?.addEventListener("click", () => {
    carousel.prev({ align: "center" });
  });
  nextButton?.addEventListener("click", () => {
    carousel.next({ align: "center" });
  });
  window.addEventListener(
    "pagehide",
    () => {
      carousel.destroy();
    },
    { once: true }
  );
}

Lazy-Loading The Core Package

Touch devices already provide direct swipe scrolling. A conditional import can load the drag engine only when the browser reports a fine pointer and hover support.

const hasFinePointer = window.matchMedia(
  "(hover: hover) and (pointer: fine)"
).matches;
if (hasFinePointer) {
  const { Blossom } = await import("@blossom-carousel/core");
  const track = document.querySelector("#lazy-carousel");
  if (track instanceof HTMLElement) {
    const carousel = Blossom(track);
    carousel.init();
  }
}

Command Events

Navigation components dispatch command events to their target carousel.

The command value identifies the requested action:

  • --blossom-prev
  • --blossom-next
  • --blossom-goto-{index}

Dragging and free scrolling do not dispatch these commands. Use the event when application logic needs to record clicks on arrows or dots.

const carousel = document.getElementById("featured-products");
carousel.addEventListener("command", (event) => {
  const command =
    event.command ?? event.detail?.command;
  console.log("Carousel command:", command);
});

Overscroll Effects

The overscroll event fires when dragging passes a carousel edge. Calling preventDefault() removes the built-in rubber-band transform and leaves the visual response to application code.

The event’s detail.left value represents the horizontal overscroll distance.

const carousel = document.getElementById("featured-products");
carousel.addEventListener("overscroll", (event) => {
  event.preventDefault();
  const distance = Math.abs(event.detail.left);
  const scale = Math.max(0.94, 1 - distance * 0.001);
  Array.from(carousel.children).forEach((slide) => {
    slide.style.transform = `scale(${scale})`;
  });
});

Experimental Repeat Mode

The Web Component accepts a repeat attribute for cyclical scrolling. The Core package uses { repeat: true }.

Repeat remains experimental. Test slide widths, focus order, navigation controls, and dynamic content before using it in production.

<blossom-carousel repeat class="carousel">
  <div class="slide">Slide 1</div>
  <div class="slide">Slide 2</div>
  <div class="slide">Slide 3</div>
  <div class="slide">Slide 4</div>
</blossom-carousel>
const carousel = Blossom(track, {
  repeat: true
});
carousel.init();

Framework Integration

React

Import the stylesheet from a global entry point in Next.js applications.

import {
  BlossomCarousel,
  BlossomPrev,
  BlossomNext,
  BlossomDots
} from "@blossom-carousel/react";
export function ProductCarousel({ products }) {
  return (
    <>
      <BlossomCarousel id="products" as="ul">
        {products.map((product) => (
          <li key={product.id} data-blossom-slide>
            {product.name}
          </li>
        ))}
      </BlossomCarousel>
      <BlossomPrev for="products" />
      <BlossomDots for="products" />
      <BlossomNext for="products" />
    </>
  );
}

Vue

<script setup>
import {
  BlossomCarousel,
  BlossomPrev,
  BlossomNext,
  BlossomDots
} from "@blossom-carousel/vue";
import "@blossom-carousel/vue/style.css";
</script>
<template>
  <BlossomCarousel id="products" as="ul">
    <li
      v-for="product in products"
-key="product.id"
      data-blossom-slide
    >
      {{ product.name }}
    </li>
  </BlossomCarousel>
  <BlossomPrev for="products" />
  <BlossomDots for="products" />
  <BlossomNext for="products" />
</template>

Alternatives:

You Might Be Interested In:


Leave a Reply