Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion gulp/helpers/markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,21 @@ module.exports = rootDir => filePath => {
let example
let insertToken
let exampleLinkClass
const tokenTextContent = token.children
.filter(child => child.type === 'text')
.map(child => child.content)
.join(' ')
const hasLegacyMarker = /\blegacy\b/i.test(tokenTextContent)

token.children.some((childToken, childIdx) => {
exampleLink = examples.getLink(childToken)

// Add title
// Add example embedding (unless line contains "legacy" keyword)
if (exampleLink) {
if (hasLegacyMarker) {
return false
}

examplePath = path.isAbsolute(exampleLink)
? path.join(rootDir, exampleLink)
: path.resolve(path.dirname(filePath), exampleLink)
Expand Down
67 changes: 37 additions & 30 deletions pages/examples/widgets/accordion/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,24 @@ position: 8

# Accordions

**Accordions contain of a number of content panels, each of wich can be expanded or collapsed vertically by the user.**
**Accordions consist of a number of content panels, each of which can be expanded or collapsed vertically by the user.**

[[_TOC_]]

Accordions help to save vertical space and prevent from visual noise. Some accordions allow only a single panel to be expanded at a time, others allow multiple.
Accordions help to save vertical space and reduce visual noise. Some accordions allow only a single panel to be expanded at a time, others allow multiple.

![Accordion](_media/accordion.png)

Before you continue, please read [Tablist widgets (or: tab panels, tabs)](/examples/widgets/tablists) to understand why accordions simply are extended variants of tablists, providing a slightly different layout and (sometimes) expandability of multiple panels.

## General requirements

The following requirements are based on well established best practices and [WAI-WAI-ARIA Authoring Practices: Accordion (widget)](https://www.w3.org/TR/wai-aria-practices/#accordion).
The following requirements are based on well established best practices and the [WAI-ARIA Authoring Practices Guide (APG): Accordion Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/).

In addition to the tablists’ requirements, and besides many other requirements, we want to stress out explicitly the following:
Accordions and [Tablists](/examples/widgets/tablists) share the same underlying logic: a trigger (header/tab) controls the visibility of a content panel. While they are structurally similar, accordions have specific requirements:

- Multiple slides can be visible (optional).
- Multiple panels can be visible at the same time (optional).
- Keyboard support: Users can navigate between accordion headers using `Tab` and toggle them with `Enter` or `Space`. (Optional but recommended: Arrow key navigation).

## Proofs of concept

Expand All @@ -35,42 +36,48 @@ It is relatively simple to create a custom accordion implementation with ARIA:

#### Implementation details

A link with an `aria-expanded="true"` attribute is placed around each panel’s header; its value (`true`/`false`) and the visibility of the corresponding panel is toggled using JavaScript. See [Marking elements expandable using aria-expanded](/examples/sensible-aria-usage/expanded).
This implementation follows the current APG approach and uses a real `button` in each header.

While this may feel tempting in some circumstances, there are several drawbacks:
- The button toggles `aria-expanded` (`true`/`false`).
- The button uses `aria-controls` to reference the associated panel.
- The panel uses `role="region"` and `aria-labelledby` to expose a clear relationship back to the controlling header button.
- The panel visibility is synchronized with the semantic state using JavaScript.

- It needs more JavaScript (instead of relying on browser standard behaviour).
- The current implementation allows multiple elements to be open. If you wanted to restrict it to one element though, a lot of additional JavaScript would be needed to manage states - something that radio buttons would offer "for free".
- This solution is less intuitive: a screen reader announcement "link X collapsed" is less expressive than "show panel X checkbox not checked" or "show panel X radio button not checked 2 of 3".
- Missing backwards compatibility for older clients with incomplete/missing ARIA support.
### Native HTML Disclosure Elements

### Radio buttons implementation
For simple disclosure-like use cases, the native HTML `<details>` and `<summary>` elements are a solid, no-JavaScript option. You can find the technical specification and browser behavior in the [MDN Web Docs for the Details element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details).

This implementation is based on the [tablists’ proof of concept](/examples/widgets/tablists/#proof-of-concept), only the layout is different.
- The `summary` element works as the interactive header.
- The surrounding `details` element manages the expanded/collapsed state natively.
- This removes the need for JavaScript compared to custom ARIA widgets.

[Example](_examples/accordion-with-radio-buttons)
[Example](_examples/accordion-with-details-summary)

#### Implementation details
#### Implementation details

Some interesting peculiarities:
This implementation follows the current APG approach and uses a real `button` in each header.

- Using `.accordion:focus-within .control label`, a style can be applied to all radio button labels upon interacting with the accordion.
- This gives users a clue that they are interacting with a single control now (indicating to use the `Arrow` keys instead of `Tab` to navigate through accordion items).
- If you would rather like to make each control focusable on its own, you could use a group of checkboxes instead of radio buttons.
- Do not forget to make sure only one of them is checked at a time though (using some JavaScript).
- The button toggles `aria-expanded` (`true`/`false`).
- The button uses `aria-controls` to reference the associated panel.
- The panel uses `role="region"` and `aria-labelledby` to expose a clear relationship back to the controlling header button.
- **Keyboard Navigation:** Implementation should ideally support Arrow keys (`Up`/`Down`) to move focus between headers, and `Home`/`End` to jump to the first/last header.

### Checkboxes implementation

This implementation is based on the [tablists’ proof of concept](/examples/widgets/tablists/#proof-of-concept), with a slightly different layout:
### Comparison: ARIA vs. Native HTML

[Example](_examples/multi-accordion-with-checkboxes)
#### Comparison of Implementation Methods

#### Implementation details
| Implementation Method | Advantages | Limitations |
| :--- | :--- | :--- |
| **Custom ARIA Implementation** | <ul><li>Full control over keyboard behavior (e.g. arrow keys).</li><li>Better for complex layouts/nested widgets.</li><li>Exact state control via JS.</li></ul> | <ul><li>Requires JavaScript for state and interaction.</li><li>Higher maintenance (must handle all ARIA states manually).</li></ul> |
| **Native Disclosure Elements (`<details>`)**| <ul><li>Works without JavaScript.</li><li>Native accessibility "out of the box".</li><li>Minimal code footprint.</li></ul> | <ul><li>Limited styling options.</li><li>No native support for "only one open" (requires JS).</li><li>Default keyboard support is limited to Tab/Space/Enter.</li></ul> |


### Legacy implementations (Historical)

**Note:** The following legacy variants are deprecated and provided for historical reference only.

Some interesting peculiarities:
For all new projects, use one of the recommended implementations above (ARIA or native `<details>`/`<summary>`, depending on your requirements).

- Checkboxes replace the radio buttons to offer multiple selection.
- We waived using a `<fieldset>`/`<legend>` structure, as this is no traditional group of checkboxes, and JAWS tends to be very wordy with focusable items nested within those, see [Grouping form controls with fieldset and legend](/examples/forms/grouping-with-fieldset-legend).
- By default, only the `Space` key is used to toggle a checkbox (while pressing `Enter` submits a form).
- To make it more intuitive for visual users (who do not know about any checkbox behind the scenes, and thinking they are interacting with a link or button), the `Enter` key was re-wired to also toggle the checkboxes.
- In contrast to the radio button solution above, we omitted a visual `.accordion:focus-within .control label` state for the accordion items, as checkboxes are individual controls and (thereby accessed by the `Tab` key, as most users would expect).
[Accordion with radio buttons](_examples/accordion-with-radio-buttons) *(Legacy — for reference only)**
[Multi accordion with checkboxes](_examples/multi-accordion-with-checkboxes) *(Legacy — for reference only)*
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: "Accordion with ARIA"
title: "Accordion with ARIA (APG pattern)"
compatibility:
Keyboard only:
status: pass
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,60 +1,82 @@
*:focus {
outline: 2px dotted black;
body {
padding: 20px;
font-family: system-ui, sans-serif;
}

.visually-hidden {
position: absolute;
margin: -1px;
border: 0;
padding: 0;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
.accordion-widget > h2 {
margin: 0 0 12px;
}

.accordion button[aria-controls] {
.accordion-item {
margin: 0;
}

.accordion-trigger {
appearance: none;
all: inherit;
display: block;
background: none;
font: inherit;
color: inherit;
display: flex;
align-items: center;
box-sizing: border-box;
margin: 0 0 -1px 0;
margin: 0;
border: 1px solid black;
padding: 4px 10px;
padding: 6px 12px;
width: 100%;
background-color: lightyellow;
cursor: pointer;
text-align: left;
}

.accordion button[aria-controls]:hover {
text-decoration: underline;
.accordion-trigger:hover {
background: #fff9c4;
}

.accordion *:focus,
.accordion button[aria-controls]:focus {
outline: 2px dotted red;
.accordion-trigger:focus-visible {
outline: 2px dotted;
outline-offset: 2px;
}

.accordion button[aria-controls]::before {
content: "-";
display: inline-block;
width: 0.75em;
.indicator {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1rem;
flex: 0 0 1rem;
margin-right: 8px;
font-weight: normal;
}

.accordion button[aria-controls][aria-expanded=false]::before {
content: "+";
.indicator-closed {
display: none;
}

.header {
margin-bottom: 0;
.accordion-trigger[aria-expanded='false'] .indicator-open {
display: none;
}

.panel {
.accordion-trigger[aria-expanded='false'] .indicator-closed {
display: inline-block;
}

.accordion-header {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}

.accordion-panel {
border: 1px solid black;
border-top-color: lightyellow;
padding: 0 0 0 10px;
background-color: lightyellow;
border-top: none;
padding: 1rem;
background: lightyellow;
}

.accordion-trigger[aria-expanded='true'] {
background: lightyellow;
}

a:focus-visible,
button:focus-visible {
outline: 2px dotted;
outline-offset: 2px;
}
Original file line number Diff line number Diff line change
@@ -1,58 +1,30 @@
class AdgAccordion {
constructor(el) {
this.element = el
this.triggers = this.element.querySelectorAll('[aria-controls]')
this.triggers = Array.from(
this.element.querySelectorAll('.accordion-trigger')
)
this.initTriggers()
}

initTriggers() {
this.triggers.forEach((trigger, triggerIndex, triggerArray) => {
this.triggers.forEach(trigger => {
const panelId = trigger.getAttribute('aria-controls')
const panel = document.getElementById(panelId)

this.hide(panel, trigger)
const isExpanded = trigger.getAttribute('aria-expanded') === 'true'
panel.hidden = !isExpanded

trigger.addEventListener('click', () => {
if (panel.hidden) {
this.show(panel, trigger)
} else {
this.hide(panel, trigger)
}
})

trigger.addEventListener('keydown', event => {
if (trigger === document.activeElement) {
let focusTarget
switch (event.keyCode || event.key) {
case 38:
case 'Up':
case 'ArrowUp':
focusTarget =
triggerIndex > 0 ? triggerIndex - 1 : triggerArray.length - 1
break
case 40:
case 'Down':
case 'ArrowDown':
focusTarget =
triggerIndex < triggerArray.length - 1 ? triggerIndex + 1 : 0
break
}
if (triggerArray[focusTarget]) {
triggerArray[focusTarget].focus()
}
}
this.toggle(panel, trigger)
})
})
}

show(panel, trigger) {
panel.hidden = false
trigger.setAttribute('aria-expanded', 'true')
}

hide(panel, trigger) {
panel.hidden = true
trigger.setAttribute('aria-expanded', 'false')
toggle(panel, trigger) {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true'
trigger.setAttribute('aria-expanded', String(!isExpanded))
panel.hidden = isExpanded
}
}

Expand Down
Loading