Card Stack

Layered testimonial cards that cycle forward with a spring stack. Click the front card to advance.

Add to your project

Pick the way you like to build.

Let your AI assistant add it

Copy this prompt and paste it into the AI tool building your site.

# Integrate this UI component

Add this component to my project.

1. Install any missing dependencies.
2. Make sure Tailwind CSS is set up and scans the new file.
3. Prefer passing props over editing the component source — changing the component itself may break it.
4. Adjust colors, size, and layout via props or wrapper classes to match my project. You know my codebase better than this snippet does.

## Component source

```tsx
'use client';

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

type CardStackItem = {
  id?: string;
  name?: string;
  role?: string;
  quote?: string;
  accent?: string;
};

type CardStackProps = {
  items?: CardStackItem[];
  interval?: number;
  className?: string;
};

const DEFAULT_ITEMS: CardStackItem[] = [
  {
    id: 'novak',
    name: 'Amelia Novak',
    role: 'Head of Product, Northline',
    quote: 'The stack feels like a product, not a kit. We shipped the landing page in an afternoon.',
    accent: '#4F46E5',
  },
  {
    id: 'reed',
    name: 'Jonah Reed',
    role: 'Founder, Softsignal',
    quote: 'Quiet motion, clear hierarchy. Customers notice the craft without us having to explain it.',
    accent: '#0D9488',
  },
  {
    id: 'park',
    name: 'Hana Park',
    role: 'Design Lead, Lattice & Co',
    quote: 'Copy, paste, tune. That is the whole workflow. Our team actually enjoys the frontend now.',
    accent: '#C026D3',
  },
];

export default function CardStack({
  items = DEFAULT_ITEMS,
  interval = 4200,
  className = '',
}: CardStackProps) {
  const list = items.length ? items : DEFAULT_ITEMS;
  const [active, setActive] = useState(0);
  const reduceMotion = useReducedMotion();

  useEffect(() => {
    if (list.length < 2 || reduceMotion) return;
    const id = window.setInterval(() => {
      setActive((current) => (current + 1) % list.length);
    }, interval);
    return () => window.clearInterval(id);
  }, [interval, list.length, reduceMotion]);

  function advance() {
    setActive((current) => (current + 1) % list.length);
  }

  return (
    <div
      className={['relative mx-auto h-[280px] w-full max-w-[340px]', className]
        .filter(Boolean)
        .join(' ')}
    >
      {list.map((item, index) => {
        const stackIndex = (index - active + list.length) % list.length;
        if (stackIndex > 2) return null;

        const isFront = stackIndex === 0;

        return (
          <motion.article
            key={item.id ?? `${item.name}-${index}`}
            className={[
              'absolute inset-x-0 top-0 rounded-2xl border border-gray-200 bg-white p-6 shadow-sm',
              isFront ? 'cursor-pointer' : 'pointer-events-none',
            ].join(' ')}
            style={{
              transformOrigin: 'center top',
              zIndex: list.length - stackIndex,
            }}
            animate={{
              y: stackIndex * 14,
              scale: 1 - stackIndex * 0.05,
              rotate: stackIndex === 0 ? 0 : stackIndex % 2 === 0 ? 2.4 : -2.4,
              opacity: 1 - stackIndex * 0.12,
            }}
            transition={
              reduceMotion
                ? { duration: 0 }
                : { type: 'spring', stiffness: 260, damping: 28 }
            }
            onClick={isFront ? advance : undefined}
            tabIndex={isFront ? 0 : -1}
            onKeyDown={
              isFront
                ? (event) => {
                  if (event.key === 'Enter' || event.key === ' ') {
                    event.preventDefault();
                    advance();
                  }
                }
                : undefined
            }
          >
            <span
              className='mb-4 block h-1 w-10 rounded-full'
              style={{ backgroundColor: item.accent ?? '#4F46E5' }}
              aria-hidden
            />
            <p className='text-[15px] leading-6 text-gray-700'>{item.quote}</p>
            <div className='mt-6 flex items-center gap-3'>
              <span
                className='flex h-9 w-9 items-center justify-center rounded-full text-xs font-semibold text-white'
                style={{ backgroundColor: item.accent ?? '#4F46E5' }}
                aria-hidden
              >
                {(item.name ?? 'A')
                  .split(' ')
                  .map((part) => part[0])
                  .slice(0, 2)
                  .join('')}
              </span>
              <div>
                <p className='text-sm font-semibold text-gray-900'>{item.name}</p>
                <p className='text-xs text-gray-500'>{item.role}</p>
              </div>
            </div>
          </motion.article>
        );
      })}
    </div>
  );
}

```

Works with your AI tools

ClaudeClaude*
CodexCodex*
LovableLovable
Base44Base44
Figma MakeFigma Make
ReplitReplit*
CursorCursor*
v0v0

* Compatible with most projects that use the default React/Next.js framework.

Built With

ReactTailwind CSSFramer Motion

Props

PropTypeDescription
itemsCardStackItem[]Cards to cycle: name, role, quote, accent, optional id
intervalnumberAutoplay delay in milliseconds
classNamestringStyles for the outer stack

More Cards Components

Compact Card preview

Editorial card with a full-bleed image, story metadata, and optional link wrapper.

Horizontal Card preview

Side-by-side card with image, title, description, and a clear action.

Metric Card preview

Compact dashboard metric with a trend badge and inline sparkline.