Retour au catalogue

Bento Scroll Reveal

Bento grid asymetrique en 6 cellules avec animations d'entree differenciees au scroll, scale, x, y selon la position. Hover lift avec accent border. Contenu mixte : stat, chart barres, tag cloud, quote.

bentomedium Both Responsive a11y
minimalboldeditorialsaasagencyportfoliouniversalgridasymmetric
Theme

How to build an animated bento grid in React with scroll reveal

An animated bento grid in React places 6 cards in an asymmetric CSS grid and triggers each card's entrance animation independently as it scrolls into view with Framer Motion's whileInView. Each cell gets a different initial state (scale, x, or y offset) and a staggered delay so the grid assembles progressively rather than all at once.

  • Stack: React + Framer Motion 11 + CSS custom properties, ~140 lines in BentoScrollReveal.tsx plus ~145 lines in BentoCells.tsx.
  • Animation API: motion.div with whileInView/viewport (once: true), per-cell CELL_ANIMATIONS array, stagger via delay: i * 0.07.
  • Six distinct cell types: stat, chart (animated bar chart), tags, highlight, quote, feature, all driven by a typed BentoCell interface.
  • Hover state: whileHover lifts each card 4px and swaps border color to --color-accent in 200ms.
  • Mobile: grid collapses to 2-column via an injected @media rule; content is plain semantic HTML, no extra aria roles needed.

Bento Scroll Reveal is an asymmetric 6-cell feature grid where every card enters the viewport with its own directional animation: scale, slide from the left, from the right, or from below. The staggered timing turns what could be a static information dump into something that feels alive. It works for SaaS feature sections, portfolio case studies, or any place where you need to surface a mix of metrics, social proof, and copy in one compact block.

Anatomy

The component is a two-part structure. A centered header block (badge, h2, subtitle) animates in first as a single unit via whileInView. Below it, a CSS grid with gridTemplateAreas defines three rows: the first splits 4+2 columns, the second splits 2+2+2, and the third splits 3+3, giving each of the six named areas (a through f) a distinct footprint. Each cell is a motion.div that maps to one area. The cell's inner content is delegated to BentoCells.tsx, which renders the appropriate sub-component based on the cell type.

How it works

A static CELL_ANIMATIONS array holds six preset initial/animate pairs, one per grid position: cell 0 scales from 0.92, cells 1 and 5 slide from x: 30, cell 2 slides from x: -30, and cells 3 and 4 slide from y: 30. Each motion.div reads from this array by index and passes both states directly to whileInView. The viewport prop uses once: true so animations fire once and stay put. Delay is computed as i * 0.07 seconds, so the six cards stagger across roughly 420ms total. The easing curve [0.16, 1, 0.3, 1] is a custom cubic-bezier that accelerates fast and decelerates sharply, giving each entry a physical snap. Inside chart cells, MiniChart bars get their own nested whileInView with additional stagger (delay: 0.4 + i * 0.06), so they grow after the card itself has settled.

How to build it in React

  1. Define the asymmetric grid with gridTemplateAreas

    Use a 6-column CSS grid and assign named areas. Three rows give you a 4-2, 2-2-2, and 3-3 split. Each motion.div gets a style={{ gridArea: area }} where area is one of the letters a to f.

    gridTemplateColumns: "repeat(6, 1fr)",
    gridTemplateAreas: `
      "a a a a b b"
      "c c d d b b"
      "e e e f f f"
    `,
  2. Build the CELL_ANIMATIONS lookup table

    Declare the array outside the component so it is never recreated on render. Each entry holds initial and animate objects keyed by position. The variety of directions (scale, x, y) is what gives the grid its visual rhythm as it assembles.

    const CELL_ANIMATIONS = [
      { initial: { opacity: 0, scale: 0.92 }, animate: { opacity: 1, scale: 1 } },
      { initial: { opacity: 0, x: 30 },       animate: { opacity: 1, x: 0 } },
      { initial: { opacity: 0, x: -30 },      animate: { opacity: 1, x: 0 } },
      { initial: { opacity: 0, y: 30 },       animate: { opacity: 1, y: 0 } },
    ];
  3. Wire whileInView with staggered delays

    Map over cells, read the animation config by index, and compute the delay as i * 0.07. Pass viewport={{ once: true, margin: '-50px' }} so each card triggers slightly before it fully enters the screen, keeping the reveal snappy rather than delayed.

    <motion.div
      key={cell.id}
      initial={anim.initial}
      whileInView={anim.animate}
      viewport={{ once: true, margin: "-50px" }}
      transition={{ delay: i * 0.07, duration: 0.65, ease: [0.16, 1, 0.3, 1] }}
      whileHover={{ y: -4, borderColor: "var(--color-accent)",
        transition: { duration: 0.2, ease: "easeOut" } }}
      style={{ gridArea: area }}
    >
  4. Use CSS tokens for theming, never hardcoded colors

    Every color reference uses var(--color-background-card), var(--color-accent), var(--color-border), and var(--color-foreground-muted). This lets the component work across all 7 theme presets without a prop change. MiniChart tints bars with color-mix(in srgb, var(--color-accent) N%, transparent) to derive lighter shades from the same token.

When to use it

Reach for this component in the middle of a landing page, after the hero and before the CTA, when you need to surface a mix of numbers, social proof, and feature copy in one glanceable section. It suits SaaS dashboards, agency portfolios, and product marketing pages. Avoid it when cells share too similar content types (six identical feature cards, for instance), because the asymmetric layout needs visual contrast between cells to justify itself. Also skip it if you need more than six cells; past that count the stagger loses its rhythm.

Used by

  • Stripe, Uses mixed-content bento grids on product pages to surface metrics, code snippets, and feature highlights side by side.
  • Vercel, Asymmetric feature grids with staggered reveals appear throughout the platform's marketing pages.
  • Linear, Bento-style layouts combining stat cards, feature previews, and short testimonials in a single grid section.
  • Loom, Mixed-size cards on the homepage show usage numbers, team features, and social proof in a single scannable block.

FAQ

Why use Framer Motion's whileInView instead of CSS scroll animations?

CSS scroll animations require an IntersectionObserver to toggle a class, then you manage stagger delays in CSS or JS separately. Framer Motion's whileInView handles the observer, the initial/animate states, and the per-element delay in one prop set. Less boilerplate, and changing a single cell's direction or timing does not touch a stylesheet.

How do I change which cell spans how many columns?

Edit the gridTemplateAreas string and the matching GRID_AREAS array. Each letter in the string corresponds to a cell by index. Keep every area rectangular and keep the letters in GRID_AREAS in the same order as the cells array.

Can I add more than 6 cells?

The component slices cells to 6 with cells.slice(0, 6) and the gridTemplateAreas is hardcoded for exactly six named areas. To add more cells, extend both the areas string and the CELL_ANIMATIONS array. Past 8 cells the accumulated stagger delay starts to feel slow on initial load.

Does the scroll animation replay when scrolling back up?

No. viewport={{ once: true }} means each cell animates the first time it crosses the margin threshold and stays in its final state. Remove the once flag if you want cards to reset and re-animate on every scroll cycle, though that tends to feel excessive in a feature section.

"use client";

import React from "react";
import { motion } from "framer-motion";
import { CellContent, type BentoCell } from "./BentoCells";

interface BentoScrollRevealProps {
  badge?: string;
  title?: string;
  subtitle?: string;
  cells: BentoCell[];
}

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

const CELL_ANIMATIONS = [
  { initial: { opacity: 0, scale: 0.92 }, animate: { opacity: 1, scale: 1 } },
  { initial: { opacity: 0, x: 30 },       animate: { opacity: 1, x: 0 } },
  { initial: { opacity: 0, x: -30 },      animate: { opacity: 1, x: 0 } },
  { initial: { opacity: 0, y: 30 },       animate: { opacity: 1, y: 0 } },
  { initial: { opacity: 0, y: 30 },       animate: { opacity: 1, y: 0 } },
  { initial: { opacity: 0, x: 30 },       animate: { opacity: 1, x: 0 } },

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Reviews

React Bento Grid with Scroll Animations, Framer Motion