Design system
GitHub

Section

FAQ

FAQ section with optional topic tabs and expandable answers.

npx -y github:anwarlemu/agentcard-brand-cli#release add faq
Open alone
faq.preview
Preview

Usage

use-it-for.md

Pass title, lead and topics containing questions and answers. A single topic hides the tabs. Set bar to label the window.

never.md
  • More than four topics
  • Answers longer than a short paragraph
  • Questions in caps
  • A chevron in place of the drawn +

Dependencies: accordion, window, @radix-ui/react-tabs.

Example

examples/faq.tsx
import { Faq } from "@/components/site/faq";

export function Site() {
  return (
    <section className="mx-auto w-full max-w-(--ds-frame-column) py-20">
      <Faq
        bar="faq.help / Agentcard"
        title="Frequently asked questions"
        lead="Everything you need to know about Agentcard."
        topics={[
          {
            label: "General",
            items: [
              { question: "What does Agentcard do?", answer: "Agentcard lets AI agents buy anything online: it stores your customers’ cards or issues new ones, and pays where a human would." },
              { question: "Vault or Issuing: which should we start with?", answer: "Vault if your customers already have cards they want their agent to use; Issuing if the agent should spend from a card you fund." },
              { question: "Is Agentcard B2B or for consumers?", answer: "Both. Consumer apps store their users’ cards; companies give cards to their own agents." },
              { question: "Does the merchant need to integrate anything?", answer: "No. The agent pays at checkout with a card, as a person would." },
              { question: "How is this different from Stripe Link?", answer: "Link fills in a saved card for a person. Agentcard lets an agent pay on its own, within the limits the owner sets." },
              { question: "How does the Purchase Agent work, and how reliable is it?", answer: "It drives a real browser through checkout and hands the result back to your agent." },
              { question: "Do you support x402, AP2 or other agent payment protocols?", answer: "Yes, where the merchant supports them; a card works everywhere else." },
            ],
          },
          {
            label: "Vault",
            items: [
              { question: "Where are the cards stored?", answer: "Cards are encrypted on the user’s device before they are stored." },
              { question: "Can users approve each payment?", answer: "Yes, with their passkey or master password, or automatically within the limits they set." },
              { question: "Who owns the Vault?", answer: "You do, and so does every card in it." },
            ],
          },
          {
            label: "Issuing",
            items: [
              { question: "What kinds of cards can we issue?", answer: "One-time cards for a single purchase, and multi-use cards with a monthly limit." },
              { question: "How are the cards funded?", answer: "From a balance you top up." },
            ],
          },
        ]}
      />
    </section>
  );
}

Source

components/site/faq.tsx
"use client";

import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";

import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { Window } from "@/components/ui/window";
import { cn } from "@/lib/utils";

// The FAQ (agentcard.sh home; .faq-card, .faq-tabs, .faq-list): one large window (the sky bar, faq.help) holding the
// 52px title, a one-line lead, topic tabs on the brand's sunken track with the blue key gliding to the chosen one, and
// the questions on light rules. A question is the display face at 14px and 500 beside a drawn +, which turns into an x
// as the answer slides open; hovering washes the row and turns it blue. The topics share one cell, so the window keeps
// the tallest topic's height and never jumps; switching fades the old questions out and brings the new ones up in turn.

type FaqItem = { question: React.ReactNode; answer: React.ReactNode };
type FaqTopic = { label: string; items: FaqItem[] };

const QUESTION =
  "group items-baseline gap-4 rounded-sm px-3.5 py-5 font-display text-body-sm leading-4 font-medium tracking-tight text-fg transition-[background-color,color] duration-(--ds-duration-fast) ease-out hover:bg-[color-mix(in_srgb,var(--ds-color-ink)_5%,transparent)] hover:text-fg-brand data-[state=open]:text-fg-brand motion-reduce:transition-none";

// The + is drawn, not typed: a bar across and a bar down, turning an eighth into an x when open.
const PLUS =
  "h-3.5 w-7.5 flex-none self-center bg-[linear-gradient(currentColor,currentColor),linear-gradient(currentColor,currentColor)] bg-[length:12px_1.5px,1.5px_12px] bg-center bg-no-repeat text-fg-faint transition-[rotate,color] duration-200 ease-out group-hover:text-fg-brand group-data-[state=open]:rotate-45 group-data-[state=open]:text-fg-brand motion-reduce:transition-none";

function Faq({
  title,
  lead,
  topics,
  bar = "faq.help",
  className,
  ...props
}: Omit<React.ComponentProps<"div">, "title"> & { title: React.ReactNode; lead?: React.ReactNode; topics: FaqTopic[]; bar?: React.ReactNode }) {
  const [active, setActive] = React.useState(0);
  const index = Math.min(active, topics.length - 1);
  const tabbed = topics.length > 1;
  return (
    <Window
      name={bar}
      data-slot="faq"
      className={cn("mx-auto w-full max-w-198 shadow-[var(--ds-shadow-card),0_18px_40px_-24px_color-mix(in_srgb,var(--ds-color-ink)_45%,transparent)]", className)}
      bodyClassName="flex flex-col items-center px-5 pt-5 pb-5 md:px-14 md:pt-13.5 md:pb-13.5"
      {...props}
    >
      <div className="flex flex-col items-center gap-4 text-center">
        <h2 className="font-display text-h1 leading-display font-medium tracking-tight text-balance">{title}</h2>
        {lead && <p className="max-w-[36ch] text-body text-fg-muted">{lead}</p>}
      </div>
      <TabsPrimitive.Root value={String(index)} onValueChange={(v) => setActive(Number(v))} className="flex w-full max-w-170 flex-col">
        {tabbed && (
          <TabsPrimitive.List
            style={{ "--tabs": topics.length, "--tab": index } as React.CSSProperties}
            className="relative isolate mt-8 mb-6 grid grid-cols-[repeat(var(--tabs),minmax(0,1fr))] gap-1 rounded-card border border-hairline bg-subtle p-1 shadow-well md:mt-11 md:mb-9"
          >
            {/* The blue key: one thumb under the labels, travelling to the chosen tab. */}
            <span
              aria-hidden
              className="pointer-events-none absolute top-1 bottom-1 left-1 w-[calc((100%-0.5rem-(var(--tabs)-1)*0.25rem)/var(--tabs))] translate-x-[calc(var(--tab)*(100%+0.25rem))] rounded-control border border-line-brand-edge bg-primary shadow-key-brand transition-[translate] duration-(--ds-duration-moderate) ease-in-out motion-reduce:transition-none"
            />
            {topics.map((topic, i) => (
              <TabsPrimitive.Trigger
                key={topic.label}
                value={String(i)}
                className="relative z-10 flex h-10 items-center justify-center rounded-control border border-transparent px-4 font-display text-body-sm whitespace-nowrap text-fg-muted outline-none transition-[background-color,color] duration-150 ease-out hover:bg-[color-mix(in_srgb,var(--ds-color-ink)_6%,transparent)] hover:text-fg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus-ring data-[state=active]:text-on-primary data-[state=active]:delay-100 data-[state=active]:hover:bg-transparent max-sm:px-0 motion-reduce:transition-none"
              >
                {topic.label}
              </TabsPrimitive.Trigger>
            ))}
          </TabsPrimitive.List>
        )}
        <div className={cn("grid w-full", !tabbed && "mt-8 md:mt-11")}>
          {topics.map((topic, t) => (
            <TabsPrimitive.Content
              key={topic.label}
              value={String(t)}
              forceMount
              className="group/topic [grid-area:1/1] self-start outline-none transition-[visibility] duration-150 data-[state=inactive]:pointer-events-none data-[state=inactive]:invisible"
            >
              <Accordion type="multiple">
                {topic.items.map((item, i) => (
                  <AccordionItem
                    key={i}
                    value={String(i)}
                    // The chosen topic's questions come up one after another; the old ones fade out at once.
                    style={{ transitionDelay: t === index ? `${110 + i * 22}ms` : "0ms" }}
                    className="border-b border-[color-mix(in_srgb,var(--ds-color-ink)_20%,transparent)] transition-[opacity,translate] duration-(--ds-duration-moderate) ease-out first:border-t first:border-t-line group-data-[state=inactive]/topic:opacity-0 group-data-[state=inactive]/topic:duration-150 motion-safe:group-data-[state=inactive]/topic:translate-y-2"
                  >
                    <AccordionTrigger className={QUESTION} icon={<span aria-hidden className={PLUS} />}>
                      {item.question}
                    </AccordionTrigger>
                    <AccordionContent className="max-w-[70ch] px-3.5 pb-5.5 text-body-sm leading-6.5 text-fg-muted opacity-80">{item.answer}</AccordionContent>
                  </AccordionItem>
                ))}
              </Accordion>
            </TabsPrimitive.Content>
          ))}
        </div>
      </TabsPrimitive.Root>
    </Window>
  );
}

export { Faq, type FaqTopic, type FaqItem };

Reference: agentcard-website src/components/faq.tsx, src/app/globals.css:932 (.faq2, .faq-list); agentcard.sh / (.faq-card, .faq-tabs, .faq-list, computed).