# FAQ Grid Cards — React FAQ Section

> A grid of question cards that flip over in 3D on a spring to show the answer on the back; cards peek on hover and a control flips the whole deck in a wave.

- Category: [FAQs](https://pixelpapa.com/components/faqs.md)
- Tags: faq, grid, cards, flip, 3d
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/faqs/faq-grid-cards

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/faq-grid-cards.json"
```

The file is written to `components/pixelpapa/faq-grid-cards.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { BatteryCharging, MapPin, RefreshCw, ShieldCheck, Truck, Undo2, Wrench } from "lucide-react";
import { motion, useReducedMotion } from "motion/react";
import { useState, type ComponentType } from "react";
import { cn } from "@/lib/utils";

type CardFaq = {
  topic: string;
  icon: ComponentType<{ className?: string }>;
  question: string;
  answer: string;
};

const defaultFaqs: CardFaq[] = [
  {
    topic: "Range",
    icon: BatteryCharging,
    question: "How far does one charge take me?",
    answer:
      "Around 110 km in Eco and 65 km in Boost, measured with an 80 kg rider in mixed city traffic. The battery lifts out, so you can charge it at your desk in 3.5 hours.",
  },
  {
    topic: "Delivery",
    icon: Truck,
    question: "How does the bike arrive?",
    answer:
      "95% assembled, in a reusable box. Straighten the handlebar, fit the pedals with the included tool and you are riding in about ten minutes.",
  },
  {
    topic: "Warranty",
    icon: ShieldCheck,
    question: "What does the warranty cover?",
    answer:
      "Five years on the frame, two on motor, battery and electronics. If something fails, a mobile mechanic comes to you in 40 cities. No shipping the bike back.",
  },
  {
    topic: "Returns",
    icon: Undo2,
    question: "Can I return it after riding?",
    answer:
      "Yes. Ride it for 30 days. If it is not right we collect it for free and refund you in full, even with a few hundred kilometres on the clock.",
  },
  {
    topic: "Service",
    icon: Wrench,
    question: "Who looks after servicing?",
    answer:
      "Any bike shop can handle brakes, tyres and chain, because they are standard parts. Motor and software diagnostics run over the air through the Volta app.",
  },
  {
    topic: "Security",
    icon: MapPin,
    question: "What if it gets stolen?",
    answer:
      "Every Volta has built-in GPS and a motor lock. Report it in the app and our recovery team tracks it. Not found in 14 days? Theft cover replaces it.",
  },
];

type FlipCardProps = {
  faq: CardFaq;
  index: number;
  flipped: boolean;
  /** Seconds to wait before turning, used to ripple the flip-all wave. */
  delay: number;
  onToggle: () => void;
};

export function FlipCard({ faq, index, flipped, delay, onToggle }: FlipCardProps) {
  const reduce = useReducedMotion() ?? false;
  const Icon = faq.icon;
  const face =
    "absolute inset-0 flex flex-col rounded-3xl p-6 [backface-visibility:hidden] [-webkit-backface-visibility:hidden] sm:p-7";

  return (
    <div className="[perspective:1400px]">
      <motion.button
        type="button"
        aria-expanded={flipped}
        onClick={onToggle}
        initial={false}
        animate={{ rotateY: flipped ? 180 : 0 }}
        // A few degrees of turn on hover hints that there is a back side.
        whileHover={reduce ? undefined : { rotateY: flipped ? 170 : 10 }}
        whileTap={reduce ? undefined : { scale: 0.98 }}
        transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 150, damping: 19, mass: 0.9, delay }}
        style={{ transformStyle: "preserve-3d" }}
        className="group relative block h-64 w-full rounded-3xl text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-4 focus-visible:ring-offset-background"
      >
        <span
          aria-hidden={flipped}
          className={cn(face, "border border-border bg-card shadow-sm transition-shadow group-hover:shadow-xl group-hover:shadow-primary/10")}
        >
          <span className="flex items-center justify-between">
            <span className="flex size-11 items-center justify-center rounded-2xl bg-primary/10 text-primary">
              <Icon className="size-5" />
            </span>
            <span className="font-mono text-xs text-muted-foreground">
              {String(index + 1).padStart(2, "0")} / {faq.topic}
            </span>
          </span>
          <span className="mt-auto text-xl leading-snug font-medium tracking-tight text-balance text-card-foreground sm:text-2xl">
            {faq.question}
          </span>
          <span className="mt-4 flex items-center gap-2 text-sm text-muted-foreground">
            <RefreshCw className="size-3.5 transition-transform duration-500 group-hover:rotate-180" />
            Flip for the answer
          </span>
        </span>

        <span
          aria-hidden={!flipped}
          className={cn(face, "overflow-hidden bg-primary text-primary-foreground shadow-xl shadow-primary/25 [transform:rotateY(180deg)]")}
        >
          <span
            aria-hidden
            className="pointer-events-none absolute -top-16 -right-16 size-48 rounded-full bg-primary-foreground/10 blur-2xl"
          />
          <span className="relative flex items-center justify-between font-mono text-xs opacity-75">
            <span>{faq.topic}</span>
            <span>{String(index + 1).padStart(2, "0")}</span>
          </span>
          <span className="relative mt-4 text-sm font-medium opacity-75">{faq.question}</span>
          <span className="relative mt-2 text-base leading-relaxed text-pretty">{faq.answer}</span>
          <span className="relative mt-auto flex items-center gap-2 pt-3 text-sm opacity-75">
            <RefreshCw className="size-3.5" />
            Flip back
          </span>
        </span>
      </motion.button>
    </div>
  );
}

type FaqGridCardsProps = {
  faqs?: CardFaq[];
  /** Indexes that start answer-side up. */
  defaultFlipped?: number[];
};

export default function FaqGridCards({ faqs = defaultFaqs, defaultFlipped = [1] }: FaqGridCardsProps) {
  const [flipped, setFlipped] = useState<boolean[]>(() => faqs.map((_, i) => defaultFlipped.includes(i)));
  const [wave, setWave] = useState(false);
  const reduce = useReducedMotion() ?? false;
  const allFlipped = flipped.every(Boolean);

  function toggle(i: number) {
    setWave(false);
    setFlipped((f) => f.map((v, j) => (j === i ? !v : v)));
  }

  function flipAll() {
    setWave(true);
    setFlipped(faqs.map(() => !allFlipped));
  }

  return (
    <section className="bg-background px-6 py-20 sm:py-24">
      <div className="mx-auto max-w-6xl">
        <div className="flex flex-wrap items-end justify-between gap-6">
          <div className="max-w-xl">
            <p className="text-xs font-medium tracking-[0.2em] text-primary uppercase">Volta One · FAQ</p>
            <h2 className="mt-4 text-4xl font-semibold tracking-tight text-balance text-foreground sm:text-5xl">
              Six questions before you ride
            </h2>
            <p className="mt-4 text-pretty text-muted-foreground">
              Every card has an answer on the back. Turn one over, or flip the whole deck.
            </p>
          </div>
          <button
            type="button"
            onClick={flipAll}
            aria-pressed={allFlipped}
            className="group inline-flex h-11 items-center gap-2 rounded-full border border-border bg-card px-5 text-sm font-medium text-foreground outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
          >
            <RefreshCw className="size-4 text-muted-foreground transition-transform duration-500 group-hover:rotate-180" />
            {allFlipped ? "Show questions" : "Reveal all answers"}
          </button>
        </div>

        <div className="mt-12 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
          {faqs.map((faq, i) => (
            <motion.div
              key={faq.question}
              initial={reduce ? { opacity: 0 } : { opacity: 0, y: 28 }}
              whileInView={{ opacity: 1, y: 0 }}
              viewport={{ once: true }}
              transition={{ duration: 0.55, delay: reduce ? 0 : i * 0.06, ease: [0.22, 1, 0.36, 1] }}
            >
              <FlipCard
                faq={faq}
                index={i}
                flipped={flipped[i] ?? false}
                delay={wave ? i * 0.07 : 0}
                onToggle={() => toggle(i)}
              />
            </motion.div>
          ))}
        </div>
      </div>
    </section>
  );
}
```

## AI prompt

```text
Add the "FAQ Grid Cards" component from PixelPapa UI to this project.

What it is: A grid of question cards that flip over in 3D on a spring to show the answer on the back; cards peek on hover and a control flips the whole deck in a wave.
Category: FAQs. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/faq-grid-cards.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/faq-grid-cards.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/faq-grid-cards" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More faqs

- [FAQ Chat Style](https://pixelpapa.com/components/faqs/faq-chat-style.md) (Pro): A support chat window where tapping a suggested question sends it as a bubble, shows a typing indicator, then types the answer in word by word.
- [FAQ Numbered Editorial](https://pixelpapa.com/components/faqs/faq-numbered-editorial.md): Magazine-style numbered questions in a serif: a rule draws across the open row, the label rolls from Read to Close and a short answer sits beside the long one.
- [FAQ Search Filter](https://pixelpapa.com/components/faqs/faq-search-filter.md) (Pro): Type to filter questions live: matches are highlighted in both question and answer, rows reflow with layout animation and topic chips update their counts.
- [FAQ Smooth Accordion](https://pixelpapa.com/components/faqs/faq-smooth-accordion.md): Single-column accordion whose panels spring open to their natural height while the plus icon folds into a minus and the open card lifts.
- [FAQ Two Column Categories](https://pixelpapa.com/components/faqs/faq-two-column-categories.md) (Pro): Category tabs on the left with a sliding highlight; the question list on the right swaps with a stagger and each row expands in place.
- [FAQ With Contact Card](https://pixelpapa.com/components/faqs/faq-with-contact-card.md) (Pro): Accordion beside a sticky support card; marking an answer as unhelpful makes the card hop, pulse and rewrite itself around that question.
