Accordion
Questions that each reveal an answer: the FAQ on photon.codes, as a
component. Built on Base UI Accordion. The list is bare, so the page
decides the surface; on the site it is the section’s card, in a settings
page a Card. Every item is independent, so several can be open at once.
Import
import { Accordion } from "@photon-ai/pho-ui/components/accordion";
Basic
Each Accordion.Item carries a value. Set the initially open items with
defaultValue (an array), or drive the open set with value / onValueChange.
The trigger takes the question and wears the chevron itself; the panel takes
the answer.
<Accordion.Root defaultValue={["self-host"]}>
<Accordion.Item value="self-host">
<Accordion.Header>
<Accordion.Trigger>Can I self-host Photon?</Accordion.Trigger>
</Accordion.Header>
<Accordion.Panel>Yes, you can self-host Photon…</Accordion.Panel>
</Accordion.Item>
</Accordion.Root>
Sizes
Three sizes, sm, md, lg, one step of the type scale apart; the default
is md. lg is the site’s FAQ to the pixel: the question at 20/28, the
answer at 18/28, 20px above and below, 8px between. The chevron is the same
small glyph in the same 24px box at every size.
<Accordion.Root size="sm">…</Accordion.Root>
<Accordion.Root size="md">…</Accordion.Root>
<Accordion.Root size="lg">…</Accordion.Root>
Single open
Items are independent by default. Set multiple={false} and one panel opens
as another closes; defaultValue (and value) still hold an array.
<Accordion.Root multiple={false} defaultValue={["payment"]}>
{/* items */}
</Accordion.Root>
Motion
The site’s springs, sampled as CSS easings. Opening, the panel’s height,
the chevron’s half turn, and the question’s tone settle together on the
section spring (--ease-pho-spring-reveal, stiffness 436, damping 42,
500ms). The answer arrives 100ms later on the appear spring
(--ease-pho-spring-appear, 417 and 68, 700ms), fading in as it rises
20px. Closing, the answer is gone at once and the height collapses on the
section spring.
Anatomy
- Accordion.Root — the list; owns the size and the open set:
size,multiple,value/defaultValue - Accordion.Item — one question, identified by
value, padded above and below - Accordion.Header → Accordion.Trigger — the
<h3>heading and its toggle button, with the chevron - Accordion.Panel — the answer, animated
Props
Set on Accordion.Root:
- size —
"sm" | "md" | "lg"(default"md") — type, padding, and gap step together;lgis the size of the site’s FAQ - value —
Value[]— controlled array of open item values - defaultValue —
Value[]— uncontrolled initial open items - onValueChange —
(value, eventDetails) => void— fires when the open set changes - multiple —
boolean(defaulttrue) — several panels open at once;falsefor one at a time - disabled —
boolean(defaultfalse) — disable every item - keepMounted —
boolean(defaultfalse) — keep collapsed panels in the DOM - hiddenUntilFound —
boolean(defaultfalse) — keep panels findable by the browser’s in-page search (implieskeepMounted)
Set on Accordion.Item:
- value —
any— unique identifier for the item; auto-generated if omitted - disabled —
boolean(defaultfalse) — disable this item - onOpenChange —
(open, eventDetails) => void— fires when this item opens or closes
Accordion.Trigger renders a native <button>; Accordion.Panel accepts
keepMounted and hiddenUntilFound to override the Root defaults per item.
Accessibility
Each Accordion.Header renders an <h3> that wraps the Accordion.Trigger
<button>. The trigger carries aria-expanded and aria-controls pointing at
its panel; the Accordion.Panel is a role="region" labelled by its trigger
via aria-labelledby. Keep the surrounding heading levels sensible — if <h3>
is wrong for the page, re-render the header as another heading with render.
Keyboard:
- Tab / Shift + Tab — move between triggers
- Enter / Space — toggle the focused trigger
Base UI follows the updated APG guidance that removed roving focus, so triggers
sit in the normal tab order — there is no arrow-key navigation between headers.
A collapsed panel is removed from the accessibility tree (and the DOM, unless
keepMounted or hiddenUntilFound is set).
Best practices
- Reach for an accordion when content is long and scannable by heading (FAQs, settings groups).
- Leave items independent for an FAQ, where readers compare answers; set
multiple={false}when the page should stay short. - Hand the panel the answer as text or paragraphs; it sets the type and the gap under the question, and the item pads the bottom.
- Give the list its surface from outside: the site’s FAQ card is 32px of
padding around an
lglist; a settings page puts anmdlist in aCard. - Write triggers as questions or noun phrases that stand alone in a screen reader’s heading list; the panel should answer the header.