reactjsaccessibilitywai-aria

WAI-ARIA attribute needs a unique id, however this react component might be created multiple times


I'm trying to create an Accordion component with WAI-ARIA attributes, however, some elements are required to reference a unique id. In this case, the Accordion's content (accordion__content) has to have a unique id that is referenced by the button's attribute aria-controls.

Since the component might be used multiple times in different places within the page, using 'any-id' as an id would not work.

type AccordionProps = {
  children: ReactNode;
  label: string;
};

export const Accordion = ({ label, children }: AccordionProps) => {
  const { isContentShowing, setIsContentShowing } = useAccordion();

  return (
    <div className="accordion" aria-label={`${label} Accordion`} data-testid="accordion">
      <button
        className="accordion__button"
        onClick={setIsContentShowing}
        aria-expanded={isContentShowing}
        aria-controls="UNIQUE-ID"
      >
        <span className="accordion__text">{label}</span>
        <AccordionArrowIcon isContentShowing={isContentShowing} />
      </button>
      {isContentShowing && (
        <div className="accordion__content" id="UNIQUE-ID" aria-hidden={!isContentShowing}>
          {children}
        </div>
      )}
    </div>
  );
};

One option would be to generate a random id and just assign that id to the attributes, or we could also take the id as a prop. Is there any recommended way for doing this?


Solution

  • When I've had problems like this in the past, I've generated a uuid for the component. That would make your code like this:

    import React from "react";
    // This is the library I'd use, but feel free to make up the ID however you want
    import { uuid } from 'uuidv4';
    
    type AccordionProps = {
      children: ReactNode;
      label: string;
    };
    
    export const Accordion = ({ label, children }: AccordionProps) => {
      const { isContentShowing, setIsContentShowing } = useAccordion();
      // Note that the ID will be regenerated on every re-render, but will still stay internally consistent
      const id = uuid();
    
      return (
        <div className="accordion" aria-label={`${label} Accordion`} data-testid="accordion">
          <button
            className="accordion__button"
            onClick={setIsContentShowing}
            aria-expanded={isContentShowing}
            aria-controls={id}
          >
            <span className="accordion__text">{label}</span>
            <AccordionArrowIcon isContentShowing={isContentShowing} />
          </button>
          {isContentShowing && (
            <div className="accordion__content" id={id} aria-hidden={!isContentShowing}>
              {children}
            </div>
          )}
        </div>
      );
    

    Also, your aria-hidden isn't doing anything. There's logically no point where it would be true, since the element doesn't exist when the content isn't showing.