Skip to content

FAQ Search

Searchable FAQ: a huge ruled search field that presses in on focus, live result count, and native <details> cards with tags; a pink empty state points to support.

brutalist/section/faq-search
Open ↗

Source

"use client";

import { useId, useMemo, useState } from "react";
import { Search } from "lucide-react";
import { cn } from "@/lib/utils";

export interface FaqSearchItem {
  question: string;
  answer: string;
  tags?: string[];
}

export interface FaqSearchProps {
  kicker?: string;
  title?: string;
  placeholder?: string;
  items?: FaqSearchItem[];
  className?: string;
}

const DEFAULT_ITEMS: FaqSearchItem[] = [
  {
    question: "How do I connect GitHub?",
    answer: "Settings → Integrations → GitHub, then pick the repositories. Only PR metadata is read.",
    tags: ["setup", "github"],
  },
  {
    question: "Can I import an existing changelog?",
    answer: "Yes: paste a Markdown file, point us at an RSS feed or use the API. Dates and links are kept.",
    tags: ["migration"],
  },
  {
    question: "How do I add a custom domain?",
    answer: "Add a CNAME to cname.shipyard.dev and click verify. TLS is provisioned automatically.",
    tags: ["setup", "domain"],
  },
  {
    question: "Can I schedule a release?",
    answer: "Pick a date and time in the publish menu. Digests follow the same schedule.",
    tags: ["publishing"],
  },
  {
    question: "How do I hide a release from the widget?",
    answer: "Toggle 'Widget' off in the release sidebar. It stays on the public page.",
    tags: ["widget"],
  },
  {
    question: "Where do I find invoices?",
    answer: "Billing → Invoices. You can add a VAT number and a billing email there.",
    tags: ["billing"],
  },
];

export function FaqSearch({
  kicker = "[ 07 ] Docs, the short version",
  title = "Search the FAQ.",
  placeholder = "Try “domain”, “github” or “invoice”",
  items = DEFAULT_ITEMS,
  className,
}: FaqSearchProps) {
  const [query, setQuery] = useState("");
  const inputId = useId();
  const results = useMemo(() => {
    const q = query.trim().toLowerCase();
    if (!q) return items;
    return items.filter((i) => `${i.question} ${i.answer} ${i.tags?.join(" ") ?? ""}`.toLowerCase().includes(q));
  }, [items, query]);

  return (
    <section className={cn("da-section da-stroke-b bg-da-bg text-da-fg", className)} aria-labelledby={`${inputId}-title`}>
      <div className="da-container">
        <div className="mx-auto max-w-4xl">
          <p className="font-da-mono text-xs font-bold tracking-da-label text-da-muted-fg uppercase">{kicker}</p>
          <h2 id={`${inputId}-title`} className="mt-3 font-da-display text-[clamp(2rem,5vw,3.5rem)] leading-[0.95] tracking-da-display uppercase">
            {title}
          </h2>
          <div className="da-stroke mt-8 flex items-center gap-3 bg-da-input px-4 shadow-da-md focus-within:translate-x-[3px] focus-within:translate-y-[3px] focus-within:shadow-none focus-within:outline-[length:var(--da-ring-width)] focus-within:outline-offset-[var(--da-ring-offset)] focus-within:outline-da-ring focus-within:outline-solid">
            <Search aria-hidden className="size-6 shrink-0" strokeWidth={3} />
            <label htmlFor={inputId} className="sr-only">
              Search questions
            </label>
            <input
              id={inputId}
              type="search"
              value={query}
              onChange={(e) => setQuery(e.target.value)}
              placeholder={placeholder}
              className="h-16 min-w-0 flex-1 bg-transparent text-xl font-bold text-da-fg outline-none placeholder:font-medium placeholder:text-da-muted-fg"
            />
          </div>
          <p className="mt-4 font-da-mono text-xs font-bold text-da-muted-fg uppercase" aria-live="polite">
            {results.length} result{results.length === 1 ? "" : "s"}
          </p>
          <ul className="mt-4 flex flex-col gap-4">
            {results.map((r) => (
              <li key={r.question} className="da-stroke bg-da-surface p-5 text-da-surface-fg">
                <details className="group">
                  <summary className="da-focus flex cursor-pointer list-none items-center justify-between gap-4 text-lg font-bold [&::-webkit-details-marker]:hidden">
                    {r.question}
                    <span
                      aria-hidden
                      className="da-stroke grid size-8 shrink-0 place-items-center bg-da-primary font-da-mono text-da-primary-fg group-open:bg-da-fg group-open:text-da-bg"
                    >
                      <span className="group-open:hidden">+</span>
                      <span className="hidden group-open:inline">−</span>
                    </span>
                  </summary>
                  <p className="mt-3 text-da-muted-fg">{r.answer}</p>
                  {r.tags && (
                    <p className="mt-3 flex flex-wrap gap-2">
                      {r.tags.map((t) => (
                        <span key={t} className="da-stroke bg-da-surface-2 px-1.5 py-0.5 font-da-mono text-[11px] font-bold uppercase">
                          #{t}
                        </span>
                      ))}
                    </p>
                  )}
                </details>
              </li>
            ))}
          </ul>
          {results.length === 0 && (
            <p className="da-stroke bg-da-accent p-6 font-bold text-da-accent-fg">
              No answer for “{query}”. Our engineers reply to support@shipyard.dev in under 2h.
            </p>
          )}
        </div>
      </div>
    </section>
  );
}

export default FaqSearch;

modules/brutalist/section/faq-search/index.tsx

Props

PropTypeDefaultDescription
kicker / titlestring—Header copy.
placeholderstring—Search placeholder.
itemsFaqSearchItem[]—{ question, answer, tags? }[]; search matches question, answer and tags.
classNamestring—Classes on the <section>.

Other faq variants in Brutalist

FAQ in other art directions

Faq Cards

All-open FAQ: a 3-column grid of glass cards, each with a gradient number badge, question and answer, followed by a glass contact pill with an ink button.

GlassFAQ

Faq Chat

FAQ as a chat: suggested question chips on the left; picking one adds a gradient user bubble and, after a typing indicator, a glass answer bubble in the conversation panel.

GlassFAQ

FAQ Glass

Centered FAQ with stacked frosted accordion cards; the plus button spins into a gradient cross with a slight overshoot and answers expand smoothly. Built on Radix Accordion.

GlassFAQ

Faq Search

Help-center FAQ: large glass search pill filtering a frosted Radix accordion live, with category tags, result count and highlighted matches; empty state.

GlassFAQ

Faq Grid

All answers visible at once: a three-column grid of short questions and answers under a header, followed by two support link cards (documentation, talk to us).

MinimalFAQ

Faq List

Centered single-column FAQ built on native details/summary (zero JS): hairline dividers, a plus icon that rotates into a cross when open, and a contact line underneath.

MinimalFAQ

FAQ Split

Two-column FAQ: title, description and support link on the left, hairline-divided Radix accordion on the right with a rotating chevron and a smooth height + fade expand.

MinimalFAQ

Faq Tabs

FAQ grouped by category behind an underline tab bar (Radix Tabs, with counts); the active category shows every question and answer in a two-column definition list.

MinimalFAQ

Faq Inline

Ultra-compact FAQ: short questions and one-line answers flowing as a 3-column grid of mono question labels over large answers — scannable in seconds.

Mono CleanFAQ

Faq List

Numbered accordion FAQ in a ruled list: mono index + question, a +/− glyph that swaps, answer indented under the question; left column holds the label and title.

Mono CleanFAQ

Faq Split

Always-open FAQ as a two-column definition list (question left in medium weight, answer right in muted text) on hairline rows, followed by a mono contact line.

Mono CleanFAQ

Faq Tabs

FAQ grouped by topic with text tabs (Radix Tabs) underlined in ink when active, and each topic’s answers as a ruled definition list.

Mono CleanFAQ

Faq Accordion

Two-column FAQ: heading + description + contact link on the left, bordered Radix accordion with plus icons rotating to × on the right.

Neo CorporateFAQ

Faq Security

Security & compliance FAQ: dark-tinted certification cards (SOC 2, ISO 27001, GDPR, PCI) with shield icons, then security questions as native <details> disclosure rows.

Neo CorporateFAQ

Faq Sidebar

Help-center style FAQ: a vertical category nav (left, becomes horizontal scroll chips on mobile) and the selected category’s questions as an open definition list.

Neo CorporateFAQ

Faq Support

Compact 2×3 grid of short Q&As followed by three bordered support channel cards (docs, chat, dedicated support).

Neo CorporateFAQ

Faq Columns

Editorial FAQ: serif title and intro in a narrow left column, questions as a two-column definition list on the right separated by warm hairlines.

Organic SoftFAQ

Faq Contact

FAQ beside a contact card: native disclosure list on the left, and a clay-tinted card with a sprouting-leaf illustration, email and chat buttons on the right.

Organic SoftFAQ

Faq Search

Searchable FAQ: rounded search field plus topic pills filter a list of questions (native details), with a friendly empty state and live result count.

Organic SoftFAQ

Faq Soft

Soft accordion: each question is its own rounded paper pill-card that turns sage when open, with a round plus that rotates into a cross; generous spacing.

Organic SoftFAQ

Faq Columns

All-visible FAQ: sticky intro with a peach “Chat with us” pill on the left, six Q&As in a 2-column grid with round lavender number badges on the right.

Soft FlatFAQ

Faq Help

Help-center FAQ: lavender panel with a friendly greeting, big white search pill and popular-search chips; matching answers show as white cards with a peach empty state.

Soft FlatFAQ

Faq Pills

Single-column FAQ of rounded white blocks on native details/summary; the open item turns lavender and its round chevron button flips and fills periwinkle.

Soft FlatFAQ

Faq Tinted

FAQ by topic: three big pastel topic buttons (emoji + label) switch the list of white Q&A cards below, which fades in on change.

Soft FlatFAQ