Enforcing Accessibility Best Practices with Automatically-Generated IDs

One of the best things about design systems is you can create components that have design, development, accessibility, responsive, performance, etc best practices baked right into them. By taking care of the boring stuff, users of the design system don’t have to think about (or at least think as hard about) certain things, freeing them up to focus on more pressing tasks.

You likely know that form fields must have an accompanying label, which is often handled by providing an id attribute to the label, like so:

<label for="first-name">First Name</label>
<input type="text" id="first-name" />

This creates a relationship between the label and the input, resulting in an accessible experience for users using assistive technologies and a better UI experience since now clicking/tapping the label focuses users into the input. Pretty cool!

This is relatively easy to wrap your head around, but it can get tricky fast when you want to provide additional instructions for a form field, such as using the aria-describedby attribute to create a relationship between the field and helper text.

<label for="password">Choose a password</label>
<input type="password" id="password" aria-describedby="instructions" />
<p id="instructions">Your password must be at least 8 characters long</p>

Now you have two id attributes to juggle, and when you play this out across a whole bunch of form fields it can get hard to keep track of everything. As a result, maybe developers start dropping the ball and omit the aria-describedby attribute or even the main field id. That’s a problem!

How can design systems help with this? One way is to enforce this things using mechanisms like PropTypes, but it would be even better if the system could take care of this itself.

One little trick that I picked up from browsing the Lightning Design System React library was automatically generating ids using a little library called shortid, which generates a unique gobbledygook string like qPbdivDEiH. We can still give developers to define their own id and aria-describedby attributes, but if they fail to enter them those ids are automatically generated. What this looks like in React-land is something like this:

import shortid from "shortid";

class TextField extends React.Component {

  componentWillMount() {
    this.generateId=shortid.generate();
    this.generateAriaId=shortid.generate();
  }

  render() {
    const { id, label, ariaDescribedBy, instructions, ..other} = this.props;
    return (
      <div className="c-text-field">
        <label htmlFor={id || this.generateId}>{label}</label>
        <input type="text" id={id || this.generateId} aria-describedby={ariaDescribedBy || this.generateAriaId} />
        <p id={ariaDescribedBy || this.generateAriaId}>{ instructions }</p>
      </div>
    ) 
  } 
}

What’s going on is we’re generating two new ids using the shortid library, but also defining an id prop. Looking at the code id={id || this.generateId} means “use the user-provided id prop, but if the user doesn’t provide one use the generated shortid.

Using the component now looks something like this:

<TextField label="Last Name" id="last-name" ariaDescribedBy="last-name-instructions" instructions="Please provide your family name" />

Will result in:

<label for="last-name">Last Name</label>
<input type="text" id="last-name" aria-describedby="last-name-instructions" />
<p id="last-name-instructions">Please provide your family name</p>

But if the user doesn’t declare any ids like so:

<TextField label="Last Name">

The result looks like this:

<label for="qPbdivDEiH">Last Name</label>
<input type="text" id="qPbdivDEiH" aria-describedby="0LvdKMfqA" />
<p id="0LvdKMfqA">Please provide your family name</p>

Because the user didn’t enter specific values, the component uses the generated shortids. While it looks a little uglier, it still provides the same accessible experience.

What does all this mean?

What this means is that it’s impossible for users of the design system to omit an important attribute and accidentally create an inaccessible experience. This technique works well for any component that needs to create an association between two or more ids, like:

  • Accordions
  • Tabs
  • Radio group fields
  • Checkbox group fields
  • A bunch of other stuff

Of course, this doesn’t solve all our accessibility issues, but it at least takes care of something important that can feel a bit tedious.

Are you using techniques like this in your design system? Any gotchas? Would love to hear from you.