How to build a quote wall grid in React with staggered animations
A React quote wall lays variable-size cards in a CSS grid and animates each card into view with Framer Motion's whileInView, staggering delays by index and adding a small alternating rotation that settles to zero, no JavaScript masonry library required.
- Stack: React + Framer Motion + Tailwind v4 + lucide-react (Quote icon), ~67 lines total.
- Layout engine: CSS grid with col-span classes (1, 2, or 3 columns), no JS masonry.
- Animation: whileInView with once:true, 0.08s stagger per card, ±2° rotation on enter that settles to 0.
- Fully theme-aware: every color is a CSS custom property (--color-background, --color-accent, --color-border).
- Accessible: semantic <section> and <p> markup; Quote icon is decorative (aria-hidden implicit).
About Quote Wall is a React section that turns a set of founder quotes, company values, or testimonials into a living mosaic. Cards come in three sizes that span different column counts, creating an editorial-magazine feel without any masonry library. Each card tilts slightly on entry then straightens as it settles into place.
Anatomy
The component has two parts. At the top, a centered header with an optional badge (pill with border) and an h2 title, both wrapped in a single whileInView motion.div that fades up. Below, a CSS grid with `grid-cols-1 md:grid-cols-3 lg:grid-cols-6` lays out the quote cards. Each card is a motion.div with a rounded border, a semi-transparent Quote icon at top-left, serif italic body text, and a bottom attribution block (name + optional role) separated by a hairline border.
How it works
Each card initialises with `opacity: 0`, `y: 20`, and a rotation of `+2°` (even index) or `-2°` (odd index). As it enters the viewport, whileInView drives it to `opacity: 1`, `y: 0`, `rotate: 0` over 0.5 s. The stagger is manual: `delay: i * 0.08`, so the 8th card starts 640 ms after the first, reading left-to-right like a reveal. The `once: true` viewport option means the animation fires once per session, keeping scroll performance clean.
How to build it in React
Define the data shape and size map
Create a QuoteItem interface with id, text, author, optional role, and an optional size ('small' | 'medium' | 'large'). Map sizes to Tailwind col-span classes and text-size classes up front so the render loop stays simple.
const sizeClasses = { small: "col-span-1", medium: "col-span-1 md:col-span-2", large: "col-span-1 md:col-span-2 lg:col-span-3", };Build the CSS grid container
Use `grid-cols-1 md:grid-cols-3 lg:grid-cols-6` with `auto-rows-auto` so cards only take the height they need. Six columns at large breakpoint lets a 'large' card fill half the row, a 'medium' card a third, and a 'small' card a sixth.
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-4 auto-rows-auto">Animate each card with rotation stagger
Wrap each card in a motion.div. Set the initial rotation to `(i % 2 === 0 ? 1 : -1) * 2` degrees so adjacent cards lean in opposite directions on entry. Drive everything to zero in whileInView with a per-card delay.
<motion.div initial={{ opacity: 0, y: 20, rotate: (i % 2 === 0 ? 1 : -1) * 2 }} whileInView={{ opacity: 1, y: 0, rotate: 0 }} viewport={{ once: true }} transition={{ delay: i * 0.08, duration: 0.5 }} >Compose the card interior
Place the Quote icon (lucide-react) at 50% opacity as a decorative marker, then the quote text in a serif italic paragraph, then an attribution block separated by a border-t. All colors reference CSS custom properties so the card inherits any active theme preset.
<Quote className="h-5 w-5 mb-3" style={{ color: "var(--color-accent)", opacity: 0.5 }} /> <p className="font-serif italic">“{quote.text}”</p> <div className="mt-4 pt-3 border-t" style={{ borderColor: "var(--color-border)" }}> <p className="text-xs font-semibold">{quote.author}</p> </div>
When to use it
This section works well as a culture or values block on an agency or portfolio about page, a startup's 'what we believe' section, or a product page where social proof comes from founders rather than users. Pair it after a narrative about paragraph. Skip it for high-volume testimonial pages (20+ reviews) where a carousel or paginated list fits better, and for conversion-critical pages where focused copy matters more than a mosaic.
Used by
- Notion, Uses a wall of founder and team quotes on its about page to convey company philosophy.
- Basecamp, Presents company values and principles as standalone quote-style blocks in a dispersed editorial layout.
- Mailchimp, Mixes pull-quotes and founder statements in a mosaic grid on its culture and about pages.
FAQ
How do I control which cards are wide?
Set size: 'medium' or size: 'large' on individual QuoteItem objects in your data array. Medium cards span 2 of 3 columns at md breakpoint; large cards span 3 of 6 at lg. Small cards always take 1 column.
Can I use this for customer testimonials instead of internal quotes?
Yes, the data shape is identical, swap the author/role values for customer name and company title. For social proof at scale (50+ testimonials), filter down to 5-8 highlights before passing them in; the grid does not paginate.
Does the rotation stagger work on mobile?
The animation itself runs fine on mobile. The grid collapses to a single column, so all size variants stack full-width. The rotation and stagger are still visible as the user scrolls down.
How do I change the font to match my brand?
The quote text uses Tailwind's font-serif utility. Override the serif stack globally via your Tailwind config or the @font-face rule for your chosen typeface, no changes needed inside the component.