Accessible Component Series: Common Patterns - Accordions
Tags: a11y, react, framermotion, accessiblecomponentseries
Published: 2022-01-10 05:57:32

## Intro

### About this series

Web accessibility is often overlooked by developers. As such, this series is intended to serve as how-two for developers to implement commonly used patterns with accessibility in mind. 

We'll be using React, Typescript and Framer Motion throughout this series.

I have another post, available [here](url), highlighting a number of reasons why I think developers should put more emphasis on accessibility.

If you're interested in finding other articles in this series, you can refer to [this post](url) which I'll continue to update as new posts go live.

### What we're building

In this post, I'll be going through the ins-and-outs of building an Accordion. Common usages include FAQs, Product Description Sections, etc..

### Assumptions

This post assumes knowledge of JavaScript, React, and a _tiny_ bit of TypeScript. Even if you're not up to speed on Typescript, you should have no issue following along. We'll also be using [Framer Motion](https://www.framer.com/docs/) to improve the UX of our animations.

### A quick note on Accessibility + ARIA Attributes

It's **incredibly important** to how understand _how_ and _why_ specific ARIA Attributes are being used. ARIA Attributes, when used incorrectly, can potentially make a user's experience even worse.

---

## TL;DR

If you want to dive right in and take a look under the hood, you can check out the final result on [CodeSandbox](https://codesandbox.io/s/yb7j8?file=/src/App.tsx) or toy with the finished version below:

{% codesandbox accessible-components-accordion-yb7j8 hidenavigation=1 codemirror=1 previewwindow=browser view=preview hidenavigation=1 hidedevtools=1 %}

---

## 🔧 Let's get building


**1. The Accordion Container**

First, let's setup our base Accordion component:

```javascript
// src/components/Accordion.tsx

import React from "react";

import { AccordionItem } from "components/Accordion/AccordionItem";

// Component Props
interface Props {
  defaultIndex?: number;
  sections: Array<{
    title: string;
    body: string;
  }>;
}

const Accordion: React.FC<Props> = ({ defaultIndex = -1, sections = [] }) => {
  // Used to track the currently active (open) accordion item
  // Note: If we pass in -1, all items will be closed by default
  const [activeIndex, setActiveIndex] = React.useState(defaultIndex);

  // A handler for setting active accordion item
  const handleSetActiveIndex = (n: number) => {
    // If the user clicks the active accordion item, close it
    if (n === activeIndex) setActiveIndex(-1);
    // Otherwise set the clicked item to active
    else setActiveIndex(n);
  };

  return (
    <ul className="accordion">
      {sections.map((s, idx) => (
        <AccordionItem
          key={s.title}
          item={s}
          idx={idx}
          activeIndex={activeIndex}
          handleClick={handleSetActiveIndex}
        />
      ))}
    </ul>
  );
};

export { Accordion };


```

Nothing special or out of the ordinary here. Simply tracking state via `activeIndex` and iterating over our `sections`, passed in via `props`, and returning our `AccordionItem` component defined in the next step below.


**2. The Accordion Item**

```javascript
// src/components/Accordion/AccordionItem.tsx

import React from "react";
import { AnimatePresence, useReducedMotion, m } from "framer-motion";

import { SVG } from "components/SVG";

// Component Props
interface Props {
  idx: number;
  activeIndex: number;
  item: { title: string; body: string };
  handleClick: (n: number) => void;
}

const AccordionItem: React.FC<Props> = ({
  item,
  idx,
  activeIndex,
  handleClick
}) => {
  // Get browser's reduce motion setting
  const shouldReduceMotion = useReducedMotion();
  // Active State
  const active = idx === activeIndex;
  // Button ID : Must be unique to each accordion.
  const buttonId = `button-${idx}`;
  // Panel ID : Must be unique to each accordion
  const panelId = `panel-${idx}`;

  // Framer Motion Variants
  const variants = {
    active: { height: "auto", marginTop: "1rem" },
    inactive: { height: 0, marginTop: "0rem" }
  };

  // If browser's reduce motion settings are true, respect them otherwise use default animation
  const transition = shouldReduceMotion ? { type: "just" } : undefined;

  return (
    <li className="accordion__item">
      <button
        id={buttonId}
        // Aria Controls - Denotes what element this element controls
        aria-controls={panelId}
        // Aria Expanded - Denotes the expanded state of the element this element controls
        aria-expanded={active}
        // On Click, pass the index back up to the parent component
        onClick={() => handleClick(idx)}
      >
        <span className="t-heading">{item.title}</span>
        <SVG.PlusMinus active={active} />
      </button>
      <AnimatePresence>
        {active && (
          <m.div
            id={panelId}
            // Aria Labelled By - Denotes what element this element is controlled by
            aria-labelledby={buttonId}
            initial={"inactive"}
            animate={"active"}
            exit={"inactive"}
            variants={variants}
            transition={transition}
          >
            <p>{item.body}</p>
          </m.div>
        )}
      </AnimatePresence>
    </li>
  );
};

export { AccordionItem };


```

Here we're getting into some real accessibility-related topics, namely the use of `aria-controls`, `aria-expanded`, and `aria-labelledby`. Links for further information are found in the Accessibility Resources & References section below.

In short, we're using some IDs, unique to this list, to create relationships between `button` elements and `div` elements. This is a bit of a contrived example and if this were to be used in production, it would be wise to ensure IDs are unique to the entire page to avoid conflicts.

We're also using a few helpers from Framer Motion. The `useReducedMotion` hook to helps us decide which animation to use when transitioning between states. The `AnimatePresence` component helps us smoothly mount and un-mount a given accordion panel.


**3. SVG Indicator**

```javascript
// src/components/SVG/PlusMinus.tsx

import React from "react";
import { m, useReducedMotion } from "framer-motion";

const variants = {
  active: { rotate: 90 },
  inactive: { rotate: 0 }
};

interface SVGProps {
  className?: string;
  active: boolean;
}

const PlusMinus: React.FC<SVGProps> = ({ className = "", active = false }) => {
  // Get browser's reduce motion setting
  const shouldReduceMotion = useReducedMotion();

  // If browser's reduce motion settings are true, respect them otherwise use default animation
  const transition = shouldReduceMotion ? { type: "just" } : undefined;
  return (
    <m.svg
      className={className}
      width="12"
      height="12"
      viewBox="0 0 12 12"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <m.line
        x1="6"
        y1="-4.37114e-08"
        x2="6"
        y2="12"
        stroke="currentColor"
        strokeWidth="2"
        animate={active ? "active" : "inactive"}
        variants={variants}
        transition={transition}
      />
      <m.line y1="6" x2="12" y2="6" stroke="currentColor" strokeWidth="2" />
    </m.svg>
  );
};

export { PlusMinus };

```

While this component isn't critical to the function or accessibility of the accordion, it's a slick little indicator that helps us assign a visual cue to the state of our accordion items.


**4. Adding some data**

The last thing to do is add some data. In this example, we're passing in some hard-coded placeholder data to the `Accordion` component via `App.tsx`

```javascript
// src/App.tsx
import React from 'react';
import { Accordion } from "components/Accordion";

const data = [
  {
    title: "Section One",
    body:
      "Lorem ipsum dolor sit amet, consectetur adipiscing elit.Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tincidunt vitae semper quis lectus nulla. Risus nullam eget felis eget nunc lobortis. Cum sociis natoque penatibus et magnis dis parturient montes nascetur."
  },
  {
    title: "Section Two",
    body:
      "Dolor morbi non arcu risus quis varius quam. Leo duis ut diam quam. Leo duis ut diam quam nulla porttitor massa id neque. Vel elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi. Pretium vulputate sapien nec sagittis aliquam malesuada bibendum arcu."
  }
];

const App = () => {
  return <Accordion sections={data} />;
};

export { App };

```

And that's that.

If you're interested in seeing how things are styled in my setup, check out the [CodeSandbox](https://codesandbox.io/s/accessible-components-accordion-yb7j8)

---

## Closing Notes

### Accessibility Resources & References

[MDN Aria Attributes](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes)

### Feedback

I always welcome feedback. If you spot any errors or omissions, please let me know.