Retour au catalogue

Hero Elastic Columns

Grille 3 colonnes de cards portrait avec effet parallax décalé au scroll, colonne gauche remonte, droite descend, centre fixe. Titre centré superposé avec vignette radiale. Idéal portfolio, agence, SaaS visuellement ambitieux.

heromedium Both Responsive a11y
elegantboldeditorialagencyportfoliosaasgrid
Theme

How to build an elastic parallax columns hero in React

An elastic parallax columns hero in React places three card columns behind a centered headline and shifts the left column upward and the right column downward as the user scrolls, using Framer Motion's useScroll and useTransform to map scroll progress to vertical offsets. The center column stays fixed while a radial vignette fades the columns into the background, keeping the foreground text readable.

  • Stack: React + Framer Motion 11 + Lucide React, ~310 lines, no extra dependencies.
  • Core API: useScroll (target + offset), useTransform for per-column y values, staggered mount animations.
  • Left column shifts from -6% to 0%, right column from +6% to 0% as the section scrolls out of view.
  • Accessible: card labels are real text elements, decorative layers carry aria-hidden.
  • The parallax effect is scroll-driven and works on touch devices; no pointer required.

This React hero component places three columns of portrait cards in the background and animates them at different scroll speeds, creating an elastic depth illusion behind a centered headline. The left column eases up, the right column eases down, and the center holds steady. A radial vignette handles contrast so the CTA stays clean regardless of card content.

Anatomy

The section has four visual layers stacked absolutely inside a min-height:100vh container. At the back sits a 60px CSS grid pattern masked by a radial gradient to fade toward the edges. Behind the grid, a blurred accent ellipse creates a soft glow. The column grid (CSS grid, three equal columns, max-width 900px, centered) comes next; each column is a Framer Motion div driven by its own y transform. A radial vignette overlay (transparent center, solid background at the edges) fades the columns out. On top, the foreground content sits in a flex-centered div: badge, h1 with a gradient accent span, description paragraph, and a CTA anchor.

How it works

Framer Motion's useScroll tracks scroll progress on the section element with offset ['start start', 'end start'], so the values run from 0 (section top at viewport top) to 1 (section bottom at viewport top). useTransform maps that 0-to-1 range to vertical offsets: the left column goes from -6% to 0% (it starts higher and settles), the right column from 6% to 0% (starts lower and rises), and the center stays at 0% throughout. These MotionValues are passed as the y style prop to the Column components. On mount, each card inside a column fades in with a staggered delay (i * 0.09s per card, plus a per-column offset of 0, 0.06, 0.12s) using the custom EASE cubic-bezier [0.16, 1, 0.3, 1].

How to build it in React

  1. Set up scroll tracking on the section

    Attach a ref to the section element and pass it to useScroll with the offset pair that covers the section entering and leaving the viewport. This gives a scrollYProgress value from 0 to 1 you can pipe into transforms.

    const sectionRef = useRef<HTMLElement>(null);
    const { scrollYProgress } = useScroll({
      target: sectionRef,
      offset: ["start start", "end start"],
    });
  2. Derive per-column y offsets with useTransform

    Create three transform values from the same scrollYProgress. The outer columns get opposing ranges so they converge toward 0% as the user scrolls, producing the elastic pinch effect. The center stays constant.

    const yLeft  = useTransform(scrollYProgress, [0, 1], ["-6%", "0%"]);
    const yMid   = useTransform(scrollYProgress, [0, 1], ["0%",  "0%"]);
    const yRight = useTransform(scrollYProgress, [0, 1], ["6%",  "0%"]);
  3. Build the Column component

    Wrap the column in a motion.div that receives the y MotionValue as a style prop. Each card inside animates from opacity 0 / scale 0.94 to full opacity on mount, with a staggered delay based on the card index and a per-column offset.

    <motion.div style={{ y }}>
      {cards.map((card, i) => (
        <motion.div
          key={i}
          initial={{ opacity: 0, scale: 0.94 }}
          animate={{ opacity: 1, scale: 1 }}
          transition={{ duration: 0.6, delay: delay + i * 0.09, ease: EASE }}
        />
      ))}
    </motion.div>
  4. Layer the vignette and foreground content

    Place the column grid in absolute position behind everything, then add a radial-gradient overlay div (transparent center, opaque background toward edges) to fade the cards out. The headline, description, and CTA go in a relative z-index:10 container so they always sit above the parallax layer.

When to use it

Use this hero on agency portfolios, design tool landings, and visually ambitious SaaS homepages where you want depth without a video or heavy asset. The scroll-driven animation rewards users who engage but costs nothing on first paint. Skip it on e-commerce or transactional pages where the primary goal is a form or a buy button, the background movement competes for attention. Also avoid it if your card content is complex images rather than simple text labels, since the vignette won't fully mask busy visuals.

Used by

  • Stripe, Uses overlapping card grids with subtle depth offsets in hero sections across its product marketing pages.
  • Framer, Showcases parallax column layouts as a core template pattern in its website builder and on its own marketing site.
  • Lottiefiles, Deploys a multi-column card grid with scroll-driven vertical offsets in its hero, giving the impression of a living asset library.
  • Webflow, Regularly uses staggered card columns at different scroll speeds to demonstrate design depth on landing pages.

FAQ

Why does the left column start at -6% and move to 0% instead of the other way around?

The offset ['start start', 'end start'] means scrollYProgress is 0 when the section top aligns with the viewport top. At that moment you want the columns spread apart (-6%/+6%), so they converge toward 0% as the user scrolls down and the section exits, that's the elastic pinch.

Does this parallax effect hurt performance on mobile?

No, Framer Motion applies the y transforms via the Web Animations API using the compositor thread, so there is no layout reflow. The effect is scroll-position-driven with no JavaScript running per frame.

How do I add a fourth column without breaking the parallax symmetry?

Add a yFar value with a larger range (e.g. [-10%, 0%]) for the outer column and shift the existing columns inward. Keep opposite signs on the two outermost columns so the convergence reads correctly. Update the CSS grid from three to four equal columns.

Can I replace the scroll-based parallax with a mouse-hover parallax?

Yes. Replace useScroll/useTransform with useMotionValue for mouseX/mouseY, then map those values to column offsets using useTransform with a smaller range. Add a useSpring on top for inertia. The Column component stays identical, only the source of the y value changes.

"use client";

import { useRef } from "react";
import { motion, useScroll, useTransform, type MotionValue } from "framer-motion";
import { ArrowRight } from "lucide-react";

interface Card {
  label: string;
  accent?: boolean;
}

interface HeroElasticColumnsProps {
  title?: string;
  titleAccent?: string;
  description?: string;
  ctaLabel?: string;
  ctaUrl?: string;
  badge?: string;
  col1Cards?: Card[];
  col2Cards?: Card[];
  col3Cards?: Card[];
}

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Reviews

React Parallax Columns Hero with Framer Motion, Tutorial