Building sections

Carousels

Swiper wiring that behaves in the editor.

useCarousel owns the four behaviours every carousel in the theme needs, so all of them behave identically:

1import { Swiper, SwiperSlide } from 'swiper/react'
2import { A11y, Autoplay, Navigation, Pagination } from 'swiper/modules'
3import { editorProps, useCarousel } from '@/framework'
4import CarouselArrow from '@components/atoms/CarouselArrow'
5
6export default function MySection({ id, settings, blocks }) {
7 const carousel = useCarousel({
8 sectionId: id,
9 items: blocks,
10 loop: settings.loop,
11 autoplay: settings.autoplay,
12 autoplayDelay: settings.autoplay_interval,
13 slidesPerView: 3,
14 })
15
16 if (carousel.count === 0) return null
17
18 return (
19 <div className="relative">
20 <Swiper
21 {...carousel.swiperProps}
22 modules={[A11y, Autoplay, Navigation, Pagination]}
23 slidesPerView={1}
24 breakpoints={{ 1024: { slidesPerView: 3 } }}
25 navigation={carousel.navigation}
26 >
27 {blocks.map((block) => (
28 <SwiperSlide key={block.id} {...editorProps(block.shopify_attributes)}>
29 {block.settings.heading}
30 </SwiperSlide>
31 ))}
32 </Swiper>
33
34 <CarouselArrow ref={carousel.setPrevEl} side="left" disabled={carousel.atStart} overlay />
35 <CarouselArrow ref={carousel.setNextEl} side="right" disabled={carousel.atEnd} overlay />
36 </div>
37 )
38}

The four behaviours

  1. Selecting a block scrolls to it. Depends on the whole selection object, not .id, so re-selecting the block already selected still works.
  2. Autoplay pauses while a block is selected, or the merchant is editing a moving target.
  3. Position survives a remount. Editing a setting replaces the section's markup, which would otherwise bounce the merchant back to the first slide.
  4. Loop and autoplay turn themselves off when they cannot work — too few slides to fill the view, or prefers-reduced-motion. Otherwise the merchant gets a control that silently does nothing.

What it returns

swiperPropsSpread onto <Swiper>. Anything you pass after it wins
navigationPass to <Swiper navigation={…}> once the arrow refs exist
setPrevEl / setNextElref for your previous / next buttons
canLoop, autoplayEnabledThe resolved values, after the viability checks
isSelectedTrue while the editor has one of this carousel's blocks selected
atStart, atEndEdge state — always false while looping
countitems.length, so an empty carousel can bail early
swiperThe instance, for anything the hook doesn't cover

useCarousel is Swiper-specific, but the editor behaviour underneath is not. The part that matters for any component is useRevealOnSelect — an accordion, a set of tabs, a rotator and a carousel all need the same trigger and differ only in what they do with it. useCarousel is a consumer of that hook, not a replacement for it.

Why a hook, not a component

The three carousels in the theme differ in layout — a full-bleed hero with fade transitions, an auto-height bar with arrows beside it, a responsive product grid — and folding those into one component means a prop for every difference.

Behaviour is what they actually share. Layout is not.

Arrows

CarouselArrow is a real <button> with an accessible name and a working disabled state, rather than Swiper's CSS-pseudo-element arrows, which are neither. Two looks:

  • solid (default) — a bordered pill, for arrows sitting over the slides.
  • bare — a plain icon button, for arrows sitting beside them in a row.

Add overlay to absolutely position it over the track, and inset to set its distance from the edge.