Canvas Design System
Main Site Tokens

Accordion

Collapses a list of headings so readers can scan first and expand only what they need.

When to Use

Use when: You have FAQs, step-by-step instructions, or a long list of independent sections readers will scan and sample. Accordion cuts page length without hiding anything from search or the DOM.
Don't use when: All the content should be visible at once (a short form), or there are only two items — show those inline. For switching between mutually exclusive views, use Tabs.

Variants

Default

Divider-separated rows. Opening one panel closes the others.

Workshopr is a platform for workshop facilitators featuring a content library and AI Facilitation Coach.

<?= ws_accordion([
    ['title' => 'Question 1', 'content' => 'Answer 1', 'open' => true],
    ['title' => 'Question 2', 'content' => 'Answer 2'],
]) ?>

Bordered

Each panel becomes a discrete card. Use when the accordion sits directly on a page background rather than inside a container.

<?= ws_accordion($items, ['variant' => 'bordered']) ?>

Multiple open

Set multiple: true when panels are independent and readers may want to compare them side by side.

This accordion allows multiple panels to be open simultaneously.

Click to open without closing the other panels.

<?= ws_accordion($items, ['multiple' => true]) ?>

With icons

A per-item Material icon adds category context. Keep icons consistent across the set — a mix of iconed and plain rows reads as an error.

<?= ws_accordion([
    ['title' => 'Design Sprint', 'content' => '...', 'icon' => 'design_services'],
    ['title' => 'Retrospective', 'content' => '...', 'icon' => 'history'],
], ['variant' => 'bordered']) ?>

States

StateBehavior
CollapsedPanel carries hidden; trigger reports aria-expanded="false". The content stays in the DOM.
OpenItem gains .is-open, hidden is removed, aria-expanded="true". Fires a bubbling ws-accordion-open event.
ClosingFires ws-accordion-close. In single-open mode, closing the siblings happens before the new panel opens.
DisabledItem gains .is-disabled and the trigger's click handler returns early. Set per item, not per accordion.
info Both events bubble, so you can listen once on the container rather than per item — useful for analytics on which FAQ entries get opened.

Real-World Usage

The pricing FAQ pattern: single-open (so the page length stays stable), bordered (it sits on the page background), first item closed so the reader sees the full question list before committing to one.

<?= ws_accordion($faqItems, [
    'id'      => 'pricing-faq',
    'variant' => 'bordered',
]) ?>

Options

Accordion options

OptionTypeDefaultPurpose
idstring'ws-accordion-' . uniqid()Container ID. Auto-generated, but set it explicitly if you need to target the accordion from JS.
variantstring'default'default | bordered | flush
sizestring'md'sm | md | lg — drives trigger padding and title size
multipleboolfalseAllow several panels open at once. Emitted as data-multiple and read by the JS at init.
classstring''Additional CSS classes

Item options

OptionTypeDefaultPurpose
titlestring''Trigger text. Write it as the question the reader is actually asking.
contentstring''Panel body. Rendered as HTML, so wrap prose in <p>.
openboolfalseInitially expanded. In single-open mode, marking more than one leaves them all open until the first interaction.
iconstringnullMaterial icon name, rendered before the title
disabledboolfalsePrevents toggling

Accessibility

Structurally this follows the WAI-ARIA Accordion pattern. The wiring below is what the component actually ships — see the gap note at the end.

ConcernBehavior
ARIATriggers carry aria-expanded (kept in sync by wsAccordionOpen/Close) and aria-controls pointing at their panel. Panels are role="region" with aria-labelledby back to the trigger.
KeyboardTab moves between triggers; Enter and Space toggle the focused panel. Both come free from using real <button> elements — no JS key handling is involved.
FocusFocus stays on the trigger when a panel opens, so the next Tab enters the revealed content. Collapsed panels use hidden, which removes their contents from the tab order.
Screen readersCollapsed content stays in the DOM under hidden rather than being destroyed, so the accordion's structure is traversable and the expanded/collapsed state is announced.
Reduced motionNo JS-driven animation; the panel show/hide is an attribute toggle. Nothing to suppress.
warning Known gap: the APG pattern's optional roving arrow-key navigation (//Home/End moving between triggers) is not implemented — wsAccordionInit() binds click only. This page previously documented those keys as working; they never were. Tab-based navigation is conformant, so this is an enhancement rather than a WCAG failure.

Tokens

TokenUsed for
--gray-50Trigger hover fill
--gray-200Item dividers and the bordered variant's outline
--gray-400Chevron in its resting state
--mainsite-primaryLeading icon accent
--radius-xlBordered-variant card corners
--text-default / --text-darkTitle and body text
--text-h3 / --text-body / --text-ui / --text-small / --text-metaType scale across the size modifiers
--font-body / --font-semiboldBody face and trigger weight
--transition-base / --transition-fastChevron rotation and hover fill

CSS Classes

ClassPurpose
.ws-accordionOuter container. Auto-initialised on DOMContentLoaded.
.ws-accordion--borderedCard-per-panel variant
.ws-accordion--flushBorderless variant
.ws-accordion--sm / --lgSize modifiers (md is the default and emits nothing)
.ws-accordion__itemPanel wrapper
.ws-accordion__item.is-openExpanded state — the hook JS toggles
.ws-accordion__item.is-disabledDisabled state
.ws-accordion__triggerThe <button> header
.ws-accordion__iconOptional leading icon
.ws-accordion__titleHeading text
.ws-accordion__arrowExpand/collapse chevron
.ws-accordion__panelCollapsible role="region" container
.ws-accordion__contentPadding wrapper inside the panel

Files

FilePurpose
includes/components/helpers.phpws_accordion() helper function
includes/components/accordion.phpTemplate — markup and ARIA wiring
includes/components/components.cssStyles (.ws-accordion rules)
includes/components/components.jswsAccordionInit(), wsAccordionOpen/Close/Toggle() and the auto-init