Accordion

These are my notes on accordions.

ChatGPT

When building an accordion that complies with WCAG 2.2 (Web Content Accessibility Guidelines), you need to ensure it is:

  • Perceivable
  • Operable
  • Understandable
  • Robust

Here’s a breakdown of what that means specifically for an accordion component:


✅ Accessibility Requirements for an Accordion (WCAG 2.2)

1. Keyboard Accessibility (Operable – [2.1.1 Keyboard], [2.1.2 No Keyboard Trap])

  • Tab: Moves focus between accordion headers.
  • Enter or Space: Expands or collapses the focused panel.
  • Arrow keys (optional but recommended): Navigate between headers (Up/Down or Left/Right). This helps users who navigate using a keyboard or assistive tech.

2. ARIA Roles and Attributes (Robust – [4.1.2 Name, Role, Value])

Use ARIA to communicate the state and structure to assistive technologies:

  • role="button" (if using a non-button element like a <div>).
  • aria-expanded="true|false": Indicates whether the panel is open.
  • aria-controls="panel-id": Links header to its content panel.
  • The panel should have role="region" and aria-labelledby="header-id".
Example:
<div class="accordion">
  <div id="section1-header" class="accordion-header" role="button"
       aria-expanded="false" aria-controls="section1-panel" tabindex="0">
    Section 1
  </div>
  <div id="section1-panel" class="accordion-panel" role="region"
       aria-labelledby="section1-header" hidden>
    Content for section 1.
  </div>
</div>

3. Focus Indicators (Visible Focus – [2.4.7])

  • Ensure visible focus styles are present when navigating with a keyboard.
  • Don’t remove the browser’s default focus outlines unless replacing them with a custom visible alternative.

4. Headings and Structure (Perceivable – [1.3.1 Info and Relationships])

  • Use appropriate heading levels (<h2>, <h3>, etc.) inside or alongside headers for document structure and screen reader users.

5. Visible Labels Match Accessible Names ([2.5.3 Label in Name])

  • The visual label (text on the button/header) should be part of the accessible name (for screen readers).

6. No Motion Triggers (if animation is present) ([2.3.3 Animation from Interactions] – new in WCAG 2.2)

If the accordion expands with an animation (e.g., slide down), and that animation lasts more than 250ms, users must be able to disable it unless it’s essential.

  • Use CSS media queries like:
@media (prefers-reduced-motion: reduce) {
  .accordion-panel {
    transition: none;
  }
}

✅ Best Practices

  • Use native <button> elements if possible instead of divs – less ARIA needed.
  • Set hidden on collapsed panels for screen readers.
  • Consider using aria-disabled="true" if a panel should not be interactable.
  • Support for screen reader users with live region updates is not necessary for static accordions.

You’ll create a custom block plugin that:

  • Registers a wcag/accordion block
  • Uses Gutenberg’s block editor interface
  • Allows adding accordion items via a repeater UI
  • Outputs accessible HTML with proper ARIA, keyboard nav, and WCAG 2.2 compliance

✅ What You Need

  1. A small plugin (you can drop into /wp-content/plugins/)
  2. Editor-side JavaScript (React-based)
  3. Front-end CSS/JS (optional enqueue)

✅ Step-by-Step Setup

📁 Folder Structure

/wp-content/plugins/wcag-accordion-block/
│
├── wcag-accordion-block.php
├── block.js
├── style.css
└── script.js

✅ 1. wcag-accordion-block.php (Main plugin file)

<?php
/**
 * Plugin Name: WCAG Accordion Block
 * Description: A WCAG 2.2 compliant accordion block for the WordPress block editor.
 * Version: 1.0
 * Author: You
 */

defined('ABSPATH') || exit;

// Register block
function wcag_register_accordion_block() {
    wp_register_script(
        'wcag-accordion-block-editor',
        plugins_url('block.js', __FILE__),
        ['wp-blocks', 'wp-element', 'wp-editor', 'wp-components'],
        null,
        true
    );

    wp_register_style(
        'wcag-accordion-block-style',
        plugins_url('style.css', __FILE__),
        [],
        null
    );

    wp_register_script(
        'wcag-accordion-block-frontend',
        plugins_url('script.js', __FILE__),
        [],
        null,
        true
    );

    register_block_type('wcag/accordion', [
        'editor_script'   => 'wcag-accordion-block-editor',
        'style'           => 'wcag-accordion-block-style',
        'render_callback' => 'wcag_render_accordion_block',
    ]);

    wp_enqueue_script('wcag-accordion-block-frontend');
}
add_action('init', 'wcag_register_accordion_block');

function wcag_render_accordion_block($attributes) {
    if (!isset($attributes['items']) || !is_array($attributes['items'])) {
        return '';
    }

    ob_start();
    ?>
    <div class="wcag-accordion">
        <?php foreach ($attributes['items'] as $index => $item): 
            $heading_id = 'accordion-heading-' . $index;
            $panel_id = 'accordion-panel-' . $index;
        ?>
            <div class="accordion-item">
                <h3>
                    <button type="button"
                            class="accordion-header"
                            aria-expanded="false"
                            aria-controls="<?php echo esc_attr($panel_id); ?>"
                            id="<?php echo esc_attr($heading_id); ?>">
                        <?php echo esc_html($item['title']); ?>
                    </button>
                </h3>
                <div id="<?php echo esc_attr($panel_id); ?>"
                     class="accordion-panel"
                     role="region"
                     aria-labelledby="<?php echo esc_attr($heading_id); ?>"
                     hidden
                     aria-hidden="true">
                    <?php echo wp_kses_post($item['content']); ?>
                </div>
            </div>
        <?php endforeach; ?>
    </div>
    <?php
    return ob_get_clean();
}

✅ 2. block.js (Gutenberg block editor)

const { registerBlockType } = wp.blocks;
const { useBlockProps, RichText } = wp.blockEditor;
const { Button, PanelBody, TextControl } = wp.components;
const { Fragment } = wp.element;
const { InspectorControls } = wp.blockEditor;

registerBlockType('wcag/accordion', {
    title: 'WCAG Accordion',
    icon: 'menu',
    category: 'layout',
    attributes: {
        items: {
            type: 'array',
            default: [],
        }
    },
    edit({ attributes, setAttributes }) {
        const { items } = attributes;

        const updateItem = (index, field, value) => {
            const newItems = [...items];
            newItems[index][field] = value;
            setAttributes({ items: newItems });
        };

        const addItem = () => {
            const newItems = [...items, { title: 'Accordion Title', content: 'Accordion content' }];
            setAttributes({ items: newItems });
        };

        const removeItem = (index) => {
            const newItems = [...items];
            newItems.splice(index, 1);
            setAttributes({ items: newItems });
        };

        return (
            <Fragment>
                <InspectorControls>
                    <PanelBody title="Accordion Settings">
                        <Button onClick={addItem} variant="primary" isSmall>Add Accordion Item</Button>
                    </PanelBody>
                </InspectorControls>

                <div {...useBlockProps()}>
                    {items.map((item, index) => (
                        <div key={index} className="editor-accordion-item">
                            <TextControl
                                label={`Title ${index + 1}`}
                                value={item.title}
                                onChange={(value) => updateItem(index, 'title', value)}
                            />
                            <RichText
                                tagName="div"
                                label={`Content ${index + 1}`}
                                value={item.content}
                                onChange={(value) => updateItem(index, 'content', value)}
                            />
                            <Button onClick={() => removeItem(index)} isLink isDestructive>Remove</Button>
                            <hr />
                        </div>
                    ))}
                    {items.length === 0 && (
                        <Button onClick={addItem} variant="secondary">Add First Accordion Item</Button>
                    )}
                </div>
            </Fragment>
        );
    },
    save() {
        return null; // Rendered via PHP
    }
});

✅ 3. style.css (Front-end CSS)

.wcag-accordion {
  border: 1px solid #ccc;
  border-radius: 4px;
  max-width: 700px;
}

.accordion-item + .accordion-item {
  border-top: 1px solid #ccc;
}

.accordion-header {
  background-color: #f9f9f9;
  padding: 1em;
  width: 100%;
  text-align: left;
  border: none;
  font-size: 1.1em;
  cursor: pointer;
}

.accordion-header:focus {
  outline: 3px solid #007cba;
  outline-offset: 2px;
}

.accordion-panel {
  padding: 1em;
  display: none;
}

.accordion-panel[aria-hidden="false"] {
  display: block;
}

@media (prefers-reduced-motion: reduce) {
  .accordion-panel {
    transition: none !important;
  }
}

✅ 4. script.js (Front-end JS)

document.addEventListener('DOMContentLoaded', function () {
    const headers = document.querySelectorAll('.accordion-header');

    headers.forEach(header => {
        header.addEventListener('click', () => {
            const expanded = header.getAttribute('aria-expanded') === 'true';
            const panelId = header.getAttribute('aria-controls');
            const panel = document.getElementById(panelId);

            // Collapse all
            headers.forEach(h => {
                h.setAttribute('aria-expanded', 'false');
                const p = document.getElementById(h.getAttribute('aria-controls'));
                p.setAttribute('hidden', '');
                p.setAttribute('aria-hidden', 'true');
            });

            // Expand clicked one if it was collapsed
            if (!expanded) {
                header.setAttribute('aria-expanded', 'true');
                panel.removeAttribute('hidden');
                panel.setAttribute('aria-hidden', 'false');
            }
        });

        // Optional: arrow key navigation
        header.addEventListener('keydown', e => {
            const currentIndex = Array.from(headers).indexOf(header);
            if (e.key === 'ArrowDown') {
                const next = headers[(currentIndex + 1) % headers.length];
                next.focus();
                e.preventDefault();
            } else if (e.key === 'ArrowUp') {
                const prev = headers[(currentIndex - 1 + headers.length) % headers.length];
                prev.focus();
                e.preventDefault();
            }
        });
    });
});

✅ Final Steps

  1. Place the wcag-accordion-block folder into /wp-content/plugins/
  2. Activate the plugin from the WordPress dashboard
  3. Use the “WCAG Accordion” block in the block editor
  4. Add accordion items and content easily from the sidebar
Categories Uncategorized
Last updated on
Index