FAQ
Searchable FAQ with filters
Search field, topic pills, results list
Search field, topic pills, results list
Build a searchable help centre FAQ with Tailwind CSS: a centered heading, a prominent search input with a leading magnifier glyph, and a row of scrollable topic filter pills where the first is active and dark. Below, a results count line, then a list of question rows built from native details and summary elements, each tagged with a data-topic attribute and showing the question, a small topic chip on the right, and an answer that reveals on open with a "Was this helpful?" thumbs row underneath. Then add the JavaScript that makes the search and pills real: filter rows on every keystroke by matching the typed text against both question and answer, intersect that with the selected topic pill, keep the results count in sync, and render an empty state when a query matches nothing. <section class="mx-auto max-w-3xl px-6 py-16">
<h2 class="text-center text-3xl font-semibold text-neutral-900">How can we help?</h2>
<div class="relative mt-6">
<span class="pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-neutral-400"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="inline-block h-[1em] w-[1em] shrink-0 align-[-0.125em] transition-transform"><circle cx="11" cy="11" r="7"/><path d="m16 16 4 4"/></svg></span>
<input id="faqSearch" placeholder="Search billing, API keys, exports…"
class="w-full rounded-2xl border border-neutral-200 bg-white py-3.5 pl-11 pr-4 text-sm shadow-sm outline-none focus:border-neutral-900 focus:ring-4 focus:ring-neutral-900/10" />
</div>
<div id="faqTopics" class="mt-4 flex gap-2 overflow-x-auto pb-1 text-sm">
<button data-topic="all" class="shrink-0 rounded-full bg-neutral-900 px-3.5 py-1.5 font-medium text-white">All</button>
<button data-topic="Billing" class="shrink-0 rounded-full border border-neutral-200 px-3.5 py-1.5 text-neutral-600 hover:bg-neutral-50">Billing</button>
<button data-topic="Accounts" class="shrink-0 rounded-full border border-neutral-200 px-3.5 py-1.5 text-neutral-600 hover:bg-neutral-50">Accounts</button>
<button data-topic="API" class="shrink-0 rounded-full border border-neutral-200 px-3.5 py-1.5 text-neutral-600 hover:bg-neutral-50">API</button>
<button data-topic="Security" class="shrink-0 rounded-full border border-neutral-200 px-3.5 py-1.5 text-neutral-600 hover:bg-neutral-50">Security</button>
</div>
<p id="faqCount" class="mt-6 text-xs text-neutral-400">Showing 3 of 3 articles</p>
<div id="faqList" class="mt-2 divide-y divide-neutral-200 rounded-2xl border border-neutral-200 bg-white">
<p id="faqEmpty" class="hidden p-8 text-center text-sm text-neutral-500">No articles match that search.</p>
<details data-topic="API" class="group p-5" open>
<summary class="flex cursor-pointer items-center justify-between gap-4 text-sm font-medium text-neutral-900 [&::-webkit-details-marker]:hidden">
<span>How do I rotate an API key without downtime?</span>
<span class="shrink-0 rounded-full bg-neutral-100 px-2 py-0.5 text-xs font-normal text-neutral-500">API</span>
</summary>
<p class="mt-3 text-sm leading-relaxed text-neutral-500">Create a second key, deploy it alongside the first, then revoke the original once traffic has fully shifted. Both keys stay valid during the overlap window.</p>
<div class="mt-4 flex items-center gap-3 text-xs text-neutral-400">
Was this helpful?
<button class="rounded-lg border border-neutral-200 px-2 py-1 hover:bg-neutral-50"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="inline-block h-[1em] w-[1em] shrink-0 align-[-0.125em] transition-transform"><path d="M7 10v11H3V10h4Zm0 9h10.2a2 2 0 0 0 1.9-1.4l1.7-5A2 2 0 0 0 18.9 10H15l.7-3.1A3.2 3.2 0 0 0 12.5 3L7 10Z"/></svg></button>
<button class="rounded-lg border border-neutral-200 px-2 py-1 hover:bg-neutral-50"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="inline-block h-[1em] w-[1em] shrink-0 align-[-0.125em] transition-transform"><path d="M7 14V3H3v11h4Zm0-9h10.2a2 2 0 0 1 1.9 1.4l1.7 5a2 2 0 0 1-1.9 2.6H15l.7 3.1a3.2 3.2 0 0 1-3.2 3.9L7 14Z"/></svg></button>
</div>
</details>
<details data-topic="Accounts" class="group p-5">
<summary class="flex cursor-pointer items-center justify-between gap-4 text-sm font-medium text-neutral-900 [&::-webkit-details-marker]:hidden">
<span>Can I move a project to another workspace?</span>
<span class="shrink-0 rounded-full bg-neutral-100 px-2 py-0.5 text-xs font-normal text-neutral-500">Accounts</span>
</summary>
<p class="mt-3 text-sm leading-relaxed text-neutral-500">Yes. Owners can transfer a project from its settings page. History, comments, and integrations move with it.</p>
</details>
<details data-topic="Billing" class="group p-5">
<summary class="flex cursor-pointer items-center justify-between gap-4 text-sm font-medium text-neutral-900 [&::-webkit-details-marker]:hidden">
<span>When exactly am I charged for extra seats?</span>
<span class="shrink-0 rounded-full bg-neutral-100 px-2 py-0.5 text-xs font-normal text-neutral-500">Billing</span>
</summary>
<p class="mt-3 text-sm leading-relaxed text-neutral-500">Seats are prorated to the day and appear on your next invoice. Removing a seat credits the unused portion automatically.</p>
</details>
</div>
</section> var search = document.getElementById("faqSearch");
var pills = [].slice.call(document.querySelectorAll("#faqTopics [data-topic]"));
var articles = [].slice.call(document.querySelectorAll("#faqList details"));
var count = document.getElementById("faqCount");
var empty = document.getElementById("faqEmpty");
var ACTIVE = "shrink-0 rounded-full bg-neutral-900 px-3.5 py-1.5 font-medium text-white";
var IDLE = "shrink-0 rounded-full border border-neutral-200 px-3.5 py-1.5 text-neutral-600 hover:bg-neutral-50";
var topic = "all";
function apply() {
var query = search.value.trim().toLowerCase();
var shown = 0;
articles.forEach(function (item) {
var matchesTopic = topic === "all" || item.getAttribute("data-topic") === topic;
// Search the answer too — people paste error text, not our headings.
var matchesQuery = !query || item.textContent.toLowerCase().indexOf(query) !== -1;
var visible = matchesTopic && matchesQuery;
item.classList.toggle("hidden", !visible);
if (visible) shown += 1;
else item.open = false;
});
empty.classList.toggle("hidden", shown > 0);
count.textContent = "Showing " + shown + " of " + articles.length + " articles";
}
pills.forEach(function (pill) {
pill.addEventListener("click", function () {
topic = pill.getAttribute("data-topic");
pills.forEach(function (p) { p.className = p === pill ? ACTIVE : IDLE; });
apply();
});
});
search.addEventListener("input", apply);
apply();