Details FAQ
Questions on native details elements: keyboard and screen-reader friendly, and working without JavaScript.
npx shadcn@latest add @vibebox/faq-detailsControls
<FaqDetails />About
Rebuilds the site kit's FAQ. The free default (and only) FAQ block: native details do the job better than any script. Visible questions also feed FAQPage JSON-LD in assembled apps.
Use it when
- Customers ask the same things before they book or buy.
Skip it when
- There are fewer than four real questions: answer them in the services copy.
Install
- 1Add the VibeBox registry to
components.json:{ "registries": { "@vibebox": "https://motion.vibeboxph.com/r/{name}.json" } } - 2Add the block with the shadcn CLI:
npx shadcn@latest add @vibebox/faq-details - 3Use it:
import { FaqDetails } from "@/components/vibebox/faq-details"; import { home } from "@/content/pages/home"; <FaqDetails content={home.faq} variant="split" id="faq" />
Code
2 filescomponents/vibebox/faq-details/index.tsx
/**
* VibeBox Motion · Details FAQ
* Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
* https://motion.vibeboxph.com/license
* Use it in unlimited personal, commercial and client projects.
* Do not sell or redistribute it as a component, kit, template or builder.
*/
import { Container, headingId, Icon, REVEAL, RichTextView, Section, SectionHead, SlotText, TextLink, type BlockProps } from "@/lib/vibebox/block-kit";
import { cn } from "@/lib/utils";
import type { Content, Variant } from "./schema";
/*
* Questions on native <details>: they open and close without JavaScript, by keyboard and screen
* reader, and the first one starts open. Answers are rich text and can hold a fill-in for a fact
* still to come.
*/
export type FaqDetailsProps = BlockProps<Content, Variant>;
export function FaqDetails({ content, variant = "split", id, tone, className }: FaqDetailsProps) {
const split = variant === "split";
return (
<Section slug="faq-details" variant={variant} id={id} tone={tone} labelledBy={headingId(id)} className={className}>
<Container className={cn("grid gap-[clamp(32px,4vw,56px)]", split && "lg:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)] lg:gap-20")}>
<SectionHead eyebrow={content.eyebrow} heading={content.heading} lead={content.lead} id={id} align={split ? "start" : "center"} className={cn(split && "lg:sticky lg:top-24 lg:self-start")}>
{content.action && <TextLink link={content.action} path="action" className="mt-1" />}
</SectionHead>
<div className={cn("grid border-t border-border", !split && "mx-auto w-full max-w-3xl")}>
{content.items.map((item, index) => (
<details key={index} open={index === 0} className={cn("group/faq border-b border-border", REVEAL)}>
<summary className="flex cursor-pointer list-none items-start justify-between gap-6 py-5 text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring [&::-webkit-details-marker]:hidden">
<SlotText value={item.question} path={`items.${index}.question`} className="text-[1.075rem] font-semibold tracking-tight" />
<span aria-hidden="true" className="mt-0.5 inline-grid size-7 shrink-0 place-items-center rounded-full ring-1 ring-border-strong transition-transform duration-(--vb-duration-reveal) ease-expo-out group-open/faq:rotate-45">
<Icon name="plus" className="size-4" />
</span>
</summary>
<RichTextView value={item.answer} path={`items.${index}.answer`} className="max-w-[62ch] pr-12 pb-6 leading-relaxed text-pretty text-muted-foreground" />
</details>
))}
</div>
</Container>
</Section>
);
}components/vibebox/faq-details/schema.ts
/**
* VibeBox Motion · Details FAQ
* Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
* https://motion.vibeboxph.com/license
* Use it in unlimited personal, commercial and client projects.
* Do not sell or redistribute it as a component, kit, template or builder.
*/
import { sections, type z } from "@/lib/vibebox/content";
export const variants = ["split", "stacked"] as const;
export type Variant = (typeof variants)[number];
export const content = sections.faq;
export type Content = z.infer<typeof content>;Content
Typed slots from schema.ts. Facts (hours, prices, quotes, names) come only from the business; until then they render as fill-ins.
| Slot | Label | Type | Limit | Fact |
|---|---|---|---|---|
| eyebrow | Kicker | text | 32 chars | — |
| heading | Heading | text | 60 chars | — |
| lead | Intro | text | 160 chars | — |
| items | Questions | list | up to 8 | — |
| items[].question | Question | text | 100 chars | — |
| items[].answer | Answer | richText | 400 chars | — |
| action | Button | action | 24 chars | — |
Variants
Splitdefault
split
Heading and action left (sticky on desktop), questions right.
Stacked
stacked
Centred heading, questions beneath in one column.
Accessibility and motion
- Keyboard
- Tab to a question; Enter or Space opens and closes it.
- Screen readers
- Native disclosures that announce expanded or collapsed; answers follow their questions.
- Touch
- Whole question rows are tap targets.
- Reduced motion
- The plus icon does not rotate.
Edge cases and limits
- Eight 400-character answers: the page grows; questions stay scannable.
- Answers never take HTML strings.
Related

About Story
FreeThe business in its own words: a tall photo with a caption beside the story and a few honest details.
About block · Server

Bento Hero
FreeThe promise beside a three-tile mosaic: a tall photo, a second image and a card of what the business offers.
Heroes block · Server

Booking Band
FreeWhen, how long and one clear booking action, on a tinted band or a centred card.
Booking block · Server