scroll-carousel

Coming from Swiper

Five carousels as they appear on large shops, each next to the Swiper options it usually takes and what does the same here. Below them, every option and API call side by side.

Five storefront patterns

Drawn as wireframes and built with this package. Each table puts the options Swiper usually takes next to what does the same here.

Brand teasers

Three teasers per view with an image, a logo tile over its corner and a dark text panel; all the same height, dots on top of the panels. On phones one teaser and part of the next.

NeedTypical library setupHere
Three per view, 1.2 on phonesslidesPerView and breakpoints, applied by script after load--sc-per-view in a container query, final in the server markup
2 px between teasersspaceBetween: 2--sc-gap: 2px
Page by threeslidesPerGroup: 3--sc-group: 3
Same heightwrapper CSS on top of the librarydefault: slides stretch
Square dots over the panelspagination with custom bullets[data-sc-dots] placed by the host; --sc-dot-radius: 0

As a shadcn block: npx shadcn@latest add studio-nordwerk/oss-ui/brand-teasers

Product recommendations

Product tiles two pixels apart with a badge and a wish-list heart. Square arrows at the edges, centred on the images; no dots.

NeedTypical library setupHere
Arrows at the edgesnavigation with the shop's own arrow componentsany element with data-sc-prev / data-sc-next, or the defaults with --sc-control-radius: 0
Next shows the next full viewslidesPerGroup equal to slidesPerView--sc-group: page
No arrows when nothing scrollswatchOverflowautomatic: [data-sc-overflow]
Dragging with a mousesimulateTouch, on by defaultdrag() plugin, opt-in

As a shadcn block: npx shadcn@latest add studio-nordwerk/oss-ui/product-row

Stage with autoplay

Full-width banners that move on every 5 seconds. After the last banner the first one fades in again instead of an endless loop of cloned slides.

NeedTypical library setupHere
Endlessloop: true: cloned slides, a teleported position and clones hidden from assistive technologyrewind: true: the first banner fades in again
Every 5 secondsautoplay: { delay: 5000 }autoplay({ delay: 5000 }) plugin
A way to stop it (WCAG 2.2.2)not built in[data-sc-play] button; focus entering the stage stops it
Pause on hoverpauseOnMouseEnterbuilt in, also while off-screen or in a hidden tab

As a shadcn block: npx shadcn@latest add studio-nordwerk/oss-ui/hero-autoplay

Logo belt in free mode

Logos as wide as they are, scrolled freely without snapping. No controls on narrow rows.

NeedTypical library setupHere
Content widthsslidesPerView: 'auto'--sc-slide-size: auto
Free scrollingfreeMode: true--sc-snap: none
Free, but coming to rest on an itemfreeMode: { sticky: true }--sc-snap: mandatory: momentum still carries across items
No controls on narrow rowsnavigation switched off per breakpoint--sc-controls: none in a container query

As a shadcn block: npx shadcn@latest add studio-nordwerk/oss-ui/logo-belt

Every option and API call

Option and API names on the left are those of Swiper, the most widely used carousel library; other libraries name the same things similarly. The main change of mindset: layout moves from JavaScript options into CSS, so breakpoints become media or container queries instead of options that the script re-applies after load.

Options

SwiperHereNotes
slidesPerView: 3--sc-per-view: 3Fractions work the same way
slidesPerView: 'auto'--sc-slide-size: autoOr any length, e.g. 18rem
spaceBetween: 16--sc-gap: 16px
slidesOffsetBefore / After--sc-offset-before / --sc-offset-afterSnapped slides line up after the offset
breakpoints: { … }@media or @container sc (min-width: …)Container queries follow the carousel's own width
slidesPerGroup: 3--sc-group: 3Only group starts are snap points
slidesPerGroupAuto--sc-group: pageAs many whole slides as fit
centeredSlides: true--sc-align: center; --sc-centered: 1
centeredSlidesBounds--sc-align: center without --sc-centered
centerInsufficientSlidesclass sc--center-few
clamping slides per view to the slide count--sc-count: <n>
initialSlide: 4data-sc-initial on the slide, or initial: 4Plus the inline PRE_POSITION snippet for server rendering
freeMode: true--sc-snap: none
freeMode: { sticky: true }--sc-snap: mandatoryNative momentum still carries across several slides
loop: truerewind: trueFades back to the start instead of cloning slides
rewind: truerewind: 'scroll'Scrolls back across all slides
autoplay: { delay }autoplay({ delay }) pluginAdds a required pause button; focus stops it
simulateTouch (mouse drag)drag() pluginOpt-in
navigation[data-sc-prev], [data-sc-next] on any elementOr call next() / prev() from your own components
pagination (bullets)[data-sc-dots]Or render your own from state.page / state.pageCount
watchOverflowautomaticdata-sc-overflow on the root; controls hide without it
a11y modulebuilt inLive region, aria-disabled, keyboard
lazynative loading="lazy" on images
speednot configurableNative smooth scrolling; instant with reduced motion
allowTouchMove: falseoverflow-x: hidden on the trackArrows and the API still work

API

SwiperHere
new Swiper(el, options)attach(root, options)
swiper.slideTo(i)carousel.slideTo(i) (shows the page that contains slide i)
swiper.slideToLoop(i)carousel.slideTo(i)
swiper.slideNext() / slidePrev()carousel.next() / prev()
swiper.activeIndex, realIndexcarousel.index
swiper.snapIndexcarousel.page
swiper.isBeginning / isEndthe same names
swiper.slidescarousel.slides
swiper.el, wrapperElcarousel.root, carousel.track
swiper.update()carousel.update(), rarely needed: it measures on resize and content changes
swiper.destroy()carousel.destroy()
on('slideChange')sc:change event on the root, or onChange, once per settled move
onSwiper(swiper)carousel returned by attach, or carouselRef in React and Preact

Behaviour that differs:

  • No change event on attach. A wrapper that promised one should call its handler once with carousel.state after attaching.
  • index is the slide at the snap position. At the end of a row it is the first slide of the last full view, not the last slide.
  • Arrows at the ends are disabled only without rewind; with rewind they stay enabled.
  • There are no duplicate slides, so code that skips .swiper-slide-duplicate or reads data-swiper-slide-index goes away, as do workarounds that reset a container's scroll position after focus moved into a slide.

Deliberately not supported

  • A true infinite loop (cloned slides and a teleported scroll position). It is fragile on native scrolling and confusing for assistive technology; use rewind.
  • Vertical carousels, zoom, a draggable scrollbar, slide effects (fade, cube, flip), virtual slides, parallax, and syncing thumbnails with a second carousel. Thumbnails can call slideTo() and listen to sc:change.
  • Changing the transition speed or easing of moves.

Migrating step by step

  1. Keep your component's public props and imperative handle; swap its internals.
  2. Move per-device options into CSS: one media or container query per former breakpoint.
  3. Replace loop with rewind; check with product owners where an endless loop was visible.
  4. Keep your own arrows and dots, bound to next(), prev(), goToPage() and sc:change.
  5. Remove layout workarounds made for script-computed layout, such as first-image fixes for server rendering.
  6. Behind a flag, migrate one call site at a time and compare with visual tests.

scroll-carousel 0.1.5

MIT licence. Not included on purpose: vertical carousels, zoom, a draggable scrollbar, slide effects, virtual slides, synced thumbnails and a true infinite loop.