SectionsSimpleApril 9, 2026
FAQ Section
Two-column FAQ layout with a sticky heading on the left and an accordion list on the right. Expand/collapse is driven by CSS grid-template-rows animation — no JavaScript animation library needed. Items, questions, and answers are fully prop-driven.
View Full Demo →Preview
Source
demo.jsx
import FAQSection from "./index.jsx";
export default function FAQSectionDemo() {
return <FAQSection />;
}
index.jsx
"use client";
import { useState } from "react";
import Section from "@/components/primitives/Section";
import Container from "@/components/primitives/Container";
import styles from "./styles.module.css";
const FAQ_ITEMS = [
{
question: "What can I send you?",
answer:
"You can send us any design or development request — landing pages, web apps, dashboards, components, or full product builds. If you're unsure, just reach out and we'll let you know if it's a good fit.",
},
{
question: "How fast will I get my work?",
answer:
"Most requests are completed within 2–4 business days. Larger or more complex work may take longer, but we'll always give you a clear timeline upfront.",
},
{
question: "Who will I work with?",
answer:
"You'll work directly with Kevin — no account managers or hand-offs. Every project gets personal attention from start to finish.",
},
{
question: "Can I pause or cancel?",
answer:
"Yes. You can pause or cancel your subscription at any time. There are no long-term contracts or cancellation fees.",
},
{
question: "What's not included?",
answer:
"We don't cover print design, video production, or SEO copywriting. If you're unsure whether your request is in scope, just ask.",
},
{
question: "What if I'm not happy with the work?",
answer:
"We'll revise until you're satisfied. Our goal is to deliver work you're proud of — if something isn't right, we'll make it right.",
},
];
function PlusIcon() {
return (
<svg width="20" height="20" viewBox="0 0 16 16" fill="none"
stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
<line x1="8" y1="2" x2="8" y2="14" />
<line x1="2" y1="8" x2="14" y2="8" />
</svg>
);
}
function CloseIcon() {
return (
<svg width="20" height="20" viewBox="0 0 16 16" fill="none"
stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
<line x1="2" y1="2" x2="14" y2="14" />
<line x1="14" y1="2" x2="2" y2="14" />
</svg>
);
}
function FAQItem({ question, answer, isOpen, onToggle }) {
return (
<div className={`${styles.faqItem} ${isOpen ? styles.faqItemOpen : ""}`}>
<button
className={styles.faqHeader}
onClick={onToggle}
aria-expanded={isOpen}
>
<span className={styles.faqQuestion}>{question}</span>
<span className={styles.faqIcon} aria-hidden="true">
{isOpen ? <CloseIcon /> : <PlusIcon />}
</span>
</button>
<div className={styles.faqBodyWrapper}>
<div className={styles.faqBodyInner}>
<p className={styles.faqAnswer}>{answer}</p>
</div>
</div>
</div>
);
}
export default function FAQSection({ items = FAQ_ITEMS }) {
const [openIndex, setOpenIndex] = useState(null);
function handleToggle(index) {
setOpenIndex((prev) => (prev === index ? null : index));
}
return (
<Section className={styles.section} variant="bare">
<Container>
<div className={styles.faqInner}>
<div className={styles.faqLeft}>
<h2 className={styles.heading}>
Common<br />questions
</h2>
</div>
<div className={styles.faqList}>
{items.map((item, index) => (
<FAQItem
key={index}
question={item.question}
answer={item.answer}
isOpen={openIndex === index}
onToggle={() => handleToggle(index)}
/>
))}
</div>
</div>
</Container>
</Section>
);
}
styles.module.css
/* Local tokens — component is self-contained */
.section {
--color-white: #ffffff;
--color-gray: rgba(255, 255, 255, 0.45);
--space-xl: 1.5rem;
--space-2xl: 2.5rem;
--space-4xl: 3.5rem;
--space-6xl: 4.5rem;
--space-10xl: 7rem;
--font-body: 1rem;
background-color: #1f1f20;
padding-block: var(--space-10xl);
}
.faqInner {
display: flex;
flex-direction: row;
gap: var(--space-xl);
align-items: flex-start;
}
.faqLeft {
flex-shrink: 0;
width: 35.9375rem;
}
.heading {
font-size: clamp(2.25rem, 5vw, 4.25rem);
font-weight: 500;
color: var(--color-white);
line-height: 0.95;
letter-spacing: -0.02em;
font-family: Arial, Helvetica, sans-serif;
}
.faqList {
flex: 1;
display: flex;
flex-direction: column;
}
.faqItem {
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
}
.faqItem:first-child {
border-top: 1px solid rgba(255, 255, 255, 0.15);
}
.faqHeader {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding-block: var(--space-xl);
background: none;
border: none;
cursor: pointer;
text-align: left;
gap: var(--space-2xl);
outline: none;
}
.faqHeader:focus-visible {
outline: 2px solid var(--color-white);
outline-offset: 2px;
}
.faqQuestion {
font-size: 1rem;
font-weight: var(--weight-medium);
color: var(--color-gray);
line-height: 1.4;
letter-spacing: 0.1em;
text-transform: uppercase;
font-family: var(--font-mono);
transition: color 200ms ease;
}
.faqItemOpen .faqQuestion,
.faqHeader:hover .faqQuestion {
color: var(--color-white);
}
.faqIcon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--color-gray);
transition: color 200ms ease;
}
.faqItemOpen .faqIcon,
.faqHeader:hover .faqIcon {
color: var(--color-white);
}
/* CSS grid accordion — no JS needed for the animation */
.faqBodyWrapper {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 400ms cubic-bezier(0.625, 0.05, 0, 1);
}
.faqItemOpen .faqBodyWrapper {
grid-template-rows: 1fr;
}
.faqBodyInner {
overflow: hidden;
}
.faqAnswer {
padding-bottom: var(--space-xl);
font-size: var(--font-body);
font-weight: var(--weight-normal);
color: rgba(255, 255, 255, 0.6);
line-height: 1.6;
font-family: var(--font-mono);
max-width: 40rem;
}
@media (max-width: 991px) {
.faqInner {
gap: var(--space-6xl);
}
.faqLeft {
width: 14rem;
}
}
@media (max-width: 479px) {
.section {
padding-block: var(--space-4xl);
}
.faqInner {
flex-direction: column;
gap: var(--space-4xl);
}
.faqLeft {
width: 100%;
}
}