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

Créer une bento grid React animée avec révélation au scroll

Une bento grid animée en React place 6 cartes dans une grille CSS asymétrique et déclenche l'animation d'entrée de chaque carte séparément au scroll via whileInView de Framer Motion. Chaque cellule reçoit un état initial différent (scale, décalage x ou y) et un délai échelonné, pour que la grille s'assemble progressivement plutôt que d'un coup.

  • Stack : React + Framer Motion 11 + CSS custom properties, ~140 lignes dans BentoScrollReveal.tsx et ~145 lignes dans BentoCells.tsx.
  • API animation : motion.div avec whileInView/viewport (once: true), tableau CELL_ANIMATIONS par cellule, stagger via delay: i * 0.07.
  • Six types de cellules distincts : stat, chart (graphique à barres animé), tags, highlight, quote, feature, tous pilotés par une interface BentoCell typée.
  • État hover : whileHover monte chaque carte de 4px et passe la bordure en --color-accent en 200ms.
  • Mobile : la grille passe en 2 colonnes via une règle @media injectée ; le contenu est du HTML sémantique simple, pas de aria roles supplémentaires nécessaires.

Bento Scroll Reveal est une grille de 6 cartes asymétriques où chacune entre dans le viewport avec sa propre animation directionnelle : scale, glissement depuis la gauche, la droite ou le bas. Le timing échelonné transforme ce qui pourrait être un bloc d'informations statique en quelque chose de vivant. Ça fonctionne pour les sections features SaaS, les études de cas portfolio, ou partout où tu dois présenter un mix de métriques, de preuve sociale et de texte dans un bloc compact.

Anatomie

Le composant est une structure en deux parties. Un bloc d'en-tête centré (badge, h2, sous-titre) s'anime en premier comme une seule unité via whileInView. En dessous, une grille CSS avec gridTemplateAreas définit trois lignes : la première divise en 4+2 colonnes, la deuxième en 2+2+2, la troisième en 3+3, donnant à chacune des six zones nommées (a à f) une empreinte distincte. Chaque cellule est une motion.div qui correspond à une zone. Le contenu interne est délégué à BentoCells.tsx, qui rend le sous-composant approprié selon le type de cellule.

Comment ça marche

Un tableau statique CELL_ANIMATIONS contient six paires initial/animate prédéfinies, une par position dans la grille : la cellule 0 scale depuis 0.92, les cellules 1 et 5 glissent depuis x: 30, la cellule 2 depuis x: -30, et les cellules 3 et 4 depuis y: 30. Chaque motion.div lit dans ce tableau par index et passe les deux états directement à whileInView. La prop viewport utilise once: true pour que les animations se déclenchent une seule fois. Le délai est calculé à i * 0.07s, les six cartes s'étalant sur environ 420ms au total. La courbe d'easing [0.16, 1, 0.3, 1] est un cubic-bezier custom qui accélère vite et décélère nettement, donnant à chaque entrée un snap physique. Dans les cellules chart, les barres MiniChart ont leur propre whileInView imbriqué avec un stagger supplémentaire (delay: 0.4 + i * 0.06), pour qu'elles poussent après que la carte elle-même se soit stabilisée.

Comment le coder en React

  1. Définir la grille asymétrique avec gridTemplateAreas

    Utilise une grille CSS à 6 colonnes et assigne des zones nommées. Trois lignes donnent un découpage 4-2, 2-2-2 et 3-3. Chaque motion.div reçoit un style={{ gridArea: area }} où area est une des lettres a à 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. Construire le tableau CELL_ANIMATIONS

    Déclare le tableau en dehors du composant pour éviter qu'il soit recréé à chaque rendu. Chaque entrée contient des objets initial et animate indexés par position. La variété des directions (scale, x, y) est ce qui donne à la grille son rythme visuel lors de l'assemblage.

    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. Brancher whileInView avec des délais échelonnés

    Itère sur les cellules, lis la config d'animation par index, et calcule le délai à i * 0.07. Passe viewport={{ once: true, margin: '-50px' }} pour que chaque carte se déclenche légèrement avant d'être entièrement visible, ce qui rend la révélation vive plutôt que tardive.

    <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. Utiliser les tokens CSS pour le thème, jamais de couleurs en dur

    Chaque référence de couleur utilise var(--color-background-card), var(--color-accent), var(--color-border) et var(--color-foreground-muted). Le composant fonctionne ainsi sur les 7 presets de thème sans changer une seule prop. MiniChart teinte les barres avec color-mix(in srgb, var(--color-accent) N%, transparent) pour dériver des teintes plus légères depuis le même token.

Quand l'utiliser

Utilise ce composant au milieu d'une landing page, après le hero et avant le CTA, quand tu dois afficher un mix de chiffres, de preuve sociale et de texte features dans une section lisible d'un coup d'oeil. Il convient aux dashboards SaaS, portfolios d'agences et pages de marketing produit. À éviter quand les cellules partagent des types de contenu trop similaires (six cartes de features identiques, par exemple), car la mise en page asymétrique a besoin de contraste visuel entre les cellules pour se justifier. Passe également outre si tu as besoin de plus de six cellules ; au-delà de ce nombre, le stagger perd son rythme.

Utilisé par

  • Stripe, Utilise des grilles bento à contenu mixte sur ses pages produit pour présenter métriques, extraits de code et highlights features côte à côte.
  • Vercel, Des grilles de features asymétriques avec reveals décalés apparaissent tout au long des pages marketing de la plateforme.
  • Linear, Layouts de type bento combinant cartes de stats, aperçus de features et courtes testimonials dans une seule section grille.
  • Loom, Des cartes de tailles variées sur la homepage présentent chiffres d'usage, features équipe et preuve sociale dans un seul bloc lisible d'un coup d'oeil.

FAQ

Pourquoi utiliser le whileInView de Framer Motion plutôt que des animations CSS au scroll ?

Les animations CSS au scroll nécessitent un IntersectionObserver pour basculer une classe, puis tu gères les délais de stagger en CSS ou JS séparément. Le whileInView de Framer Motion gère l'observer, les états initial/animate et le délai par élément dans un seul jeu de props. Moins de boilerplate, et modifier la direction ou le timing d'une cellule ne touche pas une feuille de style.

Comment modifier l'empan de colonnes de chaque cellule ?

Modifie la chaîne gridTemplateAreas et le tableau GRID_AREAS correspondant. Chaque lettre dans la chaîne correspond à une cellule par index. Assure-toi que chaque zone est rectangulaire et que les lettres dans GRID_AREAS restent dans le même ordre que le tableau de cellules.

Puis-je ajouter plus de 6 cellules ?

Le composant tronque le tableau à 6 avec cells.slice(0, 6) et le gridTemplateAreas est codé en dur pour exactement six zones nommées. Pour ajouter des cellules, étends à la fois la chaîne areas et le tableau CELL_ANIMATIONS. Au-delà de 8 cellules, le délai de stagger accumulé commence à paraître lent au chargement initial.

L'animation au scroll se rejoue-t-elle quand on remonte ?

Non. viewport={{ once: true }} signifie que chaque cellule s'anime la première fois qu'elle franchit le seuil de marge et reste dans son état final. Retire le flag once si tu veux que les cartes se réinitialisent et rejouent à chaque cycle de scroll, même si c'est souvent excessif dans une section features.

"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

Avis

Bento grid React avec animations au scroll, Framer Motion