Accordion
A stack of disclosures, each a native <details> and <summary>, so a reader opens only the ones they need.
Code
The three render the same class names, the same ARIA and the same data attributes. That is checked when this site builds: a class in any of these samples that neither the markup contract nor the shipped stylesheet defines fails the build.
<div class="wsu-accordion">
<details class="wsu-accordion__item" open>
<summary class="wsu-accordion__summary">
Why do I have an advising hold?
<svg class="wsu-icon wsu-accordion__icon" aria-hidden="true"><use href="/assets/wayne-ui/icons.svg#wsu-chevron-down"/></svg>
</summary>
<div class="wsu-accordion__panel">
<p>A hold is placed automatically once you register for more than 12 credits, so an advisor can confirm the load before it is billed.</p>
</div>
</details>
<details class="wsu-accordion__item">
<summary class="wsu-accordion__summary">
Can I register for a closed section?
<svg class="wsu-icon wsu-accordion__icon" aria-hidden="true"><use href="/assets/wayne-ui/icons.svg#wsu-chevron-down"/></svg>
</summary>
<div class="wsu-accordion__panel">
<p>Only with an override from the department offering it.</p>
</div>
</details>
</div><x-wsu::accordion :items="[
['id' => 'hold', 'summary' => 'Why do I have an advising hold?', 'open' => true],
['id' => 'closed', 'summary' => 'Can I register for a closed section?'],
]">
<x-slot:hold>
<p>A hold is placed automatically once you register for more than 12 credits, so an advisor can confirm the load before it is billed.</p>
</x-slot:hold>
<x-slot:closed>
<p>Only with an override from the department offering it.</p>
</x-slot:closed>
</x-wsu::accordion><Accordion :items="[
{ id: 'hold', summary: 'Why do I have an advising hold?', open: true },
{ id: 'closed', summary: 'Can I register for a closed section?' },
]">
<template #hold>
<p>A hold is placed automatically once you register for more than 12 credits, so an advisor can confirm the load before it is billed.</p>
</template>
<template #closed>
<p>Only with an override from the department offering it.</p>
</template>
</Accordion>Why it works this way
- <details> and <summary> already carry the whole pattern: the browser maintains the open state and exposes summary as a button with an expanded state derived from [open]. Nothing here needs script or a data-wsu-* hook, unlike Tabs, which has to implement the APG pattern itself because no element gives it that for free.
- Uncontrolled by design. The open flag on an item only sets its initial state, the same as the native attribute would. Reading the state back into a v-model would mean re-rendering the element the browser itself just toggled, racing the one thing already working correctly.
- Panel content comes from a slot named after each item's id, not from a prop, so the Blade and Vue versions can hold arbitrary markup rather than a string.
Accessibility
- summary already has button semantics and its own accessible name, so there is nothing left for 4.1.2 to add.
- [open] drives the chevron and the browser's own expanded state together, so the two cannot disagree.
Success criteria in brackets refer to WCAG 2.2. Every one of them is checked by axe-core in both themes at three viewport widths before this page can be published. See Accessibility for what the gates are.
Props
Blade
| Attribute | Default | What it does |
|---|---|---|
items | [] | [{ id, summary, open }]. Panel content comes from a slot named after each id. |
Vue
| Prop | Type and default | What it does |
|---|---|---|
items | Array, [] | [{ id, summary, open }]. Panel content comes from a slot named after each id. |
In the markup contract
contract/markup-contract.json is extracted from the two static templates and every package tests against it. This is what it records for .wsu-accordion.
| Template | Elements | Accessibility attributes | Uses |
|---|---|---|---|
| internal apps/examples/static-html/index.html | div | none recorded | 1 |
| public apps/examples/static-html/public-app.html | div | none recorded | 1 |