FAQ 3
Searchable FAQ with category tabs, a live-filtered accordion, an empty state for missed queries and a support CTA strip.
Preview
Installation
npx shadcn@latest add https://hirael.com/r/faq-03.jsonCode
components/blocks/faq-03.tsx
'use client';
import * as React from 'react';
import { ArrowUpRight, LifeBuoy, Mail, Search, SearchX } from 'lucide-react';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/registry/hirael/bases/radix/ui/accordion';
import { Button } from '@/registry/hirael/bases/radix/ui/button';
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@/registry/hirael/bases/radix/ui/empty';
import { InputGroup, InputGroupAddon, InputGroupInput } from '@/registry/hirael/bases/radix/ui/input-group';
import { Tabs, TabsList, TabsTrigger } from '@/registry/hirael/bases/radix/ui/tabs';
type Category = 'getting-started' | 'billing' | 'licensing';
const CATEGORIES: { value: Category | 'all'; label: string }[] = [
{ value: 'all', label: 'All' },
{ value: 'getting-started', label: 'Getting started' },
{ value: 'billing', label: 'Billing' },
{ value: 'licensing', label: 'Licensing' },
];
const CATEGORY_LABELS: Record<Category, string> = {
'getting-started': 'Getting started',
billing: 'Billing',
licensing: 'Licensing',
};
const FAQS: readonly { q: string; a: string; category: Category }[] = [
{
q: 'How do I install my first block?',
a: "Run the shadcn CLI with the block's registry URL; the source lands in components/blocks/ and is yours to edit. No package to add, nothing to configure.",
category: 'getting-started',
},
{
q: 'Do blocks work without the rest of Hirael?',
a: 'Yes. Each block declares its own dependencies, and the CLI resolves only what that block needs. You can install a single FAQ section into an existing app.',
category: 'getting-started',
},
{
q: 'Which frameworks are supported?',
a: 'Anywhere React runs: Next.js App or Pages Router, Remix, Vite. Blocks avoid framework-specific APIs unless the block page says otherwise.',
category: 'getting-started',
},
{
q: 'Is there a paid tier?',
a: 'Everything currently published is free. If a pro tier ships later, existing blocks stay free and installed copies are unaffected.',
category: 'billing',
},
{
q: 'Do you offer team invoicing?',
a: "There's nothing to invoice today; installs are free and unmetered. For procurement paperwork, contact us and we'll sort something out.",
category: 'billing',
},
{
q: 'Can I use blocks in client work?',
a: "Yes. Copies installed into a client project belong to that codebase. There's no per-seat or per-project license to track.",
category: 'licensing',
},
{
q: 'Can I republish blocks as my own library?',
a: "Shipping products with blocks inside is encouraged; repackaging the registry itself as a competing collection isn't. When in doubt, ask.",
category: 'licensing',
},
{
q: 'Do I need to credit Hirael?',
a: 'No attribution required. A mention is appreciated but never a condition of use.',
category: 'licensing',
},
];
const Faq03 = () => {
const [query, setQuery] = React.useState('');
const [category, setCategory] = React.useState<Category | 'all'>('all');
const normalized = query.trim().toLowerCase();
const visible = FAQS.filter((f) => {
if (category !== 'all' && f.category !== category) return false;
if (!normalized) return true;
return f.q.toLowerCase().includes(normalized) || f.a.toLowerCase().includes(normalized);
});
const clearFilters = () => {
setQuery('');
setCategory('all');
};
return (
<section className="bg-background py-20 md:py-28">
<div className="mx-auto flex w-full max-w-3xl flex-col gap-10 px-6 md:px-10">
<div className="flex flex-col items-center gap-4 text-center">
<span className="font-mono text-[10px] uppercase tracking-[0.16em] text-foreground">help center</span>
<h2 className="max-w-2xl font-serif text-4xl font-medium leading-[1.04] tracking-tight sm:text-5xl md:text-6xl">
Find the answer before you file the issue.
</h2>
<p className="max-w-xl text-sm text-muted-foreground">
Search the questions we hear most, or narrow by topic. Anything unanswered lands in the inbox below.
</p>
</div>
<div className="flex flex-col items-center gap-4">
<InputGroup className="w-full max-w-md">
<InputGroupAddon>
<Search className="size-4" />
</InputGroupAddon>
<InputGroupInput
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search questions…"
aria-label="Search questions"
/>
</InputGroup>
<Tabs value={category} onValueChange={(v) => setCategory(v as Category | 'all')} className="w-fit">
<TabsList>
{CATEGORIES.map((c) => (
<TabsTrigger key={c.value} value={c.value}>
{c.label}
</TabsTrigger>
))}
</TabsList>
</Tabs>
</div>
{visible.length > 0 ? (
<Accordion type="single" collapsible className="border-y border-border">
{visible.map((f) => (
<AccordionItem key={f.q} value={f.q} className="px-1">
<AccordionTrigger>
<span className="flex flex-1 items-baseline justify-between gap-4">
<span>{f.q}</span>
<span className="shrink-0 font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground">
{CATEGORY_LABELS[f.category]}
</span>
</span>
</AccordionTrigger>
<AccordionContent>{f.a}</AccordionContent>
</AccordionItem>
))}
</Accordion>
) : (
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<SearchX />
</EmptyMedia>
<EmptyTitle>No matching questions</EmptyTitle>
<EmptyDescription>
Nothing matches “{query.trim()}”
{category !== 'all' ? ` in ${CATEGORY_LABELS[category as Category]}` : ''}. Try different keywords or
clear the filters.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button variant="outline" size="sm" onClick={clearFilters}>
Clear filters
</Button>
</EmptyContent>
</Empty>
)}
<div className="flex flex-col items-start justify-between gap-4 rounded-md border border-border bg-card/40 p-5 sm:flex-row sm:items-center sm:p-6">
<div className="flex items-start gap-3">
<span className="inline-flex size-9 shrink-0 items-center justify-center rounded-sm border border-border bg-background">
<LifeBuoy className="size-4" />
</span>
<div className="flex flex-col gap-0.5">
<span className="text-sm font-medium">Still stuck?</span>
<span className="text-xs text-muted-foreground">We answer most questions within a day.</span>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<Button variant="outline" size="sm" asChild>
<a href="mailto:support@hirael.com">
<Mail className="size-3.5" />
Email support
</a>
</Button>
<Button size="sm" asChild>
<a href="#">
Open an issue
<ArrowUpRight className="size-3.5" />
</a>
</Button>
</div>
</div>
</div>
</section>
);
};
export default Faq03;
Dependencies
shadcn registry
accordionbuttonemptyinput-grouptabs
npm
lucide-react