Installation
Usage
import { FAQChatAccordion } from "@/components/ruixen/faq-chat-accordion";
import type { FAQItem } from "@/components/ruixen/faq-chat-accordion";
const items: FAQItem[] = [
{
question: "How do I get started?",
answer: "Sign up for a free account and follow the onboarding guide.",
},
{
question: "Is there a free plan?",
answer: "Yes, we offer a generous free tier for individual developers.",
},
];
export default function MyFAQ() {
return <FAQChatAccordion items={items} />;
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | "Have questions?" | Section heading |
items | FAQItem[] | Built-in 5 defaults | Array of FAQ items |
typingDelay | number | 400 | Milliseconds to show typing dots before answer. 0 to skip |
className | string | — | Additional classes on root section |
FAQItem
interface FAQItem {
question: string;
answer: string;
}Conversational Layout
Questions are right-aligned bubbles (bg-foreground text-background, rounded with a 6px bottom-right tail). Answers are left-aligned bubbles (bg-muted, rounded with a 6px bottom-left tail). This alternating alignment creates the iMessage / WhatsApp conversation pattern — you "sent" the question, the system "replied."
A Today date separator sits between the header and the messages, reinforcing the chat metaphor.
CSS Grid Height Architecture
All height transitions use CSS grid-template-rows (0fr ↔ 1fr). Unlike transform-based animations, CSS grid changes actual DOM height — siblings follow the transition naturally without FLIP tricks or layout springs. Zero jank. Zero double-shift.
┌─ Outer grid (0fr ↔ 1fr) ─── bubble enter/exit
│ └─ motion.div (y + opacity) ─── spring entrance feel
│ └─ Bubble container (bg-muted, rounded)
│ ├─ Inner grid A (dots: 1fr ↔ 0fr) ─── dots collapse
│ └─ Inner grid B (text: 0fr ↔ 1fr) ─── text expand
└─ Siblings follow naturally (real height, not transforms)
Outer grid — bubble enter/exit
The answer area is always in the DOM. A CSS grid with grid-template-rows: 0fr keeps it collapsed. When active, it transitions to 1fr over 400ms with cubic-bezier(0.22, 1, 0.36, 1) (strong expo). Exit is 250ms with cubic-bezier(0.16, 1, 0.3, 1).
Inner grids — dots→text morph
Inside the bubble, two opposing grids crossfade:
- Dots section —
grid-template-rows: 1fr → 0frwhen answer ready (300ms expo) - Text section —
grid-template-rows: 0fr → 1frwhen answer ready (300ms expo)
Because both run simultaneously inside the same container, the bubble's total height morphs smoothly from dot-height to text-height. Text opacity fades in over 200ms with 80ms delay.
Spring entrance feel
A motion.div adds y: 4→0 with spring(380, 26) and opacity fade — purely cosmetic, not affecting layout flow.
Typing Indicator
Three dots pulse inside the bubble with staggered animation-delay (0ms, 150ms, 300ms). Each dot bounces translateY(-3px) with a 1.2s cycle. The dots are part of the bubble itself, not a separate element.
Tactile Press
Question bubbles use whileTap={{ scale: 0.98 }} with spring(500, 30) — a precise, subtle press that doesn't distort the text.
Natural Sibling Repositioning
Because CSS grid changes real DOM height (not CSS transforms), all sibling messages below the expanding bubble follow the height change naturally through normal document flow. No layout="position", no LayoutGroup, no FLIP calculations — the browser handles it natively.
Features
- CSS grid height transitions — real DOM height, not transforms
- Dots→text crossfade inside stable bubble container
- Spring entrance feel (y + opacity only)
- Natural sibling repositioning via document flow
- Typing indicator with staggered bouncing dots
- Right/left bubble alignment (conversational metaphor)
- iMessage-style bubble tails (asymmetric border-radius)
- whileTap tactile press on questions
- Theme-aware via CSS custom properties
- Accessible — semantic buttons, ARIA expanded, keyboard navigable

