Paul Cowan

Nomadic cattle rustler and inventor of the electric lasso

Creating an Accessible React Website

I’ve recently been working on an online application form in the form of a multistep wizard that had strict accessibility requirements. I’ve never worked on a project with such strict requirements before. I’ve also heard rumblings that it was not possible to make a SPA accessible. It turns out that there is not that much work involved in making your site accessible and I am going to ensure that any work I do from now on has an accessible first approach. I’m now going to outline in no particular order what I have learned over the past few months.

Use a router

The initial appeal of the SPA was that it negated the need to go to the server to render new content. An SPA that does not transition to different views with different urls makes it not a great experience for any decent sized SPA. The problem is that a newly server rendered page works great with a screen reader but when you change routes in an SPA, the screen reader does not know that there is new content.

One solution is to have a container component that checks for changes in the react-router location property and focus on an element at the top of the viewport each time the location changes.

Below is an example of such a container component:

ScrollToTop.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class ScrollToTop extends Component {
  props: Props;

  el: HTMLElement;

  focusOnElement = () => {
    setTimeout(() => {
      this.el.focus();
    }, 100);
  };

  componentDidMount() {
    this.focusOnElement();
  }

  componentDidUpdate(prevProps: Props) {
    if (
      this.props.location.hash.length ||
      this.props.location === prevProps.location
    ) {
      return;
    }

    window.scrollTo(0, 0);

    this.focusOnElement();
  }

  render() {
    const { children } = this.props;

    return (
      <div ref={el => (this.el = el)} tabIndex="-1" className="main-content">
        {children}
      </div>
    );
  }
}

export default withRouter(ScrollToTop);

The above container checks location prop changes in componentDidUpdate and will focus on the ref element tagged in line 33 if a location change is detected. A tabIndex of -1 is set on the ref which does not add the element to the natural tab order but does allow you to progrmmatically set focus on an element that would not normally receive focus.

Keyboard Navigation

As we now have a router and a container component that detects route changes, we should ensure that we can tab up and down the page on all elements that require focus.

There really is not a lot to this if you use sensible html element choices for buttons and links. You should not make a span tag or a div a button or a link for example. I’m sure I’ve been guilty of making a span clickable in the past but that is now firmly on the banned list.

One gotcha is links, if you have a link with no href then you need to give it a tbIndex of 0.

Below is a cut down example of a Link component.

Link.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const Link: React.StatelessComponent<LinkProps> = ({ href, onClick, children, ...rest }) => {
  const linkProps = {};

  if(!href) {
    linkPops.tabIndex = 0;
  } else {
    linkProps.href = href;
  }

  return (
    <a {...linkProps} {...rest}>
      {children}
    </a>
  )
}

Use a component library

Using a component library that is outside of the context of the business rules is a great choice. I have controlled components for even simple html elements like input, label etc. This allows me ensure that I am consistent about things like aria attributes. Somebody remarked in a code review that React was broken if I needed to create components for things like an input but they missed the point. Below is a simple input component that I use instead of the default react element:

Input.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export const Input: React.StatelessComponent<InputProps> = ({
  invalid,
  className,
  required,
  ...rest
}) =>
  <input
    autoComplete="off"
    required={required}
    aria-required={required}
    className={cs(className, {
      [styles.invalid]: invalid
    })}
    {...rest}
  />;

Input.displayName = 'Input';

Input.defaultProps = {
  type: 'text'
};

It is also great to think about components in isolation or out of the context that they will be used. Working in the context of a component library without redux, mobx etc. leads to better generic components.

Label all Form Controls

All form or input controls should have labels that describe the purpose of the form control.

A label for a form control helps everyone better understand its purpose. In some cases, the purpose may be clear enough from the context when the content is rendered visually. The label can be hidden visually, though it still needs to be provided within the code to support other forms of presentation and interaction, such as for screen reader and speech input users.

With the above in mind, I have the following higher order componet that wraps anything needing a label and tags it appropriately.

FormControl.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
export function FormControl<T>(
  Comp: Component<T>
): React.Component<T> {
  return class FormControlWrapper extends React.Component<T> {
    id: string;
    constructor(props) {
      super(props);

      this.id = this.props.id || this.props.name || prefixId();
    }

    render() {
      const {
        invalid,
        name,
        label,
        errorMessage,
        className,
        required,
        ...rest
      } = this.props as any;

      const errorId = `${this.id}-error`;

      return (
        <div>
          <Label
            id={`${this.id}-label`}
            htmlFor={this.id}
            required={required}
          >
            {label}
          </Label>
          <div>
            <Comp
              id={this.id}
              name={name}
              invalid={invalid}
              aria-invalid={invalid}
              required={required}
              aria-describedby={errorId}
              {...rest}
            />
          </div>
          <div
            id={errorId}
            aria-hidden={!invalid}
            role="alert"
          >
            {invalid &&
              errorMessage &&
              <Error
                errorMessage={errorMessage}
              />}
          </div>
        </div>
      );
    }
  };
}
  • Line 15 assigns an id for the passed in wrapped component by first of all checking the props or defaulting to a generated id from the prefixId function.
  • Line 29 assigns the htmlFor attribute of the label to the id member variable.
  • line 36 sets the id of the passed in wrapped component to ensure they are properly assigned.
  • The correct aria tags are also set for things like aria-invalid on line 39 in the wrapped component. The beauty of higher order components is that the wrapped component is loosely coupled from the hoc allowing them to be developed orthogonally with each having little knowledge about each other.
  • Line 41 uses the aria-describedby attribute to connect any validation messages that might occur. A correctly tagged error message element is displayed on lines 45-55 if the invalid prop is true.

With this higher order component in place, I can now add the correct labelling to any component such as the Input component previously described:

FormControls.js
1
export const FormInput = FormControl(Input);

Validation

The higher order component above takes care of displaying an error below each invalid field but a screen reader will not automatically pick this up unless the user tabs onto the form control.

To counteract this we supply a validation summary. Below is an exmple of such a validaion summary from gov.uk that I based our validation summary on:

At first glance this is complete overkill for 2 fields but in the context of a screen reader, this is a great practice. In the event of an error, focus is placed on the h2 element in the ValidationSummary component and a link is created for each validation error. The link’s href is a bookmark link to the invalid element. When the user tabs off the h2, they get an explanation of the error and a chance to jump to the form element and fix the problem.

We used redux-form and the following component was used to create a validation summary for all redux-form errors:

ValidationSummary.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
export default class ValidationSummary extends Component {
  props: ValidationSummaryProps;

  id: string;

  constructor(props: ValidationSummaryProps) {
    super(props);

    this.id = props.id || prefixId('alert');
  }

  static displayName = 'ValidationSummary';

  render() {
    const {
      syncErrors,
      submitFailed,
      heading,
      description,
      dataSelector
    } = this.props;

    const show = submitFailed && !isEmptyObject(syncErrors);

    const errors = show ? flattenValidationErrors(syncErrors) : [];

    const alertId = `alert-${this.id}`;

    return (
      <div
        className={styles.dialog}
        aria-live="polite"
        aria-labelledby={alertId}
        aria-hidden={!show}
        data-selector={dataSelector}
      >
        <Alert
          id={alertId}
          heading={heading}
          description={description}
          type="error"
          hidden={!show}
        >
          <div className="form-group">
            {errors.map((error: { error: string, id: string }, i: number) =>
              <ul className="current-errors" key={error.id}>
                <li role="tooltip" className={cs('error', 'required')}>
                  <ValidationLink
                    error={error}
                    dataSelector={`${dataSelector}-${i}`}
                  />
                </li>
              </ul>
            )}
          </div>
        </Alert>
      </div>
    );
  }
}

Aria tagging

When going for an accessibility review of the website, a knowledgable person said that the first rule of aria is to not use aria. I might have been guilty of going overboard with my aria tagging and would welcome any feedback explaining if I have done this.

One definite place to use aria is to inform the screen reader that invisible content is now visible.

Below is a help container that expands and contracts when a link is clicked:

HelpLink.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
export const HelpLink = ({
  collapsibleId,
  linkText,
  helpText,
  open,
  onClick,
  children
}) =>
  <div className={styles.container}>
    <Link
      button
      onClick={onClick}
      aria-expanded={open}
      aria-controls={collapsibleId}
      tabIndex={0}
    >
      <span
        className={cs(
          styles['link__title'],
          open && styles['link__title__open']
        )}
      >
        <span>
          {linkText}
        </span>
      </span>
    </Link>
    <div
      id={collapsibleId}
      aria-hidden={!open}
      aria-live="polite"
      className={cs(styles['closed'], open && styles['open'])}
      role="region"
      tabIndex={-1}
    >
      {helpText}
      {open && children}
    </div>
  </div>;

A combination of aria-expanded, aria-controls and aria-live are used to correctly instruct the screen reader that new content is toggled between visible and invisible states.

Epilogue

I think we can make all our work much more accessible if we put just a little bit of effort in. At the very least we should ensure we are keyboard accessible and let the screen reader know that new content is avilable.

If you disagree or agree with any of the above please leave a comment below.

Comments