Skip to main content

BlazeSlider

A wrapper around the blaze-slider library that adapts it to React conventions, exposes slider state via context, and adds a11y / analytics hooks. Replaces the Swiper-based carousel components that previously lived in ~/components/ProductCarousel, ~/components/SwoggleCarousel, etc.

Why we moved off Swiper (pre-mortem)โ€‹

Documenting up-front so the next person reading the diff understands the trade-offs we accepted.

The case for movingโ€‹

  • Bundle size. swiper ships ~140 kB minified across its modules; blaze-slider is ~3.5 kB. Carousels appear on every PDP, PLP, and homepage block, so this is paid on most page loads.
  • DOM reuse. blaze-slider physically reorders existing DOM nodes for loop wrapping (track.append / track.prepend) instead of cloning slides like Swiper. With React-rendered children that contain images, video, favorite buttons, and quick-add forms, cloning was producing duplicate-key warnings and double-mounting fetchers.
  • Imperative escape hatch. Swiper's React bindings hide the underlying instance behind onSwiper / swiperRef.current, and several lifecycle behaviors (e.g. slideTo during initial render, update() after dynamic children) are racy. blaze-slider exposes a plain class instance โ€” we read state via slider.states[slider.stateIndex].page and write via slider.next(N) / slider.prev(N) directly.
  • Reconciliation safety. Because blaze-slider reorders DOM nodes, React must NOT re-render the slide elements during navigation โ€” otherwise reconciliation snaps them back into source order, causing a visible flash. Our BlazeSlide component is intentionally context-unaware (it never re-renders on slider state changes); per-slide visibility / focus updates happen via direct DOM writes in syncSlideDOM. This separation is hard to retrofit onto Swiper's render-prop API.
  • ARIA. We can apply aria-roledescription="carousel", aria-live policies, scoped headings, and per-slide aria-label="N of M" on our own terms instead of fighting Swiper's a11y module.

What could still bite usโ€‹

  • Inline styles vs. scoped CSS. blaze-slider writes --slides-to-show and --slide-gap as inline styles on the slider root, which beats any stylesheet rule. Our useCustomBlazeSlider hook re-asserts the configured value via preserveSlideWidth() after blaze auto-clamps for static mode. If you find a carousel rendering 1 slide instead of N, suspect this path.
  • Loop wrapping race conditions. Loop mode involves a setTimeout(transitionDuration) + DOM reorder + double requestAnimationFrame`` sequence. Calls to slider.next/prev while isTransitioning is true are ignored โ€” make sure programmatic navigation (e.g. via navigateToSlide) waits for the previous transition.
  • Strict-mode double mount in dev. Initial-slide DOM reordering is idempotent (uses data-slide-index to figure out current order), but if you add new imperative effects, verify they're idempotent too.

Component anatomyโ€‹

<BlazeSlider>                       โ† BlazeSlideContext.Provider lives here
<BlazeButton direction="prev" /> โ† reads context, calls slider.prev()
<BlazeTrack> โ† wraps the .blaze-track DOM blaze needs
<BlazeSlide index={0}>... โ† intentionally context-UNAWARE (don't re-render!)
<BlazeSlide index={1}>...
</BlazeTrack>
<BlazeButton direction="next" />
</BlazeSlider>
ComponentPurposeReads context?
BlazeSliderProvider. Owns the lifecycle of the underlying blaze-slider instance via useCustomBlazeSlider. Renders scoped CSS for --slides-to-show / --slide-gap per breakpoint.Provides
BlazeTrackRenders the .blaze-track-container + .blaze-track DOM. Two render modes (see below).Yes โ€” isStatic, visibleIndices
BlazeSlideWraps each slide's content; sets data-slide-index so blaze can find it. Intentionally never re-renders on context change to avoid fighting blaze's DOM reordering during loop wrapping.No
BlazeButtonprev / next nav button. Uses context (isAtStart, isAtEnd, slider.prev/next) instead of .blaze-prev / .blaze-next class lookup so nested sliders work.Yes
CarouselCounterOptional 1 / N indicator that follows the slider state.Yes

Type quick-referenceโ€‹

Live in lib/types.d.ts:

  • BlazeSliderConfig โ€” slidesToShow / scroll / gap / breakpoints / loop / autoplay / transition.
  • SimpleBlazeSliderProps<T> โ€” simple mode (slides + slideRenderer + button/track/slide forwarding).
  • ComposableBlazeSliderProps โ€” composable mode (children).
  • BlazeTrackProps<T> โ€” both children mode (children) and renderer mode (items + slideRenderer + getSlideKey + slideProps). Selected at runtime by the presence of slideRenderer.
  • BlazeButtonProps โ€” direction + chevronSize + analytics hooks.
  • BlazeSlideProps<T> โ€” index (required, used as data-slide-index) + optional tag for semantic markup.

Existing call-sitesโ€‹

For reference patterns:

SiteModeNotable bits
ProductCarouselComposable + BlazeTrack rendererFeatured-mode CSS scaling; slidesToScroll: 1 when featured for initialSlide to map cleanly.
TabbedProductCarouselComposable + BlazeTrack rendererAll tabs mounted; CSS-hidden via [hidden].
SwoggleCarouselComposable, custom inner componentTwo paired sliders kept in positional sync via navigateToSlide on tab change.
RecentlyViewedCarouselComposableLocal ProductCarousel (not the shared one).