Skip to main content

useCustomBlazeSlider hook

Overviewโ€‹

The hook in hook/useCustomBlazeSlider.ts is what BlazeSlider uses internally. It's the single source of truth for slider state and DOM sync. Sequenced lifecycle:

1. Mountโ€‹

sliderRef.current = new BlazeSlider(el, blazeConfig);
setSlider(sliderRef.current);

Instantiates blaze-slider on the ref element. The instance is held in two places: a useRef (sliderRef.current) for synchronous reads inside effects/closures, and a reactive useState (slider) that's exposed via context so consumers re-render when the instance becomes available.

2. Reactive stateโ€‹

StateSourcePurpose
currentIndexslider.onSlide callbackActive slide index. Drives card "active" styling.
visibleIndicesgetVisibleIndices(firstVisible, slidesToShow, totalSlides)Set of all indices currently in the visible window. Used for tabIndex / lazy loading. Wraps modulo totalSlides in loop mode (e.g. getVisibleIndices(6, 4, 8) โ†’ {6, 7, 0, 1}).
isAtStart / isAtEndslider.stateIndex boundariesDisables prev/next buttons in non-loop mode. Always false in loop mode (no boundaries).
isStaticslider.isStaticTrue when totalSlides <= slidesToShow. Used to hide arrows / drop autoplay.

3. syncVisibleIndices (single source of truth for visibility)โ€‹

const syncVisibleIndices = () => {
const slidesToShow = blazeSlider.config.slidesToShow ?? 1;
const firstVisible = blazeSlider.states[blazeSlider.stateIndex]?.page[0] ?? 0;
const total = isLooping ? blazeSlider.totalSlides : undefined;
const next = getVisibleIndices(firstVisible, slidesToShow, total);
setVisibleIndices(prev => hasIndicesChanged(prev, next) ? next : prev);
syncSlideDOM(ref.current, Math.floor(firstVisible), next);
};

Called from onSlide, BlazeButton.onClick (via context), the resize handler, and initial setup. Reads firstVisible from slider.states[stateIndex] (synchronous after next()/prev()) rather than from the onSlide callback (asynchronous โ€” fires after 3+ rAFs in loop mode), so the visibility set is always current.

syncSlideDOM then walks every [data-slide-index] element under the slider scope and:

  • Toggles the active-carousel-slide class on the leftmost-visible slide.
  • Sets aria-hidden="true" on non-visible slides.
  • Sets tabIndex on every focusable descendant of every slide (<a>, <button>, etc.) โ€” visible โ†’ 0, hidden โ†’ -1. This means tabIndex={isVisible ? 0 : -1} on slide content is technically redundant; we still pass it explicitly as belt-and-suspenders for elements FOCUSABLE_SELECTOR might miss.

The scope-id check (data-blaze-scope) ensures nested sliders don't manage each other's focusable elements.

4. Initial-slide handlingโ€‹

Setting initialSlide={N} (where N > 0) requires different code paths for loop vs. non-loop, and is dependent on slidesToScroll:

  • Non-loop: calls slider.next(N) after setting transitionDuration: 0ms. next(N) advances by N states, where each state = slidesToScroll slides. With slidesToScroll: 5 and initialSlide: 3, this would advance 15 slides โ€” almost certainly not what you want. Set slidesToScroll: 1 if you want initialSlide to map 1:1 to slide index. (See the featured-carousel config in ProductCarousel for an example.)
  • Loop: sets transitionDuration: 0ms, manually rebuilds the DOM order so initialSlide is first (replicates wrapNext from blaze internals, idempotent across strict-mode double-mounts), then sets stateIndex to whichever state contains initialSlide.

5. preserveSlideWidth (workaround for static-mode clamping)โ€‹

When totalSlides < slidesToShow, blaze-slider mutates config.slidesToShow = totalSlides so all slides fit in view. It also writes --slides-to-show: <totalSlides> inline, which inflates each slide's width. Our hook detects isStatic and re-asserts the configured value:

const preserveSlideWidth = () => {
if (blazeSlider.isStatic) {
el.style.setProperty('--slides-to-show', String(getConfiguredSlidesToShow()));
}
setStatic(blazeSlider.isStatic);
};

getConfiguredSlidesToShow() walks the breakpoints config and picks the matching value at the current viewport โ€” so static-mode slides keep the same width across carousels regardless of how many items are populated.

6. Resize syncโ€‹

window.addEventListener('resize', () => {
syncBoundaries();
requestAnimationFrame(() => {
preserveSlideWidth();
syncVisibleIndices();
});
});

Runs after blaze's own resize handler (which re-evaluates breakpoints and may change slidesToShow / clamp stateIndex). The rAF defers our reads to the next frame so we observe blaze's updated state rather than the pre-resize state.

7. Autoplay listeners (conditional)โ€‹

Only wired when enableAutoplay: true โ€” pause on mouseenter / focusin, resume on mouseleave / focusout. Skipped entirely otherwise to avoid per-instance event-listener allocation in the dominant non-autoplay case.

8. Cleanupโ€‹

The effect's cleanup tears down the slide subscription, calls slider.destroy(), removes autoplay listeners (only if attached), and removes the resize listener.

Gotchasโ€‹

  • Don't re-render <BlazeSlide> on slider state. Visibility / aria / tabIndex are managed via direct DOM writes in syncSlideDOM. If you make BlazeSlide consume BlazeSlideContext, React will reorder the DOM nodes back to source order during loop wrapping โ†’ visible flash.
  • A component cannot read context it provides. If you find yourself wanting useBlazeSlideContext() next to a <BlazeSlider>, you're one component too high. Drop into a child or use renderer mode.
  • Inline --slides-to-show beats stylesheet rules. Both blaze-slider and our hook write to it inline. If a slider is rendering at the wrong width, inspect the slider element's inline style first.
  • flex containers collapse the slider to content width. <BlazeSlider>'s outer <div> has no explicit width. If you wrap it in a flex parent without setting width on the slider's outer element, slides shrink to one-slide width. Use a block container or set flex-basis: 100% / w-full on the wrapper.
  • slidesToScroll > 1 breaks 1:1 slide-index navigation. slider.next(N) and slider.prev(N) operate on states, not slides. Use navigateToSlide(slider, slideIndex, { match: 'exact' }) from lib/navigation.ts if you need to land on a specific slide regardless of slidesToScroll.
  • Memoize ProductCard-equivalent slide content. Parent re-renders on every context change (slider state, visibleIndices, etc.). Without memo, every slide reconciles on every navigation. See MemoizedProductCard in ~/components/ProductCarousel for the pattern.