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โ
| State | Source | Purpose |
|---|---|---|
currentIndex | slider.onSlide callback | Active slide index. Drives card "active" styling. |
visibleIndices | getVisibleIndices(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 / isAtEnd | slider.stateIndex boundaries | Disables prev/next buttons in non-loop mode. Always false in loop mode (no boundaries). |
isStatic | slider.isStatic | True 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-slideclass on the leftmost-visible slide. - Sets
aria-hidden="true"on non-visible slides. - Sets
tabIndexon every focusable descendant of every slide (<a>,<button>, etc.) โ visible โ 0, hidden โ -1. This meanstabIndex={isVisible ? 0 : -1}on slide content is technically redundant; we still pass it explicitly as belt-and-suspenders for elementsFOCUSABLE_SELECTORmight 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 settingtransitionDuration: 0ms.next(N)advances by N states, where each state =slidesToScrollslides. WithslidesToScroll: 5andinitialSlide: 3, this would advance 15 slides โ almost certainly not what you want. SetslidesToScroll: 1if you wantinitialSlideto map 1:1 to slide index. (See the featured-carousel config inProductCarouselfor an example.) - Loop: sets
transitionDuration: 0ms, manually rebuilds the DOM order soinitialSlideis first (replicateswrapNextfrom blaze internals, idempotent across strict-mode double-mounts), then setsstateIndexto whichever state containsinitialSlide.
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 insyncSlideDOM. If you makeBlazeSlideconsumeBlazeSlideContext, 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-showbeats stylesheet rules. Bothblaze-sliderand our hook write to it inline. If a slider is rendering at the wrong width, inspect the slider element's inlinestylefirst. flexcontainers 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 setflex-basis: 100%/w-fullon the wrapper.slidesToScroll > 1breaks 1:1 slide-index navigation.slider.next(N)andslider.prev(N)operate on states, not slides. UsenavigateToSlide(slider, slideIndex, { match: 'exact' })fromlib/navigation.tsif you need to land on a specific slide regardless ofslidesToScroll.- Memoize
ProductCard-equivalent slide content. Parent re-renders on every context change (slider state, visibleIndices, etc.). Withoutmemo, every slide reconciles on every navigation. SeeMemoizedProductCardin~/components/ProductCarouselfor the pattern.