Créer une liste blog minimaliste animée en React
Une liste blog React minimaliste affiche les articles comme des lignes animées via whileInView de Framer Motion avec un délai par élément, produisant un effet d'entrée décalé. Chaque ligne contient une date, un titre avec icône flèche et un badge de tag, tout piloté par des propriétés CSS personnalisées pour s'adapter à n'importe quel thème.
- Stack : React + Framer Motion 11 + lucide-react, ~140 lignes, aucune autre dépendance.
- Animation : whileInView glissement depuis x:-8, délai de 40ms par ligne.
- Mise en page : grille CSS trois colonnes (100px date / 1fr titre / auto tag) pour un alignement précis.
- Entièrement adaptatif au thème via les propriétés CSS personnalisées ; fonctionne en mode clair et sombre.
- Accessible : balises ancre sémantiques, navigable au clavier, texte lisible sans motion.
Blog Minimal List est une section React qui affiche les articles dans une mise en page sobre à dominante typographique, inspirée des changelogs de Notion et Linear. Pas de miniatures, pas de cartes, juste date, titre et tag sur une seule ligne horizontale séparée par une fine bordure. Framer Motion fait glisser chaque ligne depuis la gauche au moment où elle entre dans le viewport, rendant une longue liste légère plutôt que lourde.
Anatomie
La section enveloppe un conteneur centré de 720px. Un bloc d'en-tête (titre h2 + paragraphe de sous-titre) s'anime en premier avec un simple fondu-montée. En dessous, une colonne flex accueille les lignes d'articles. Chaque ligne est un élément motion.a disposé en grille CSS : une colonne de date de 100px en texte discret avec tabular-nums, une colonne titre de 1fr avec une petite icône ArrowUpRight inline, et un badge de tag en largeur auto aligné à droite. Une bordure inférieure de 1px sépare les lignes.
Comment ça marche
Chaque motion.a démarre à opacity:0 et x:-8, puis anime vers opacity:1 et x:0 dès que l'élément entre dans le viewport (viewport: { once: true }). Le délai est i * 0.04 secondes, donc la première ligne apparaît à 0ms, la deuxième à 40ms, la troisième à 80ms, créant une cascade naturelle sans API stagger dédiée. La courbe d'easing [0.16, 1, 0.3, 1] est un bezier cubique rapide-sortie/lente-entrée qui claque tôt et se pose doucement, évitant la sensation flottante de ease-in-out.
Comment le coder en React
Définir le type Article et les props du composant
Commence par une interface TypeScript minimale : title (string), date (string), tag (string). Expose-les via une prop articles optionnelle en complément de sectionTitle et sectionSubtitle. Cela garde le composant purement présentationnel, sans couplage CMS.
interface Article { title: string; date: string; tag: string; }Animer le bloc d'en-tête à l'entrée
Enveloppe le h2 et le sous-titre dans une seule motion.div. Utilise initial={{ opacity: 0, y: 12 }} et whileInView={{ opacity: 1, y: 0 }} avec viewport={{ once: true }}. Une durée de 0.5s avec l'ease personnalisé donne une entrée assurée sans voler l'attention de la liste en dessous.
const EASE = [0.16, 1, 0.3, 1] as const; <motion.div initial={{ opacity: 0, y: 12 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ duration: 0.5, ease: EASE }} >Construire les lignes d'articles décalées
Itère sur articles et rends chaque élément comme un motion.a. Définis initial={{ opacity: 0, x: -8 }} et un délai de transition à i * 0.04. La disposition en grille (gridTemplateColumns: '100px 1fr auto') aligne date, titre et badge de tag sur une même ligne de base pour toutes les lignes, quelle que soit la longueur du titre.
articles.map((article, i) => ( <motion.a key={i} initial={{ opacity: 0, x: -8 }} whileInView={{ opacity: 1, x: 0 }} viewport={{ once: true }} transition={{ duration: 0.35, delay: i * 0.04, ease: EASE }} style={{ display: "grid", gridTemplateColumns: "100px 1fr auto" }} > ... </motion.a> ))Styliser avec des propriétés CSS pour le thème
Utilise var(--color-background), var(--color-foreground), var(--color-foreground-muted), var(--color-accent) et var(--color-border) partout. Ne jamais coder une couleur en dur. Le badge de tag utilise var(--color-accent-border) pour son contour, il correspond donc automatiquement au preset de thème actif sans classe conditionnelle.
Quand l'utiliser
Cette section convient aux sites de documentation, blogs personnels, changelogs de produits ou pages marketing SaaS où le contenu est le produit et où le bruit visuel serait une distraction. Elle se marie naturellement avec une section d'inscription à une newsletter ou un bandeau CTA en dessous. À éviter quand les articles ont un contenu visuel fort (photos, illustrations) qui profiterait d'une mise en page en cartes ou en maçonnerie.
Utilisé par
- Linear, Changelog affiché comme une liste minimaliste date-et-titre, inspiration directe de ce patron en lignes.
- Vercel, Changelog axé sur le texte avec labels de tag et dates dans une mise en page compacte empilée, sans images.
- Rauno Freiberg, Index d'articles personnels sous forme de liste date-et-titre sans aucune décoration, focalisation totale sur le contenu.
- Notion Blog, Tags de catégorie et dates de publication à côté des titres d'articles dans une liste épurée alignée en grille.
FAQ
Comment lier chaque ligne à l'URL réelle de l'article ?
Ajoute un champ url dans l'interface Article, puis remplace le href="#" codé en dur sur motion.a par href={article.url}. Sous Next.js, remplace motion.a par motion(Link) pour conserver le routage côté client.
Peut-on désactiver l'animation pour les utilisateurs qui préfèrent moins de mouvement ?
Oui. Utilise le hook useReducedMotion de Framer Motion et mets initial et animate à un objet vide si la valeur est true. Le composant s'affiche alors instantanément sans transition.
Comment trier les articles par date automatiquement ?
Passe le tableau déjà trié comme prop, le composant ne trie pas lui-même par conception. Trie au niveau de la donnée : articles.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) avant de les passer.
La mise en page tient-elle sur les petits écrans ?
La colonne de date de 100px peut sembler serrée à 320px. Un correctif sûr consiste à masquer la colonne date en dessous de sm avec une media query, ou à replier la grille trois colonnes en une disposition deux lignes empilées sur mobile via une @container ou une classe de breakpoint.