How to build a before/after carousel in React
A React before/after carousel pairs each item with a boolean state (showAfter) and wraps the displayed image in an AnimatePresence block so Framer Motion crossfades between the two states. Arrow buttons and dot indicators handle navigation; navigating resets showAfter to false so every new pair always starts on the before view.
- Stack: React 18 + Framer Motion 11 + Lucide React, ~140 lines, zero extra dependencies.
- Core Framer Motion API: AnimatePresence mode='wait', motion.div with opacity 0→1.
- Supports both image URLs (background-image) and solid color fallbacks per pair.
- Accessible: every nav button carries an aria-label; dot buttons name their target pair.
- Responsive by default (max-width 800px, aspect-ratio 3/2, fluid type via clamp).
Before After Carousel is a React section that lets users browse a set of transformation pairs, one at a time, then toggle between the before and after states with a single button. It fits anywhere you need to show sequential results: design revisions, renovation projects, skincare progressions. The layout is minimal so the content stays front and center.
Anatomy
The section has a centered header (title + description) above a constrained 800px canvas. Inside that canvas: an image area with a fixed 3:2 aspect ratio and rounded corners, a state badge pinned top-left showing 'Before' or 'After', a control row below (prev/next arrows, pair counter, label, toggle button) and a dot indicator strip at the bottom. The active dot stretches from 8px to 24px wide to signal the current position without color alone.
How it works
The transition between before and after is driven by AnimatePresence mode='wait' wrapping a motion.div keyed on `${pair.id}-${showAfter ? 'after' : 'before'}`. When the key changes, the exiting frame fades out first (opacity 0, duration 0.4s), then the entering frame fades in. A custom easing array [0.16, 1, 0.3, 1] gives both fades a fast start and a gentle tail. Navigating to a new pair calls setShowAfter(false) synchronously so the key flip and the state flip produce a single, clean crossfade rather than two consecutive ones.
How to build it in React
Model each pair as a typed interface
Define a Pair interface with an id, a label and optional beforeImage/afterImage URLs plus beforeColor/afterColor fallbacks. Pass the array as a prop so the component stays purely presentational.
interface Pair { id: string; label: string; beforeImage?: string; afterImage?: string; beforeColor?: string; afterColor?: string; }Derive the background from current state
Inside the motion.div, pick the background at render time: if showAfter is true and afterImage is set, use a CSS url() background-image; fall back to afterColor, then to the accent token. The same logic applies to the before side with beforeImage and beforeColor.
background: showAfter ? (pair.afterImage ? `url(${pair.afterImage}) center/cover` : pair.afterColor ?? "var(--color-accent)") : (pair.beforeImage ? `url(${pair.beforeImage}) center/cover` : pair.beforeColor ?? "var(--color-background-alt)"),Key AnimatePresence for clean crossfades
Wrap the motion.div in AnimatePresence mode='wait'. Use a compound key that combines the pair id and the current view so any state change, navigation or toggle, produces a fresh mount/unmount cycle rather than an interrupted animation.
<AnimatePresence mode="wait"> <motion.div key={`${pair.id}-${showAfter ? "after" : "before"}`} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }} /> </AnimatePresence>Reset state on navigation
In both prev and next handlers, call setShowAfter(false) alongside the index update. This guarantees the new pair always opens on its before view, which makes the reveal pattern feel intentional rather than random.
const prev = () => { setCurrent((c) => (c - 1 + pairs.length) % pairs.length); setShowAfter(false); };
When to use it
Reach for this carousel on landing pages where you have two or more transformation cases to show, beauty treatments, renovation work, design before/after, photo editing portfolios. It works best with four to eight pairs; fewer pairs feel sparse, and beyond eight the dot strip grows unwieldy. Avoid it when you need both states visible at the same time (use a slider-based split view instead), or when all your pairs are text-only and there is nothing visual to reveal.
Used by
- Squarespace, Uses before/after carousels in its template gallery to show site design transformations.
- Lightroom (Adobe), Showcases photo editing results with before/after toggles across marketing and onboarding pages.
- Houzz, Displays home renovation pairs in a sequential before/after browse pattern throughout project galleries.
FAQ
Why use AnimatePresence mode='wait' instead of a CSS opacity transition?
mode='wait' ensures the exiting element fully fades out before the entering one fades in, producing a clean sequential crossfade. A plain CSS transition on a conditional render would cause both states to overlap visually during the transition.
Can I supply real images instead of color placeholders?
Yes. Pass beforeImage and afterImage as absolute or relative URLs in each Pair object. The component renders them as CSS background-image with center/cover sizing. If no URL is provided it falls back to beforeColor or afterColor, then to the theme tokens.
How do I change the number of visible items or loop behavior?
The component shows one pair at a time and wraps around at both ends using modulo arithmetic. To disable looping, clamp the index: `setCurrent(c => Math.max(0, c - 1))` for prev and `Math.min(pairs.length - 1, c + 1)` for next, then hide the arrows at the boundaries.
Is the component accessible without a mouse?
All interactive controls are native buttons with aria-label attributes, so keyboard navigation works out of the box. The dot indicators each name their target pair. The state badge is visible text, not an icon-only affordance.