Skip to content
← Blocks

FAQ

A complete faq section, ready to adapt to your product.

Open ↗

Make it yours.

Install the editable source and its dependencies into your React + Tailwind project.

Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/faq.json

Use it

Scroll horizontally for long lines
"use client";
import { Faq } from "@/components/jez-ui/blocks/faq";

export default function Example() {
  return (
    <>
      <Faq />
    </>
  );
}

Compose your own

These styled parts are included in the same install. Use children to build your layout; add className only when you want an override.

Scroll horizontally for long lines
"use client";
import { Faq, FaqTitle, FaqQuestions } from "@/components/jez-ui/blocks/faq";
import {
  AccordionItem,
  AccordionTrigger,
  AccordionContent,
} from "@/components/jez-ui/ui/accordion";
export default function Example() {
  return (
    <Faq>
      <FaqTitle>Your questions, answered.</FaqTitle>
      <FaqQuestions>
        <AccordionItem value="custom">
          <AccordionTrigger>Can I choose my own content?</AccordionTrigger>
          <AccordionContent>
            Yes. The sections keep their behavior when you supply your own
            content.
          </AccordionContent>
        </AccordionItem>
      </FaqQuestions>
    </Faq>
  );
}
Scroll horizontally for long lines
import { Faq, FaqTitle, FaqHeading, FaqQuestions } from "@/components/jez-ui/blocks/faq";
Read the composition guide →

Props & types

Scroll horizontally for long lines
export type FaqOptions = {
  className?: string;
  heading?: React.ReactNode;
};

export type FaqProps = Omit<React.ComponentProps<"section">, keyof FaqOptions> &
  FaqOptions;

FaqProps

Source

View 3 source files

registry/blocks/faq.tsx

Scroll horizontally for long lines
"use client";
import * as React from "react";
import { cn } from "../ui/utils";
import { Accordion } from "../ui/accordion";
export type FaqOptions = {
  className?: string;
  heading?: React.ReactNode;
};
export type FaqProps = Omit<React.ComponentProps<"section">, keyof FaqOptions> &
  FaqOptions;

function useFaqModel({
  heading = "Good questions.",
  className,
  children,
  ...rootProps
}: FaqProps) {
  return { heading, className, children, rootProps };
}
const FaqCompositionContext = React.createContext<ReturnType<
  typeof useFaqModel
> | null>(null);
function useFaqComposition() {
  const context = React.useContext(FaqCompositionContext);
  if (!context) throw new Error("Faq parts must be inside Faq.");
  return context;
}
export function Faq(props: FaqProps) {
  const model = useFaqModel(props);
  const { className, rootProps, children } = model;
  return (
    <FaqCompositionContext.Provider value={model}>
      <section {...rootProps} className={cn("py-8", className)}>
        {children !== undefined ? (
          children
        ) : (
          <>
            <FaqHeading />
            <FaqQuestions />
          </>
        )}
      </section>
    </FaqCompositionContext.Provider>
  );
}

export function FaqTitle({ className, ...props }: React.ComponentProps<"h2">) {
  return (
    <h2
      data-slot="faq-title"
      className={cn("mb-5 text-3xl", className)}
      {...props}
    />
  );
}

export function FaqHeading({
  children,
  ...props
}: Partial<React.ComponentProps<typeof FaqTitle>> & {
  children?: React.ReactNode;
}) {
  const { heading } = useFaqComposition();
  return (
    <FaqTitle {...props}>
      {children === undefined ? heading : children}
    </FaqTitle>
  );
}
export function FaqQuestions({
  children,
  ...props
}: Partial<
  Extract<React.ComponentProps<typeof Accordion>, { type: "single" }>
> & { children?: React.ReactNode }) {
  const {} = useFaqComposition();
  return (
    <Accordion
      type="single"
      collapsible
      {...props}
      items={
        children === undefined
          ? (props.items ?? [
              {
                value: "start",
                title: "How do I get started?",
                content:
                  "Choose a template, install its dependencies, and run the local development server. The included README walks through each step.",
              },
              {
                value: "source",
                title: "Can I change the source?",
                content:
                  "The source is designed to be edited. Adjust the theme and components to fit your product, subject to the distribution licence.",
              },
              {
                value: "backend",
                title: "Is a backend included?",
                content:
                  "These are frontend templates with demo data. Connect your own authentication, storage, and services at the documented integration points.",
              },
            ])
          : undefined
      }
    >
      {children}
    </Accordion>
  );
}

registry/ui/utils.ts

Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
  return twMerge(clsx(values));
}

registry/ui/accordion.tsx

Scroll horizontally for long lines
"use client";
import * as React from "react";
import { Accordion as P } from "radix-ui";
import { ChevronDown } from "lucide-react";
import { cn } from "./utils";
export function Accordion({
  items,
  children,
  className,
  ...props
}: React.ComponentProps<typeof P.Root> & {
  items?: { value: string; title: React.ReactNode; content: React.ReactNode }[];
}) {
  return (
    <P.Root className={cn("w-full", className)} {...props}>
      {items
        ? items.map((i) => (
            <AccordionItem key={i.value} value={i.value}>
              <AccordionTrigger>{i.title}</AccordionTrigger>
              <AccordionContent>{i.content}</AccordionContent>
            </AccordionItem>
          ))
        : children}
    </P.Root>
  );
}
export function AccordionItem({
  className,
  ...props
}: React.ComponentProps<typeof P.Item>) {
  return (
    <P.Item className={cn("border-b border-border", className)} {...props} />
  );
}
export function AccordionTrigger({
  className,
  children,
  ...props
}: React.ComponentProps<typeof P.Trigger>) {
  return (
    <P.Header>
      <P.Trigger
        className={cn(
          "group flex w-full items-center justify-between gap-4 py-4 text-left text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:opacity-50",
          className,
        )}
        {...props}
      >
        {children}
        <ChevronDown
          size={16}
          aria-hidden
          className="shrink-0 transition-transform group-data-[state=open]:rotate-180 motion-reduce:transition-none"
        />
      </P.Trigger>
    </P.Header>
  );
}
export function AccordionContent({
  className,
  children,
  ...props
}: React.ComponentProps<typeof P.Content>) {
  return (
    <P.Content
      style={
        {
          "--radix-collapsible-content-height":
            "var(--radix-accordion-content-height)",
        } as React.CSSProperties
      }
      className="jez-collapse overflow-hidden"
      {...props}
    >
      <div
        className={cn(
          "pb-4 text-sm leading-relaxed text-muted-foreground",
          className,
        )}
      >
        {children}
      </div>
    </P.Content>
  );
}

Dependencies

clsx@2.1.1 · tailwind-merge@3.5.0 · radix-ui@1.6.7 · lucide-react@0.577.0

Accessibility & behaviour

Provide meaningful labels and preserve keyboard focus styles. Check contrast when customising theme colours.

Read the accessibility and performance guide →