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'56export 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 })1516 if (carousel.count === 0) return null1718 return (19 <div className="relative">20 <Swiper21 {...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>3334 <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
- Selecting a block scrolls to it. Depends on the whole selection object,
not
.id, so re-selecting the block already selected still works. - Autoplay pauses while a block is selected, or the merchant is editing a moving target.
- Position survives a remount. Editing a setting replaces the section's markup, which would otherwise bounce the merchant back to the first slide.
- 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
swiperProps | Spread onto <Swiper>. Anything you pass after it wins |
navigation | Pass to <Swiper navigation={…}> once the arrow refs exist |
setPrevEl / setNextEl | ref for your previous / next buttons |
canLoop, autoplayEnabled | The resolved values, after the viability checks |
isSelected | True while the editor has one of this carousel's blocks selected |
atStart, atEnd | Edge state — always false while looping |
count | items.length, so an empty carousel can bail early |
swiper | The instance, for anything the hook doesn't cover |
Not building a carousel?
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.