Retour au catalogue

Product Showcase Exploded

Vue eclatee d'un produit : plusieurs couches se separent au scroll (translateY/X differents). Au repos, tout est assemble.

product-showcasecomplex Both Responsive a11y
boldcorporatesaasecommerceuniversalcentered
Theme

How to build a scroll-driven exploded product view in React

A scroll-driven exploded view in React stacks product layers absolutely and fans them apart as the user scrolls, using Framer Motion's useScroll to read progress and useTransform to map that progress to per-layer translateY, translateX and rotateX values. Each layer gets its own motion values derived from its distance from the center index.

  • Stack: React 19 + Framer Motion 11 + CSS custom properties, ~200 lines, zero icon dependency.
  • Core API: useScroll (target + offset), useTransform, 3D perspective on the container.
  • Accessible: each layer label is readable text; the animation is decorative and does not hide any content.
  • Responsive: maxWidth 500px centered; works on mobile (scroll is available everywhere, unlike pointer events).
  • Accepts 3 to 5 layers; fewer than 3 weakens the effect, more than 5 creates overlap clutter.

Product Showcase Exploded is a scroll-driven section that reveals the architecture of a product by separating its visual layers as the user scrolls through. At rest, all layers are stacked flush; as scroll progress advances, they fan out vertically and sideways with a slight 3D tilt, the kind of reveal that turns "trust us" copy into a concrete, explorable diagram.

Anatomy

A centered header (badge, h2, description) fades in on first viewport entry via a whileInView motion.div. Below it sits a 500px container with perspective:1200px set directly in the style prop. Each layer is an absolutely positioned motion.div (the first layer is relative to establish container height) holding a 5:3 aspect-ratio card with a semi-transparent background, a border, and a centered label. A faint scroll hint paragraph appears below the stack.

How it works

The section element is passed as target to useScroll with offset ["start 0.7", "end 0.3"], so the animation runs while the section occupies the middle 60% of the viewport. For each layer, the code computes an offset value: the layer's index minus the center index of the array. That offset is then used to scale useTransform outputs, layers above center travel upward and to the left, layers below center travel downward and to the right. rotateX animates from 0 to -8 degrees in the first half of the scroll, giving the whole stack a subtle forward lean. Opacity starts at 0.6 before the scroll window, rises to 1 during, and settles back to 0.8 at the end.

How to build it in React

  1. Set up the scroll watcher on the section

    Attach a ref to your section element and pass it to useScroll. The offset array controls when the animation window opens and closes, "start 0.7" means the animation begins when the top of the section reaches 70% from the top of the viewport.

    const sectionRef = useRef<HTMLElement>(null);
    const { scrollYProgress } = useScroll({
      target: sectionRef,
      offset: ["start 0.7", "end 0.3"],
    });
  2. Compute per-layer motion values from the center index

    Inside the map, derive an offset from the layer's distance to the center of the array. Multiply that offset by your desired spread distance to get translateY and translateX ranges. Each call to useTransform produces an independent motion value that Framer Motion subscribes to directly, no re-renders on scroll.

    const centerIndex = (totalLayers - 1) / 2;
    const offset = i - centerIndex;
    
    const translateY = useTransform(
      scrollYProgress,
      [0, 0.5, 1],
      [0, offset * 80, offset * 120],
    );
    const translateX = useTransform(
      scrollYProgress,
      [0, 0.5, 1],
      [0, offset * 15, offset * 25],
    );
  3. Add a 3D tilt with rotateX and perspective

    Set perspective:1200px on the container div (not the motion.divs themselves) so all layers share the same 3D space. Map scrollYProgress from 0 to 0.5 onto rotateX values of 0 to -8 degrees. The negative direction tilts the top of the stack toward the viewer.

    const rotateX = useTransform(
      scrollYProgress,
      [0, 0.5],
      [0, -8],
    );
    
    // On the container:
    <div style={{ perspective: "1200px" }}>
  4. Position layers absolutely, first one relative

    Set position:'relative' on the first layer so the container takes its height, and position:'absolute', top:0, left:0, right:0 on every subsequent layer. This way all layers start stacked flush and the container does not collapse. Assign zIndex as totalLayers - i so the first layer renders on top.

    position: i === 0 ? "relative" : "absolute",
    top: 0,
    left: 0,
    right: 0,
    zIndex: totalLayers - i,

When to use it

Reach for this pattern when a product has clearly distinct technical layers worth naming: SaaS platforms (UI, API, infra), hardware products (casing, PCB, sensor array), or design tools (renderer, plugin layer, canvas). It works best in a "how it works" or "technology" section placed early to middle in the page. Skip it on simple marketing pages where complexity is a liability, and avoid it directly before or after a footer or auth form, the effect needs breathing room and a reader already engaged with the product.

Used by

  • Stripe, Uses layered, stacked card illustrations that separate to reveal payment flow architecture across its product pages.
  • Linear, Employs scroll-driven layer reveals to showcase the structure of its project management engine.
  • Apple, Mac Pro and iPhone product pages use exploded hardware layer animations to communicate engineering depth.
  • Vercel, Infrastructure diagrams on the platform page separate into distinct layers (Edge Network, Functions, Build) as the user scrolls.

FAQ

Why call useTransform inside the map loop instead of computing values outside?

Each layer needs its own independent motion value, useTransform creates a derived value that Framer Motion subscribes to directly without triggering React re-renders. Calling it per-layer inside the map is intentional; rules-of-hooks violations do not apply here because the layers array length is fixed at render time.

How do I control the spread distance between layers?

The spread is driven by the multiplier in useTransform output arrays: offset * 80 for mid-scroll Y and offset * 120 for end-scroll Y. Increase those numbers to spread layers further apart, decrease them for a subtler separation. The X multipliers (15 and 25) add a slight diagonal that prevents the layers from looking like a flat stack.

Does this work without Framer Motion?

You can replicate the scroll reading with a plain scroll event listener and CSS custom properties, but you lose Framer Motion's optimized rAF scheduling and direct DOM mutation path. The gap matters at 60fps on lower-end devices, stick with Framer Motion or consider the Web Animations API as a lighter alternative.

What is the minimum number of layers that makes the effect readable?

Three layers is the practical minimum. With two, the separation looks like a simple slide rather than a structural reveal. Four is the sweet spot for most products; five works for technical deep-dives but demands careful color differentiation between adjacent layers so they do not merge visually when stacked.

"use client";

import { useRef } from "react";
import { motion, useScroll, useTransform } from "framer-motion";

interface Layer {
  label: string;
  color: string;
}

interface ProductShowcaseExplodedProps {
  badge?: string;
  title?: string;
  description?: string;
  layers?: Layer[];
}

const EASE = [0.16, 1, 0.3, 1] as const;

export default function ProductShowcaseExploded({
  badge = "Technologie",
  title = "Anatomie du produit",

Code complet réservé à Pro

Code source intégral, export multi-framework et playground.

Passer en Pro, 9,99€/mois

Autres variantes product-showcase

Reviews

React Scroll Exploded View, Framer Motion Layers