Retour au catalogue

Bento Text Image

Bento asymetrique alternant grands blocs texte et blocs visuels. Layout editorial premium.

bentomedium Both Responsive a11y
editorialelegantminimalagencyportfoliouniversalasymmetric
Theme

Créer une grille bento asymétrique en React

Une grille bento React place des cartes dans un CSS Grid avec des colonnes étendues variables. Framer Motion anime chaque carte en staggered scroll reveal dès qu'elle entre dans le viewport, et un type de carte dual (text ou visual) détermine si une icône Lucide occupe l'arrière-plan.

  • Stack : React 18 + Framer Motion 11 + Lucide React + Tailwind v4, ~90 lignes, zéro dépendance supplémentaire.
  • Grille : 3 colonnes à partir de md, lignes automatiques de 180px, col-span arbitraire via une prop string.
  • Accessible : les cartes utilisent des titres h3 sémantiques et le placeholder icône est décoratif (opacity-20, sans aria label).
  • Thème : toutes les couleurs sont des propriétés CSS custom (--color-background, --color-accent, --color-border), aucune valeur codée en dur.
  • Mobile : passe en une seule colonne sous md, les hauteurs de rangée fixes peuvent nécessiter un ajustement pour les textes longs sur petits écrans.

Bento Text Image est une grille de fonctionnalités éditoriale qui alterne de grandes cartes texte et des cartes visuelles illustrées d'icônes, dans un layout asymétrique à 3 colonnes. Chaque carte s'anime à l'entrée dans le viewport via un staggered reveal Framer Motion, donnant du rythme à la section sans logique de scroll custom. Le résultat est une présentation premium façon magazine, adaptée aux features SaaS, aux capacités d'agence ou aux temps forts d'un portfolio.

Anatomie

La section enveloppe un header centré (badge optionnel, titre h2, sous-titre) et un conteneur CSS Grid. Chaque BentoItem a un id, un titre, une description, un indicateur de type ('text' ou 'visual'), une classe Tailwind de span optionnelle (ex. 'col-span-2') et un nom d'icône Lucide optionnel. Les cartes de type visual affichent l'icône en 64px à 20% d'opacité comme fond décoratif avant le bloc texte. Les cartes text affichent seulement le titre et la description alignés en bas de la carte.

Comment ça marche

Chaque carte est une motion.div avec initial={{ opacity: 0, y: 24 }} et whileInView={{ opacity: 1, y: 0 }}. L'option viewport { once: true, margin: '-40px' } déclenche l'animation 40px avant que la carte soit entièrement visible, évitant un apparition brusque. Un délai de i * 0.1 secondes égrène les cartes séquentiellement sans surcharge d'orchestration. La résolution d'icône utilise une clé dynamique dans l'espace de noms Lucide (LucideIcons[name]) pour que n'importe laquelle des 1400+ icônes soit référençable par chaîne depuis la couche de données.

Comment le coder en React

  1. Définir l'interface BentoItem et le conteneur de grille

    Crée un type BentoItem avec id, title, description, type ('text' | 'visual'), une chaîne span optionnelle et un nom d'icône optionnel. Rends un CSS Grid à 3 colonnes sur écrans medium et des lignes automatiques de 180px. La prop span se mappe directement en classe Tailwind sur chaque carte.

    // types
    interface BentoItem {
      id: string;
      title: string;
      description: string;
      type: "text" | "visual";
      span?: string;   // e.g. "col-span-2 row-span-2"
      icon?: string;   // Lucide icon name
    }
    
    // grid container
    <div className="grid grid-cols-1 md:grid-cols-3 auto-rows-[180px] gap-5">
      {items.map((item, i) => (
        <BentoCard key={item.id} item={item} index={i} />
      ))}
    </div>
  2. Résoudre les icônes Lucide par clé string

    Importe l'espace de noms Lucide en entier et résous le composant icône par nom au moment du rendu. Cela maintient la couche de données libre de tout import React et permet de piloter les icônes depuis un CMS ou un fichier JSON.

    import * as LucideIcons from "lucide-react";
    
    function getIcon(name?: string) {
      if (!name) return null;
      return (LucideIcons as unknown as Record<string, React.ElementType>)[name] || null;
    }
    
    // inside the card
    const Icon = getIcon(item.icon);
    {Icon && <Icon className="h-16 w-16" style={{ color: "var(--color-foreground)" }} />}
  3. Ajouter des scroll reveals en stagger avec Framer Motion

    Enveloppe chaque carte dans une motion.div. Règle le délai sur i * 0.1 pour que les cartes se cascadent de gauche à droite. Le margin: '-40px' sur viewport fait démarrer l'animation légèrement avant que la carte soit complètement visible, pour un rendu fluide plutôt qu'une apparition brusque.

    <motion.div
      initial={{ opacity: 0, y: 24 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, margin: "-40px" }}
      transition={{ delay: i * 0.1, duration: 0.5 }}
      className={`rounded-2xl border p-6 flex flex-col justify-end ${item.span || "col-span-1"}`}
    >
  4. Utiliser les CSS tokens pour le thème

    Applique les couleurs exclusivement via des propriétés CSS custom pour que la grille respecte n'importe quel preset de thème sans modifier le code. Les cartes visuelles utilisent --color-background-alt pour une teinte subtile ; les cartes texte utilisent --color-background-card. Le badge et les bordures suivent --color-accent et --color-border.

    // visual card
    style={{
      backgroundColor: "var(--color-background-alt, var(--color-background-card))",
      borderColor: "var(--color-border)",
    }}
    
    // text card
    style={{
      backgroundColor: "var(--color-background-card)",
      borderColor: "var(--color-border)",
    }}

Quand l'utiliser

Utilise cette grille quand tu dois présenter 3 à 6 fonctionnalités avec des poids visuels variables, certaines nécessitent une longue description, d'autres juste un titre et une indication graphique. Elle convient bien comme bloc 'Ce que nous faisons' pour les agences, une section 'Capacités clés' pour le SaaS, ou un temps fort dans un portfolio. À éviter quand tous les éléments ont une importance égale (préfère une grille de cartes uniforme) et quand les cartes portent des contrôles interactifs nécessitant un dimensionnement homogène.

Utilisé par

  • Linear, Utilise des grilles de fonctionnalités asymétriques avec des tailles de cartes mixtes pour présenter les capacités du produit sur ses pages marketing.
  • Vercel, Déploie des blocs éditoriaux style bento pour mettre en avant des fonctionnalités d'infrastructure avec des densités de cartes texte et visuelles variées.
  • Stripe, Combine des cartes visuelles accentuées d'icônes avec des cartes plein texte dans ses sections de fonctionnalités produit pour équilibrer lisibilité et profondeur.
  • Loom, Utilise des grilles de fonctionnalités à colonnes variées sur sa page d'accueil pour donner plus de place visuelle aux différenciateurs clés qu'aux points secondaires.

FAQ

Comment faire en sorte qu'une carte occupe 2 colonnes ?

Passe une classe Tailwind de span dans le champ span de l'élément, par exemple span: 'col-span-2'. Le composant l'applique directement au classname de la motion.div, donc n'importe quelle classe Tailwind de span de grille fonctionne, y compris les variantes row-span.

Puis-je remplacer l'icône Lucide par une image personnalisée ?

Oui. Remplace la résolution getIcon par une map d'URL d'images et rends un img ou Next.js Image dans le conteneur flex-1. Garde la classe opacity-20 ou supprime-la selon si l'image doit être lue comme décorative ou principale.

Les hauteurs de rangée fixes causent-elles des problèmes de débordement avec les textes longs ?

Cela peut arriver sur mobile, où une seule colonne donne toute la largeur de texte disponible mais la hauteur de rangée de 180px peut couper les descriptions. Augmente la hauteur de rangée de base, passe en auto-rows-auto sur les petits écrans, ou limite les descriptions à 120 caractères dans tes données.

Comment désactiver l'animation de scroll pour les utilisateurs qui préfèrent les mouvements réduits ?

Conditionne les props transition et initial à une vérification de la media query prefers-reduced-motion. Lis-la avec un hook React (useReducedMotion de Framer Motion fonctionne directement) et passe des valeurs statiques quand la préférence est activée.

"use client";

import React from "react";
import { motion } from "framer-motion";
import * as LucideIcons from "lucide-react";

interface BentoItem {
  id: string;
  title: string;
  description: string;
  type: "text" | "visual";
  span?: string;
  icon?: string;
}

interface BentoTextImageProps {
  badge?: string;
  title?: string;
  subtitle?: string;
  items: BentoItem[];
}

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Avis

Grille bento React texte et visuels, Tutoriel