Animate the carousel like Carousel II, but keep the DOM footprint minimal: render only the current slide instead of all N. For heavy slides (real images), holding every one in the DOM is wasteful — so render one, and replay a CSS enter animation each time the slide changes by remounting it with a key.
// A self-contained component. No props.
function App(): JSX.Element;
A viewport that holds exactly one slide, animating each change.
Next -> slide remounts (key = index) -> enters from the right
Prev -> slide remounts -> enters from the left
the DOM always contains one .slide, regardless of how many slides exist
SLIDES[index] only — not a track of all of them.key={index} makes React unmount the old slide and mount the new, replaying its CSS animation.@keyframes slide-in animates the single mounted slide.Carousel II keeps all N slides in the DOM and slides a track. Here we keep only one slide in the DOM and animate it on change. The trick: give the slide a key={index} so React unmounts the old one and mounts a new one — and a fresh mount replays the CSS enter animation.
A track of all slides is simple, but if each slide is a real (large) image, you're paying to keep every one mounted and decoded. For a minimal footprint you render just the current slide. But then there's no adjacent slide to "slide between" — so instead of moving a track, you animate the single slide as it enters. React's key is the lever: change the key and React treats it as a brand-new element, remounting it; CSS animations run on mount, so the new slide plays its entrance. A direction tells it which side to come from.
State: index and direction (1 for next, -1 for prev). Render one slide: SLIDES[index], with key={index} and a class picked by direction (from-right when going forward, from-left when back). Because the key changes on every navigation, React swaps the DOM node, and the @keyframes (translateX(±100%) → 0) animates the entrance. Prev/Next set direction then wrap index with modulo; dots derive from index.
A first attempt renders one slide but without a changing key:
<div className="slide from-right" style={{ background: SLIDES[index].bg }}>
{SLIDES[index].label}
</div>
The content updates, but React reuses the same DOM node (same position, no key change), so the CSS animation doesn't replay — you get an instant swap, not a slide-in. Giving it key={index} forces a remount, which is what re-triggers the keyframe each time.
import { useState } from 'react';
import './styles.css';
const SLIDES = [
{ label: 'Sunset', bg: 'linear-gradient(135deg, #fb7185, #fbbf24)' },
{ label: 'Ocean', bg: 'linear-gradient(135deg, #38bdf8, #6366f1)' },
{ label: 'Forest', bg: 'linear-gradient(135deg, #4ade80, #16a34a)' },
{ label: 'Berry', bg: 'linear-gradient(135deg, #a78bfa, #ec4899)' },
];
export default function App() {
const [index, setIndex] = useState(0);
const [direction, setDirection] = useState(1);
const n = SLIDES.length;
function prev() {
setDirection(-1);
setIndex((i) => (i - 1 + n) % n);
}
function next() {
setDirection(1);
setIndex((i) => (i + 1) % n);
}
const slide = SLIDES[index];
const enter = direction > 0 ? 'from-right' : 'from-left';
return (
<main className="container">
<h1>Image Carousel III</h1>
<div className="carousel">
<button className="nav prev" aria-label="Previous" onClick={prev}>
‹
</button>
<div key={index} className={`slide ${enter}`} style={{ background: slide.bg }}>
{slide.label}
</div>
<button className="nav next" aria-label="Next" onClick={next}>
›
</button>
</div>
<div className="dots">
{SLIDES.map((_, i) => (
<button
key={i}
className={i === index ? 'dot active' : 'dot'}
aria-label={`Go to slide ${i + 1}`}
onClick={() => {
setDirection(i > index ? 1 : -1);
setIndex(i);
}}
/>
))}
</div>
</main>
);
}
Only one .slide is ever rendered — SLIDES[index]. Its key={index} is the crux: when index changes, React unmounts the previous slide node and mounts a fresh one, and the CSS animation on .from-right/.from-left runs on that mount, producing the slide-in. direction chooses the entrance side, set by Prev/Next (and by dots, comparing target to current). The index/wrap/dots logic matches the earlier carousels; the difference is purely the one-node-plus-remount rendering.
index = 0, direction = 1.
slide from-right; it animates in from the right on mount.direction = 1, index = 1. key changes 0 → 1 → React remounts the slide as Ocean with from-right; the keyframe replays, sliding it in from the right.direction = -1, index = 0. key 1 → 0 → remount as Sunset with from-left; it enters from the left, matching the back direction.direction = (3 > 0) = 1, index = 3 → Berry remounts, entering from the right..slide element — adding 100 more slides to SLIDES wouldn't add a single DOM node.key={index}.transition instead of @keyframes. A transition needs two states on a persistent node; a remounting node wants an animation. Fix: keyframes that run on mount.direction flag.position: absolute inside a positioned, clipped .carousel.index-1, index, index+1 for true between-slide motion while still bounded DOM.import{useReducer}from'react';import'./styles.css';const SLIDES=[{label:'Sunset',bg:'linear-gradient(135deg, #fb7185, #fbbf24)'},{label:'Ocean',bg:'linear-gradient(135deg, #38bdf8, #6366f1)'},{label:'Forest',bg:'linear-gradient(135deg, #4ade80, #16a34a)'},{label:'Berry',bg:'linear-gradient(135deg, #a78bfa, #ec4899)'}];type State={index:number;direction:number};type Action={type:'prev'|'next'}|{type:'go';index:number};function reducer(state:State,action:Action):State{if(action.type==='prev')return{index:(state.index+3)%4,direction:-1};if(action.type==='next')return{index:(state.index+1)%4,direction:1};return{index:action.index,direction:action.index>state.index?1:-1};}export default function App(){const[state,dispatch]=useReducer(reducer,{index:0,direction:1});const slide=SLIDES[state.index];return <main className="container"><h1>Image Carousel III</h1><div className="carousel"><button className="nav prev" aria-label="Previous" onClick={()=>dispatch({type:'prev'})}>‹</button><div key={state.index} className={`slide ${state.direction>0?'from-right':'from-left'}`} style={{background:slide.bg}}>{slide.label}</div><button className="nav next" aria-label="Next" onClick={()=>dispatch({type:'next'})}>›</button></div><div className="dots">{SLIDES.map((_,index)=><button key={index} className={index===state.index?'dot active':'dot'} aria-label={`Go to slide ${index+1}`} onClick={()=>dispatch({type:'go',index})}/>)}</div></main>;}The reducer commits the new index and its entrance direction as one transition.
import{useState}from'react';import'./styles.css';const SLIDES=[{label:'Sunset',bg:'linear-gradient(135deg, #fb7185, #fbbf24)'},{label:'Ocean',bg:'linear-gradient(135deg, #38bdf8, #6366f1)'},{label:'Forest',bg:'linear-gradient(135deg, #4ade80, #16a34a)'},{label:'Berry',bg:'linear-gradient(135deg, #a78bfa, #ec4899)'}];function useCarousel(){const[index,setIndex]=useState(0),[direction,setDirection]=useState(1);function step(delta:number){setDirection(delta);setIndex(current=>(current+delta+SLIDES.length)%SLIDES.length);}function go(target:number){setDirection(target>index?1:-1);setIndex(target);}return{index,direction,step,go};}export default function App(){const carousel=useCarousel(),slide=SLIDES[carousel.index];return <main className="container"><h1>Image Carousel III</h1><div className="carousel"><button className="nav prev" aria-label="Previous" onClick={()=>carousel.step(-1)}>‹</button><div key={carousel.index} className={`slide ${carousel.direction>0?'from-right':'from-left'}`} style={{background:slide.bg}}>{slide.label}</div><button className="nav next" aria-label="Next" onClick={()=>carousel.step(1)}>›</button></div><div className="dots">{SLIDES.map((_,index)=><button key={index} className={index===carousel.index?'dot active':'dot'} aria-label={`Go to slide ${index+1}`} onClick={()=>carousel.go(index)}/>)}</div></main>;}The hook isolates wrapping and direction while the component mounts exactly one keyed slide.
Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Animate the carousel like Carousel II, but keep the DOM footprint minimal: render only the current slide instead of all N. For heavy slides (real images), holding every one in the DOM is wasteful — so render one, and replay a CSS enter animation each time the slide changes by remounting it with a key.
// A self-contained component. No props.
function App(): JSX.Element;
A viewport that holds exactly one slide, animating each change.
Next -> slide remounts (key = index) -> enters from the right
Prev -> slide remounts -> enters from the left
the DOM always contains one .slide, regardless of how many slides exist
SLIDES[index] only — not a track of all of them.key={index} makes React unmount the old slide and mount the new, replaying its CSS animation.@keyframes slide-in animates the single mounted slide.Carousel II keeps all N slides in the DOM and slides a track. Here we keep only one slide in the DOM and animate it on change. The trick: give the slide a key={index} so React unmounts the old one and mounts a new one — and a fresh mount replays the CSS enter animation.
A track of all slides is simple, but if each slide is a real (large) image, you're paying to keep every one mounted and decoded. For a minimal footprint you render just the current slide. But then there's no adjacent slide to "slide between" — so instead of moving a track, you animate the single slide as it enters. React's key is the lever: change the key and React treats it as a brand-new element, remounting it; CSS animations run on mount, so the new slide plays its entrance. A direction tells it which side to come from.
State: index and direction (1 for next, -1 for prev). Render one slide: SLIDES[index], with key={index} and a class picked by direction (from-right when going forward, from-left when back). Because the key changes on every navigation, React swaps the DOM node, and the @keyframes (translateX(±100%) → 0) animates the entrance. Prev/Next set direction then wrap index with modulo; dots derive from index.
A first attempt renders one slide but without a changing key:
<div className="slide from-right" style={{ background: SLIDES[index].bg }}>
{SLIDES[index].label}
</div>
The content updates, but React reuses the same DOM node (same position, no key change), so the CSS animation doesn't replay — you get an instant swap, not a slide-in. Giving it key={index} forces a remount, which is what re-triggers the keyframe each time.
import { useState } from 'react';
import './styles.css';
const SLIDES = [
{ label: 'Sunset', bg: 'linear-gradient(135deg, #fb7185, #fbbf24)' },
{ label: 'Ocean', bg: 'linear-gradient(135deg, #38bdf8, #6366f1)' },
{ label: 'Forest', bg: 'linear-gradient(135deg, #4ade80, #16a34a)' },
{ label: 'Berry', bg: 'linear-gradient(135deg, #a78bfa, #ec4899)' },
];
export default function App() {
const [index, setIndex] = useState(0);
const [direction, setDirection] = useState(1);
const n = SLIDES.length;
function prev() {
setDirection(-1);
setIndex((i) => (i - 1 + n) % n);
}
function next() {
setDirection(1);
setIndex((i) => (i + 1) % n);
}
const slide = SLIDES[index];
const enter = direction > 0 ? 'from-right' : 'from-left';
return (
<main className="container">
<h1>Image Carousel III</h1>
<div className="carousel">
<button className="nav prev" aria-label="Previous" onClick={prev}>
‹
</button>
<div key={index} className={`slide ${enter}`} style={{ background: slide.bg }}>
{slide.label}
</div>
<button className="nav next" aria-label="Next" onClick={next}>
›
</button>
</div>
<div className="dots">
{SLIDES.map((_, i) => (
<button
key={i}
className={i === index ? 'dot active' : 'dot'}
aria-label={`Go to slide ${i + 1}`}
onClick={() => {
setDirection(i > index ? 1 : -1);
setIndex(i);
}}
/>
))}
</div>
</main>
);
}
Only one .slide is ever rendered — SLIDES[index]. Its key={index} is the crux: when index changes, React unmounts the previous slide node and mounts a fresh one, and the CSS animation on .from-right/.from-left runs on that mount, producing the slide-in. direction chooses the entrance side, set by Prev/Next (and by dots, comparing target to current). The index/wrap/dots logic matches the earlier carousels; the difference is purely the one-node-plus-remount rendering.
index = 0, direction = 1.
slide from-right; it animates in from the right on mount.direction = 1, index = 1. key changes 0 → 1 → React remounts the slide as Ocean with from-right; the keyframe replays, sliding it in from the right.direction = -1, index = 0. key 1 → 0 → remount as Sunset with from-left; it enters from the left, matching the back direction.direction = (3 > 0) = 1, index = 3 → Berry remounts, entering from the right..slide element — adding 100 more slides to SLIDES wouldn't add a single DOM node.key={index}.transition instead of @keyframes. A transition needs two states on a persistent node; a remounting node wants an animation. Fix: keyframes that run on mount.direction flag.position: absolute inside a positioned, clipped .carousel.index-1, index, index+1 for true between-slide motion while still bounded DOM.import{useReducer}from'react';import'./styles.css';const SLIDES=[{label:'Sunset',bg:'linear-gradient(135deg, #fb7185, #fbbf24)'},{label:'Ocean',bg:'linear-gradient(135deg, #38bdf8, #6366f1)'},{label:'Forest',bg:'linear-gradient(135deg, #4ade80, #16a34a)'},{label:'Berry',bg:'linear-gradient(135deg, #a78bfa, #ec4899)'}];type State={index:number;direction:number};type Action={type:'prev'|'next'}|{type:'go';index:number};function reducer(state:State,action:Action):State{if(action.type==='prev')return{index:(state.index+3)%4,direction:-1};if(action.type==='next')return{index:(state.index+1)%4,direction:1};return{index:action.index,direction:action.index>state.index?1:-1};}export default function App(){const[state,dispatch]=useReducer(reducer,{index:0,direction:1});const slide=SLIDES[state.index];return <main className="container"><h1>Image Carousel III</h1><div className="carousel"><button className="nav prev" aria-label="Previous" onClick={()=>dispatch({type:'prev'})}>‹</button><div key={state.index} className={`slide ${state.direction>0?'from-right':'from-left'}`} style={{background:slide.bg}}>{slide.label}</div><button className="nav next" aria-label="Next" onClick={()=>dispatch({type:'next'})}>›</button></div><div className="dots">{SLIDES.map((_,index)=><button key={index} className={index===state.index?'dot active':'dot'} aria-label={`Go to slide ${index+1}`} onClick={()=>dispatch({type:'go',index})}/>)}</div></main>;}The reducer commits the new index and its entrance direction as one transition.
import{useState}from'react';import'./styles.css';const SLIDES=[{label:'Sunset',bg:'linear-gradient(135deg, #fb7185, #fbbf24)'},{label:'Ocean',bg:'linear-gradient(135deg, #38bdf8, #6366f1)'},{label:'Forest',bg:'linear-gradient(135deg, #4ade80, #16a34a)'},{label:'Berry',bg:'linear-gradient(135deg, #a78bfa, #ec4899)'}];function useCarousel(){const[index,setIndex]=useState(0),[direction,setDirection]=useState(1);function step(delta:number){setDirection(delta);setIndex(current=>(current+delta+SLIDES.length)%SLIDES.length);}function go(target:number){setDirection(target>index?1:-1);setIndex(target);}return{index,direction,step,go};}export default function App(){const carousel=useCarousel(),slide=SLIDES[carousel.index];return <main className="container"><h1>Image Carousel III</h1><div className="carousel"><button className="nav prev" aria-label="Previous" onClick={()=>carousel.step(-1)}>‹</button><div key={carousel.index} className={`slide ${carousel.direction>0?'from-right':'from-left'}`} style={{background:slide.bg}}>{slide.label}</div><button className="nav next" aria-label="Next" onClick={()=>carousel.step(1)}>›</button></div><div className="dots">{SLIDES.map((_,index)=><button key={index} className={index===carousel.index?'dot active':'dot'} aria-label={`Go to slide ${index+1}`} onClick={()=>carousel.go(index)}/>)}</div></main>;}The hook isolates wrapping and direction while the component mounts exactly one keyed slide.
Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.