---
title: "How to Build an FAQ Accordion in Framer"
description: "Build a genuinely accessible FAQ accordion in Framer — native variables approach, a code override with keyboard support, and FAQPage schema for SEO."
canonical_url: "https://framerhub.io/blog/faq-accordion-framer"
last_updated: "2027-01-15T00:00:00.000Z"
---

FAQ accordions are one of the most commonly built — and most commonly under-built — patterns on the web. Most tutorials stop at "make it expand and collapse," which gets you something that looks right but skips the details that actually matter: keyboard accessibility, single vs. multi-open behavior, and the structured data that can turn your FAQ section into a rich search result. Here's the complete version.

## What "Done Right" Actually Means for an Accordion

A genuinely well-built FAQ accordion handles four things most quick tutorials skip:

1. **Keyboard operability** — reachable via Tab, toggleable via Enter or Space, not just clickable with a mouse.
2. **Correct semantics** — a real `<button>` triggering the toggle, with `aria-expanded` reflecting its state, so screen readers announce it correctly.
3. **Deliberate open behavior** — a decision about whether multiple items can be open simultaneously, not an accident of how the code happens to work.
4. **Structured data** — FAQPage schema so search engines can potentially surface your questions directly in results.

Most accordion tutorials online nail the animation and stop there. That's the gap this guide fills — and if structured data and search visibility matter to your site broadly, not just this one component, our [Framer SEO guide](/blog/framer-seo-guide) covers the wider technical SEO picture beyond this single pattern.

## Why FAQ Sections Are Worth Building Properly

An FAQ accordion isn't just a UX nicety — on a pricing page, product page, or service page, it's often doing real conversion work by pre-answering the objections that would otherwise land in your inbox or live chat. A visitor who finds "do you offer refunds?" answered inline is a visitor who doesn't have to leave the page to go find out, which is exactly the kind of friction that quietly kills conversions. That's also why the SEO layer matters as much as the interaction layer: a well-marked-up FAQ section can capture additional search real estate for long-tail, question-style queries your main headline copy would never rank for on its own.

## Option 1: Native Framer, No Code

Framer's Variables feature makes a working accordion achievable without any code. The setup, per question:

1. Create a Boolean **Variable** (e.g., `question1Open`) in your project's Variables panel.
2. Build your question row as a Stack with the question text and a chevron icon, and add an **onClick** interaction that toggles the variable.
3. Below the question row, add your answer text inside a Stack with its **height set to "Auto" with a variable-driven condition** — hide it (height 0, opacity 0) when the variable is `false`, show it (auto height, opacity 1) when `true`, with a transition applied to the height change.
4. Rotate the chevron icon based on the same variable so it flips as the item opens.

This works and looks correct for a small, fixed number of questions. Where it breaks down: each question needs its own dedicated variable and its own wiring, so a 12-question FAQ page means 12 near-identical variable setups built and maintained by hand. It's also easy to accidentally let multiple items stay open at once, since nothing enforces "closing" the others unless you wire that logic in too — which native Variables can do, but it multiplies the manual setup further.

## Option 2: A Code Component (Recommended for Anything Beyond a Handful of Items)

This implementation handles single-open-at-a-time behavior, keyboard accessibility, and smooth height animation, and scales to any number of questions from one prop array instead of per-item manual wiring.

```jsx
import { useState, useId } from "react"
import { motion, AnimatePresence } from "motion/react"
import { addPropertyControls, ControlType } from "framer"

export default function FAQAccordion(props) {
  const { items, allowMultiple, accentColor } = props
  const [openIndexes, setOpenIndexes] = useState([])
  const baseId = useId()

  const list = items && items.length ? items : [
    { question: "Sample question one?", answer: "Sample answer one." },
    { question: "Sample question two?", answer: "Sample answer two." },
  ]

  function toggle(i) {
    setOpenIndexes((prev) => {
      const isOpen = prev.includes(i)
      if (allowMultiple) {
        return isOpen ? prev.filter((x) => x !== i) : [...prev, i]
      }
      return isOpen ? [] : [i]
    })
  }

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
      {list.map((item, i) => {
        const isOpen = openIndexes.includes(i)
        const buttonId = `${baseId}-btn-${i}`
        const panelId = `${baseId}-panel-${i}`
        return (
          <div key={i} style={{ borderBottom: "1px solid #e5e5e5" }}>
            <button
              id={buttonId}
              aria-expanded={isOpen}
              aria-controls={panelId}
              onClick={() => toggle(i)}
              style={{
                width: "100%",
                display: "flex",
                justifyContent: "space-between",
                alignItems: "center",
                padding: "16px 0",
                background: "none",
                border: "none",
                textAlign: "left",
                font: "inherit",
                cursor: "pointer",
              }}
            >
              <span style={{ fontWeight: 600 }}>{item.question}</span>
              <motion.span
                animate={{ rotate: isOpen ? 45 : 0 }}
                transition={{ duration: 0.2 }}
                style={{ color: accentColor, fontSize: 20 }}
              >
                +
              </motion.span>
            </button>
            <AnimatePresence initial={false}>
              {isOpen && (
                <motion.div
                  id={panelId}
                  role="region"
                  aria-labelledby={buttonId}
                  initial={{ height: 0, opacity: 0 }}
                  animate={{ height: "auto", opacity: 1 }}
                  exit={{ height: 0, opacity: 0 }}
                  transition={{ duration: 0.25, ease: "easeInOut" }}
                  style={{ overflow: "hidden" }}
                >
                  <p style={{ paddingBottom: 16, margin: 0, color: "#555" }}>{item.answer}</p>
                </motion.div>
              )}
            </AnimatePresence>
          </div>
        )
      })}
    </div>
  )
}

addPropertyControls(FAQAccordion, {
  items: {
    type: ControlType.Array,
    control: {
      type: ControlType.Object,
      controls: {
        question: { type: ControlType.String, defaultValue: "Question?" },
        answer: { type: ControlType.String, defaultValue: "Answer." },
      },
    },
  },
  allowMultiple: { type: ControlType.Boolean, defaultValue: false },
  accentColor: { type: ControlType.Color, defaultValue: "#000000" },
})
```

Three details in this snippet are the ones a "just make it animate" tutorial usually skips:

- **aria-expanded on the button**, kept in sync with actual state, so a screen reader announces "collapsed" or "expanded" correctly.
- **aria-controls and matching id/aria-labelledby** linking the button to its panel, so assistive technology understands which button controls which content block.
- **A real <button> element**, not a styled `<div>` with an `onClick` — this alone is what makes the accordion Tab-reachable and operable with Enter/Space by default, without writing custom key handlers.

## Single-Open vs. Multi-Open: Make the Call Deliberately

The `allowMultiple` prop above exists because this is a decision worth making intentionally rather than by accident:

- **Single-open** (only one answer visible at a time) keeps a long FAQ page shorter and forces a visitor to choose what they actually want to read — the better default for FAQ lists with 6+ questions.
- **Multi-open** (any number of answers can be open simultaneously) works better for short lists (3-4 items) where a visitor scanning the whole section benefits from seeing everything at once without repeated clicking.

Most FAQ sections on marketing pages — pricing FAQs, product FAQs — are long enough that single-open is the right default. Reserve multi-open for genuinely short lists.

## Adding FAQPage Schema for SEO

This is the step that separates a good-looking accordion from one that can actually earn extra visibility in search results. Google can show FAQ content as an expandable rich result directly in the search listing when a page includes valid `FAQPage` structured data — meaning your questions can appear right in the SERP before a visitor even clicks through. Add this as JSON-LD in your page's `<head>` (or via Framer's page-level custom code panel), mirroring the same question/answer pairs your accordion component displays:

```json
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "Your first question here?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "The plain-text answer here, matching what's in the accordion."
      }
    }
  ]
}
```

Keep the schema's questions and answers in sync with what's actually visible in the accordion — mismatched or fabricated schema content violates Google's structured data guidelines and risks a manual action against rich result eligibility for the page, not just a missed opportunity.

## Common Mistakes That Undermine an Accordion

- **Content that only exists after the click.** If the answer text is fetched or rendered only on interaction, it may not be reliably indexed. Keep all answer content in the DOM from page load, hidden via CSS/animation state, not conditionally rendered from nothing.
- **No visual affordance that content is expandable.** A chevron, plus/minus icon, or similar visual cue is doing real communication work — without one, visitors may not realize the questions are clickable at all.
- **Animating with display: none to block.** This can't be smoothly animated with CSS transitions. Animate `height`/`max-height` and `opacity` instead, as in the snippet above.
- **Too many questions in one accordion.** Beyond roughly 10-12 items, consider splitting into categorized groups with sub-headings rather than one long undifferentiated list — it's both a UX and scannability improvement.

## Styling Notes That Make an Accordion Feel Premium

Beyond the functional pieces above, a few styling choices consistently separate a polished accordion from a generic one:

- **Icon rotation over icon swap.** Rotating a plus sign 45 degrees into an "x" (as in the snippet's `motion.span`) feels smoother than crossfading between two separate icon glyphs, and it's cheaper to implement.
- **A subtle background or left-border accent on the open item.** This gives visitors a lightweight visual anchor for which question they're currently reading, especially useful in single-open accordions with many items.
- **Consistent padding regardless of open state.** If the question row's padding shifts when it opens, the whole list visually jumps as items above and below adjust — keep question row height fixed and only animate the answer panel below it.
- **A max-width on answer text.** Full-width answer paragraphs on a wide desktop layout are harder to read than a constrained measure (60-75 characters per line) — cap answer text width even if the question row spans the full container.

## Mobile Considerations

Accordions generally translate well to mobile without much rework, since the collapsed-by-default pattern is already well-suited to a narrow viewport where space is at a premium. The one adjustment worth making: increase the tap target size on the question row (aim for at least 44px of tappable height) beyond what feels necessary on desktop, since imprecise touch input makes a tightly-padded accordion trigger frustrating to hit reliably.

## No Dedicated Component, But Two Paths That Fit

FAQ accordions are simple enough that we haven't built a dedicated marketplace component for them specifically — but if you're already assembling a page with other interactive elements, FramerHub's [component library](/components) has toggle, tab, and reveal patterns built with the same accessibility standards covered in this guide, and the [Components plugin](/plugins/framer-components) is the fastest way to browse and drop them into a project alongside a hand-built accordion like the one above. If you're building out a full FAQ-heavy page and want a broader pre-publish checklist beyond just this component, our [SEO checklist for Framer templates](/blog/seo-checklist-for-framer-templates) covers the rest of what typically needs attention before launch.

## FAQ

**Can Framer build an accordion natively without code?**
Yes — using Variables and conditional visibility on a Stack with "Auto" sizing, you can build a working expand/collapse accordion with no code at all. It takes more manual setup per question than a code component, but it's genuinely possible natively.

**Should only one FAQ item be open at a time?**
For most sites, yes — single-open accordions keep the page shorter and force visitors to actively choose what they want to read, which is generally the better UX for a long FAQ list. Multi-open makes more sense for short lists of 3-4 items where scanning everything at once is genuinely useful.

**Do I need FAQPage schema for an FAQ accordion?**
It's not required for the accordion to function, but adding FAQPage structured data lets Google potentially show your questions as expandable rich results directly in search, which can meaningfully increase your click-through rate for FAQ-heavy pages.

**Is an animated accordion bad for SEO?**
No, as long as the answer text is real HTML content in the page (even if visually hidden when collapsed), not content that only loads after a click. Search engines generally read collapsed accordion content fine, since it exists in the DOM regardless of visual state.

**How do I make an accordion keyboard accessible?**
The trigger needs to be a real button element (not a div with an onClick), toggle a proper aria-expanded attribute, and be reachable and operable via Tab and Enter/Space — a visual-only accordion built from styled divs typically fails all three.
