Content & disclosure
FAQ
Searchable FAQ accordion with categories
detent-faq
Port status
- Catalog status
- implemented
- Origin
- upstream
- Phase
- 2
- Wave
- phase-2-disclosure
- Group
- Content & disclosure
6.7 KB gzip for the class module
Variants
In the searchable FAQ above, type in the search field to filter, use the pills to narrow by category, and press Enter or Space on a header to expand it. With reduced motion the panel and the indicator jump straight to their final state.
Recorded states
focus-visiblerecorded state; no setteropenrecorded state; no setterexpandedrecorded state; no setterreduced motionrecorded state; no setter
Usage
Import
import '/components/faq/define.js';
// or register it yourself
import { DetentFAQ } from '/components/faq/faq.js';
Attributes
allow-multipleicon-stylesearch-placeholdersearchableshow-categoriesvariant
Properties
itemsopenIdsdefaultOpenrenderIcononOpenChange
Slots
No slots are recorded for this component.
Events
detent-open-change— { ids: string[] }
Example
Runnable example: site/examples/faq.ts. The snippet below is generated from the catalogued attributes.
<detent-faq
allow-multiple
icon-style="chevron"
search-placeholder
searchable
show-categories
variant="unified"
></detent-faq>
Property-only values
const element = document.querySelector('detent-faq');
if (element !== null) {
// items: FAQItem[]
element.items = [];
// defaultOpen: string[] | string
element.defaultOpen = undefined;
// openIds: string[]
element.openIds = [];
// onOpenChange: (ids: string[]) => void
element.onOpenChange = () => {};
}
Source
The pinned upstream source for this row:
src/components/ui/FAQ.tsxsha256 f2d731262db1…
Native files
The native module and its artifacts:
src/components/faq/faq.tssrc/components/faq/styles.csssrc/components/faq/define.tsdocs/components/faq.mdsite/examples/faq.tstests/browser/faq.spec.tsevidence/components/faq.md
API
Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.
Attributes
| Attribute | Property | Type | Default | Notes |
|---|---|---|---|---|
allow-multiple | allowMultiple | boolean | false | Presence semantics: present means true, absent means false. |
icon-style | iconStyle | 'chevron' | 'plus-minus' | 'custom' | 'chevron' | Kebab-case attribute; camelCase property. |
search-placeholder | — | — | — | Recorded in the catalog as an attribute. |
searchable | searchable | boolean | false | Presence semantics: present means true, absent means false. |
show-categories | showCategories | boolean | false | Presence semantics: present means true, absent means false. |
variant | variant | 'unified' | 'separated' | 'unified' | Kebab-case attribute; camelCase property. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
items | FAQItem[] | [] | Property-only: assign from script; not accepted as an attribute. |
openIds | string[] | undefined | Property-only: assign from script; not accepted as an attribute. |
defaultOpen | string[] | string | undefined | Property-only: assign from script; not accepted as an attribute. |
renderIcon | — | — | Recorded in the catalog without a top-level type; pinned from source when the row is ported. |
onOpenChange | (ids: string[]) => void | undefined | Property-only: assign from script; not accepted as an attribute. |
Events
| Event | Upstream prop | Payload type | Notes |
|---|---|---|---|
detent-open-change | onOpenChange | (ids: string[]) => void | Composed, bubbles; fires only for a user action. |
Slots
No slots are recorded for this component.
CSS customisation
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- `answer` is a `string` rendered as text, not a React node (no slots recorded, PC-11.1).
- `item.icon` is not ported; the icon surface is `icon-style` plus `render-icon`.
- `render-icon` returns `string | Element | null`; strings are inserted as text.
- Categories render only when more than one category exists (upstream parity), and the selected pill stays valid when `items` changes.
- The `openIds` getter returns the effective ids; assigning `undefined` opts out of controlled mode.
What it does
A searchable, category-filterable FAQ accordion with spring-style height expansion, single- or multi-open state, and a unified or separated card layout.
Upstream behaviour
- Smooth height calculation and rotation via Framer Motion springGentle
- Single-open accordion or multi-open simultaneous expansion modes
- Controlled and uncontrolled state management (openIds / defaultOpen)
- Integrated search filter bar and optional category filter pills
- Two layout modes: Unified grouped card or separated individual cards
- Full keyboard accessibility (Space, Enter, Tab) and ARIA attributes
Differences from upstream
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- `answer` is a `string` rendered as text, not a React node (no slots recorded, PC-11.1).
- `item.icon` is not ported; the icon surface is `icon-style` plus `render-icon`.
- `render-icon` returns `string | Element | null`; strings are inserted as text.
- Categories render only when more than one category exists (upstream parity), and the selected pill stays valid when `items` changes.
- The `openIds` getter returns the effective ids; assigning `undefined` opts out of controlled mode.