Part V β The Craft Β· Lesson 10.1
Image Slider
In one lineOne transform on a track, plus autoplay, swipe, preload, and the accessibility everyone forgets.
Where you've seen itThe homepage banner that rotates every five seconds and keeps changing while you're reading it.
Diagram
THE TRACK MODEL β move one element, not four
viewport (overflow: hidden)
βββββββββββββββββ
β slide 2 β
βββββββββββββββββ
βββββββββββββββββ¬ββββββββββββββββ¬ββββββββββββββββ¬βββββββββββββ
β slide 1 β slide 2 β slide 3 β slide 4 β
βββββββββββββββββ΄ββββββββββββββββ΄ββββββββββββββββ΄βββββββββββββ
βββ transform: translateX(-100% Γ index) βββΊ
transform + opacity only: composited, no layout, 60fps (12.4)INFINITE LOOP β clones and the silent jump
[4'] [1] [2] [3] [4] [1'] ' = clone
β² β²
clone of 4 clone of 1
at slide 4 β animate to [1'] β on transitionend, jump to [1]
with transition disabled. The user never sees the reset.AUTOPLAY RULES β or it becomes an accessibility violation
pause on hover user is reading
pause on focus within keyboard user is in it
pause when tab hidden visibilitychange β saves battery
stop permanently after explicit intent beats automation
manual interaction
respect prefers-reduced-motion no auto-advance at all
always provide play/pause WCAG 2.2.2 for anything > 5sHow it works
- Lay slides in a flex row inside an
overflow: hiddenviewport and move the track withtranslateX. - Animate
transformonly βleftormargintriggers layout on every frame (12.4). - Preload neighbours: current, next, previous eagerly; everything else
loading="lazy". - Handle swipe with pointer events, tracking delta and velocity; commit past ~25% of width or on a fast flick.
- Make it accessible:
role="region"witharia-roledescription="carousel", real<button>controls, a live region announcing "Slide 2 of 5", and full keyboard support.
const onPointerMove = (e) => {
if (!dragging) return;
const dx = e.clientX - startX;
track.style.transform = `translateX(calc(${-index * 100}% + ${dx}px))`;
};
const onPointerUp = (e) => {
const dx = e.clientX - startX;
const commit = Math.abs(dx) > width * 0.25;
setIndex(clamp(index + (commit ? -Math.sign(dx) : 0), 0, last));
};Trade-offs
| Approach | Notes |
|---|---|
| CSS scroll-snap | Free momentum + a11y; less control over autoplay |
| JS transform track | Full control; you own everything |
| Clone-based infinite | Seamless; edge cases on rapid clicks |
| Non-infinite | Simplest, and honestly fine for most banners |
Worth saying in an interview: carousels perform poorly in most published usability data β users rarely see past slide one. Suggesting a static hero or a scrollable row of cards instead of a rotating banner is a legitimate senior answer.
Interview angle
"Build an image carousel."
- Track with a single
translateX, transform-only animation, neighbour preloading, and pointer-based swipe with a commit threshold. - Autoplay pauses on hover, focus, and hidden tab, stops after interaction, and honours reduced motion.
- Mention CSS
scroll-snapas the simpler path when autoplay isn't required β it gives you momentum and accessibility for free.
Recap
- Move one track with
transform; never animate layout properties. - Preload neighbours, lazy-load the rest.
- Autoplay without pause controls is an accessibility failure, not a feature.