Skip to content
Black FridayBlack Friday launch price: Lifetime $149 until 30 Nov. Regular price $299 from 1 Dec.ends in 15d 03:13:40Get Lifetime →
loopkit
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 Pro

How 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.