Skip to content
FreeCards

Card Stack

Use this component for testimonials, feature highlights, quotes, product tips, or small case-study snippets that should rotate without a heavy carousel.

ReactTailwind CSSMotionCards

I don't like messy handoffs, so we ship clean docs with every sprint.

Elena Moss

Product Founder

The component library helped us prototype three landing pages in one afternoon.

Ravi Singh

Growth Lead

Small UI details made the product feel much more premium than our first draft.

Maya Chen

Design Partner

Overview

An auto-rotating testimonial card stack that cycles content on an interval.

Framework

React

Styling

Tailwind CSS

Animation

Motion

Create a Vite React project

bash
npm create vite@latest my-ui-app -- --template react-ts
cd my-ui-app
npm install

Create a Next.js project

bash
npx create-next-app@latest my-ui-app --typescript --tailwind --eslint
cd my-ui-app

Add Tailwind CSS to Vite

bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Install dependencies

bash
npm install framer-motion

Usage

tsx
<CardStack />

Card Stack.tsx

tsx
"use client";

import { useEffect, useState } from "react";
import { motion } from "framer-motion";

const cards = [
  {
    quote: "I don't like messy handoffs, so we ship clean docs with every sprint.",
    name: "Elena Moss",
    role: "Product Founder",
  },
  {
    quote: "The component library helped us prototype three landing pages in one afternoon.",
    name: "Ravi Singh",
    role: "Growth Lead",
  },
  {
    quote: "Small UI details made the product feel much more premium than our first draft.",
    name: "Maya Chen",
    role: "Design Partner",
  },
];

export function CardStack() {
  const [active, setActive] = useState(0);

  useEffect(() => {
    const timer = window.setInterval(
      () => setActive((current) => (current + 1) % cards.length),
      1800,
    );
    return () => window.clearInterval(timer);
  }, []);

  const orderedCards = [cards[active], ...cards.filter((_, index) => index !== active)];

  return (
    <div className="relative h-48 w-full max-w-md">
      {orderedCards.map((card, index) => (
        <motion.div
          key={card.name}
          animate={{ y: index * 10, scale: 1 - index * 0.04 }}
          transition={{ duration: 0.4 }}
          className="absolute inset-x-0 rounded-xl border border-slate-200 bg-white p-5 text-slate-900 shadow-2xl shadow-black/20"
          style={{ zIndex: orderedCards.length - index }}
        >
          <p className="text-sm leading-relaxed">{card.quote}</p>
          <p className="mt-6 text-sm font-bold">{card.name}</p>
          <p className="text-xs text-slate-500">{card.role}</p>
        </motion.div>
      ))}
    </div>
  );
}