proNew282 lines · framer-motion
FAQ Two Column
Filter by topic, then read the answer. A full-width FAQ section with a sticky left column (headline, description, category filter) and a right column of question/answer rows that cross-fade in when the filter changes and expand individually.
- Use it for
- A denser FAQ page or section covering several topics (billing, security, product) where visitors want to jump straight to their category.
- Themes
- Light + dark
- License
- Commercial use
Live
FAQ
Answers, organized by topic
Filter by what you're trying to figure out — billing, security, or how the product fits your stack — then expand a question for the full answer.
tsx
"use client";
import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { cn } from "@/lib/utils";
function useReducedMotionSafe() {
const prefersReduced = useReducedMotion();
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => setMounted(true), []);
return mounted ? !!prefersReduced : false;
}
/**
* Full-width FAQ section laid out in two columns: a left column (sticky on wide
* screens) with a headline, description, and a segmented category filter, and a
* right column of filtered question/answer rows that cross-fade in when the
* filter changes and expand individually like an accordion. Collapses to a single
* stacked column — left no longer sticky — below the `lg` breakpoint.
*/
export type FaqTwoColumnCategory = "billing" | "security" | "product";The rest is in Pro.
Full source, the rebuild prompt and registry install unlock with Pro or Lifetime.
Get ProHow it behaves
- →The left column renders a segmented group of custom <button> filters (All / Billing / Security / Product) — not a native <select> — with aria-pressed marking the active one.
- →Changing the filter swaps the right-column list inside framer-motion's AnimatePresence (mode="popLayout"): the previous set fades out while the new filtered set fades and staggers in, each row offset by index * 0.04s.
- →Each row is also its own accordion: clicking its button animates its answer panel's height from 0 to "auto" with a fade, independent of the filter cross-fade.
- →allowMultipleOpen controls whether several rows in the right column can have their answer open simultaneously, or opening one closes the rest.
- →defaultCategory sets which filter is active on mount ("all" or a single category).
- →On lg and wider the left column is sticky while the right column scrolls past it; below lg the grid collapses to a single stacked column and the left column loses its sticky positioning.
- →prefers-reduced-motion sets every transition duration to 0 and skips the entrance/stagger and cross-fade motion, so the active filter's list and any open answers render in their resting state immediately.
Accessibility
- ✓The filter group has `role="group"` with an `aria-label`, and each filter button exposes its selected state via `aria-pressed` instead of relying on color alone.
- ✓Each row trigger is a native <button> with `aria-expanded`/`aria-controls`, and its panel has `role="region"` and `aria-labelledby`; the button sits inside an <h3> via a `display:contents` wrapper for correct heading navigation.
- ✓Category labels inside each row are rendered as visible text (not icon-only), so filtering context is readable without relying on the sidebar alone.
- ✓Honours prefers-reduced-motion.