Photon Design System

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.

Yes, you can self-host Photon and use your own phone number. We recommend Photon Cloud, which is free for personal use with up to 10 users: a managed phone number at no additional cost, and a fully managed experience.

<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.

Cards through Stripe, plus invoicing on the Business tier.

Cards through Stripe, plus invoicing on the Business tier.

Cards through Stripe, plus invoicing on the Business tier.
<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.

Cards through Stripe, plus invoicing on the Business tier.

<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; lg is 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 (default true) — several panels open at once; false for one at a time
  • disabled — boolean (default false) — disable every item
  • keepMounted — boolean (default false) — keep collapsed panels in the DOM
  • hiddenUntilFound — boolean (default false) — keep panels findable by the browser’s in-page search (implies keepMounted)

Set on Accordion.Item:

  • value — any — unique identifier for the item; auto-generated if omitted
  • disabled — boolean (default false) — 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 lg list; a settings page puts an md list in a Card.
  • Write triggers as questions or noun phrases that stand alone in a screen reader’s heading list; the panel should answer the header.