How to build a minimal animated blog list in React
A minimal React blog list renders articles as animated rows using Framer Motion's whileInView with a per-item delay, producing a staggered slide-in effect. Each row holds a date, a title with an arrow icon, and a tag badge, all driven by CSS custom properties so the component adapts to any theme.
- Stack: React + Framer Motion 11 + lucide-react, ~140 lines, no other dependencies.
- Animation: whileInView slide-in from x:-8, stagger delay of 40ms per row.
- Layout: CSS grid with three columns (100px date / 1fr title / auto tag) for pixel-precise alignment.
- Fully theme-aware via CSS custom properties; works in light and dark mode out of the box.
- Accessible: semantic anchor tags, tabIndex-navigable, readable text without motion.
Blog Minimal List is a React section that displays articles in a clean, type-driven row layout inspired by Notion and Linear's changelogs. There are no thumbnails, no cards, just date, title and tag on a single horizontal line separated by a thin border. Framer Motion slides each row in from the left as it enters the viewport, making a long list feel light rather than heavy.
Anatomy
The section wraps a centred 720px container. A header block (h2 title + subtitle paragraph) animates in first with a single fade-and-rise. Below it, a flex column holds the article rows. Each row is a motion.a element laid out as a CSS grid: a 100px date column in muted text with tabular-nums, a 1fr title column with a small ArrowUpRight icon inline, and an auto-width tag badge pinned to the right. A 1px border-bottom separates rows.
How it works
Each motion.a starts with opacity:0 and x:-8, then animates to opacity:1 and x:0 once the element enters the viewport (viewport: { once: true }). The delay is i * 0.04 seconds, so the first row appears at 0ms, the second at 40ms, the third at 80ms, giving a natural cascade without a dedicated stagger API. The easing curve [0.16, 1, 0.3, 1] is a fast-out-slow-in cubic bezier that snaps early and settles gently, avoiding the floaty feel of ease-in-out.
How to build it in React
Define the Article type and component props
Start with a minimal TypeScript interface: title (string), date (string), tag (string). Expose them as an optional articles array prop alongside sectionTitle and sectionSubtitle. This keeps the component purely presentational with no CMS coupling.
interface Article { title: string; date: string; tag: string; }Animate the header block into view
Wrap the h2 and subtitle in a single motion.div. Use initial={{ opacity: 0, y: 12 }} and whileInView={{ opacity: 1, y: 0 }} with viewport={{ once: true }}. A duration of 0.5s with the custom ease gives a confident entrance without hogging attention from the list below.
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 }} >Build the staggered article rows
Map over articles and render each as a motion.a. Set initial={{ opacity: 0, x: -8 }} and transition delay to i * 0.04. The grid layout (gridTemplateColumns: '100px 1fr auto') aligns the date, title and tag badge on a single baseline across all rows, regardless of title length.
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> ))Style with CSS custom properties for theming
Use var(--color-background), var(--color-foreground), var(--color-foreground-muted), var(--color-accent) and var(--color-border) throughout. Never hardcode a colour. The tag badge uses var(--color-accent-border) for its outline, so it automatically matches the active theme preset without any conditional class.
When to use it
Reach for this section on documentation sites, personal blogs, product changelogs or SaaS marketing pages where content is the product and visual noise would distract. It pairs naturally with a newsletter sign-up section or a CTA banner below. Avoid it when articles have strong visual content (photos, illustrations) that would benefit from a card or masonry layout instead.
Used by
- Linear, Changelog rendered as a minimal date-and-title list, the direct inspiration for this row-based pattern.
- Vercel, Text-first changelog with tag labels and dates in a compact stacked layout, no images.
- Rauno Freiberg, Personal writing index as a plain date-and-title list, zero decoration, full focus on the content.
- Notion Blog, Category tags and publication dates beside article titles in a clean, grid-aligned list.
FAQ
How do I link each row to its real article URL?
Add a url field to the Article interface, then replace the hardcoded href="#" on motion.a with href={article.url}. If you use Next.js, swap motion.a for a motion(Link) to keep client-side routing.
Can I disable the animation for users who prefer reduced motion?
Yes. Read the useReducedMotion hook from Framer Motion and conditionally set initial and animate to an empty object when it returns true. The component then renders instantly without any transition.
How do I sort articles by date automatically?
Pass the sorted array as a prop, the component does no sorting itself by design. Sort at the data layer: articles.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) before passing them in.
Does the layout hold on narrow screens?
The 100px date column can feel tight at 320px. A safe fix is to hide the date column below sm with a media query or to collapse the three-column grid to a two-row stacked layout on mobile using a @container or breakpoint class.