Before diving into this post, you should know what accessibility is. A good place to start could be this "What is accessibility" article by MDN.
Usually the biggest and most common selling point that I see for writing accessible web applications is, in short, making your apps usable to users that rely on assistive technologies. That short statement alone can be split into multiple other very elaborate reasons, like the ones you'll see listed in the article I linked above. They are all true, but they all revolve around the user-facing benefits of accessibility, and this will be the case as well with most other documentation you stumble upon online.
This can be a problem because my professional experience shows that most companies and enterprises will bail out of investing in engineering efforts for accessibility claiming that the user-base percentage that will actually need it is too small to justify the expense. They will probably not use those harsh words though, or probably don't even address the issue in the first place. And you know what, although it might sound cruel, it can make total business sense in many scenarios, i.e. a software that is only used internally, and the company is 100% certain that none of its employees are handicapped in some way, therefore, will not need it.
However, despite this, I've always tried to write my code as accessible and semantic as possible within the budget my teams are allowed, as I feel it is my ethical duty as a professional of the web to not only deliver the highest quality code to my employers, but also the highest quality apps to its users. I like to think of it as an unofficial unspoken oath that I've made, similar to those doctors do on movies and T.V. shows, if you know what I mean.
In doing so, I've noticed certain unexpected developer facing benefits that hardly ever discussed and might shift the mentality of development teams and drive them to do the same. Let's go through some examples to illustrate my point.
In many teams and OSS projects that I've worked on I see this style of UI tests, or similar:
const submitBtn = document.querySelector('.btn-primary') Simulate.click(submitBtn) expect(submitBtn.classList).toInclude('btn-pimrary__disabled') expect(submitBtn.classList).toInclude('btn-pimrary__loading') // ...
In short, using CSS class names or selectors to find elements and write the assertions of the tests. To some of you reading it might be obvious that this is an anti-pattern and not the best of practices, but I assure you, it is not so obvious to everyone. Just this week alone I changed a class name that broke a multitude of tests unnecessarily that I later wasted the rest of my day fixing, that incident alone was enough motivation for me to write this post.
The HTML standard is rich enough that you can do all of this and more, more semantically and resiliently, without relying on any style related attributes or rules at all? Heck, if you are using a CSS-in-JS solution or similar that scrambles your class names this might not even be possible to you in the first place, and in that case people fall back to rely in implementation details of their UI components to achieve the same thing, which is also a bad practice.
Let's look at my proposed alternative:
const submitBtn = getByText('Submit') Simulate.click(submitBtn) expect(submitBtn.hasAttribute('disabled')).toBe(true) expect(submitBtn.hasAttribute('aria-busy')).toBe(true)
With WAI-ARIA and regular HTML attributes, you can represent almost any possible (if not all) state that your elements can be in, including active/inactive tabs, expanded/collapsed panels, loading/ready elements, disabled/enabled inputs or buttons, valid/invalid forms, visibility... you name it. You will not only be making your tests much more easier to write, but also much more robust, readable and semantic, not to mention that you would be making your app more accessible in the process, it's a win-win scenario in my book. I'm usually hesitant to talk about "readability" because I've noticed that it is hugely sensitive and subjective, but I think I'm confident with using it in this case. Click here for a full list of state related ARIA attributes.
const submitBtn = getByText('Submit') Simulate.click(submitBtn) expect(submitBtn).toBeDisabled() expect(submitBtn).toHaveAttribute('aria-busy', 'true')
And if your assertions fail, you'll get errors like:
Received element is not disabled: <button>Submit</button>
Expected the element to have attribute: aria-busy="true" Received: aria-busy="false"
Which I think we can all agree is better than just
Expected false to be true.
Let's say you have to implement a table with checkboxes that looked like this:
The checkboxes are kind of "floating" around in this table with no immediate indication as to what might be their purpose. By looking at the entire picture though, you can probably infer that each checkbox value is associated with the combination of the column and the row names. Just for the sake of an example, let's say we replace the column names with days of the week, let's go with Monday, Wednesday and Friday, and the rows with activities or chores, if we see a checkbox checked in the "Wednesday" and "Mow lawn" intersection, we could say that that is an activity that either has to be done on that day, or was done on that day.
But what if you had to only rely on the contents of the markup to figure that out, without seeing any layout? Regardless of whether this is a good design and representation for that type of data or not, let's use it for this exercise. Minimalistically speaking this could be the HTML behind it:
<table> <thead> <tr> <th></th> <th>Col1</th> <th>Col2</th> <th>Col3</th> </tr> </thead> <tbody> <tr> <td>Row1</td> <td><input type="checkbox" /></td> <td><input type="checkbox" /></td> <td><input type="checkbox" /></td> </tr> <!-- Row2, Row3... --> </tbody> </table>
Would you be able to figure out the purposes of this table and the checkboxes from that markup as quickly and easily? What if you are a developer coming into this screen for the first time to fix a bug, and maybe you are looking at this markup directly in the code or in a failed test, would it be immediately obvious to you how this UI component works? In a real scenario this could be a table rendering several dozen columns and rows, and have a lot of added markup for styling, making it even more difficult to inspect. As a side note, although we already stated that this is not a user-facing oriented post, imagine being a blind user relying on a screen reader to decipher this UI... it wouldn't go smoothly, to say the least.
We can improve this greatly by just adding:
<table> <thead> <tr> <th></th> <th>Col1</th> <th>Col2</th> <th>Col3</th> </tr> </thead> <tbody> <tr> <td>Row1</td> <td><input type="checkbox" aria-label="Col1 + Row1" /></td> <td><input type="checkbox" aria-label="Col2 + Row1" /></td> <td><input type="checkbox" aria-label="Col3 + Row1" /></td> </tr> <!-- Row2, Row3... --> </tbody> </table>
Feel free to format or phrase the label anyway you like, but now, in a huge wall of HTML it is perfectly clear what the purpose of the checkbox is, without the need to see the visual layout of the elements. This small detail can save a developer a lot of time and headaches when working with this component in the future, debugging an issue or adding new functionality.
What about writing tests?
const checkbox = getByLabelText('Col2 + Row1') as HTMLInputElement expect(checkbox.checked).toBe(true)
Without this label, you would have to rely on very flaky CSS selectors that would leak implementation details of your component into your tests, and end up breaking with the smallest change to the markup, when refactoring or changing only styling. Not going to bother providing a snippet for how that would look like since there could be a million ways to do it, and they would all be bad.
You can go one step further to improve these inputs by also providing a tooltip of some form to the input element. A quick-fix there would be reaching out for the
title attribute as well and mirroring the value of the label in it. Keep in mind though that
title attributes have certain limitations that are clearly described in this article by Heydon Pickering: Tooltips & Toggletips. Or check out Reach UI's Tooltip component, even if you're not using React, you can learn a lot from its implementation if you'd like to roll out your own. You will notice it's not trivial.
Although it may not seem like much, this approach evolves into robust and readable tests that serve as not only as bug barriers but more importantly as easy to digest coded documentation on how the components work in a way that other types of test don't, which greatly increases the productivity of developers on the team. The ones that are going to notice the most are unfamiliar developers coming into sections of the codebase and quickly being able to get up to speed.
This is extremely valuable in companies with dozens of developers that contribute across the whole platform. And that's without mentioning the implementation code itself, that will be a clearer reflection of the intent of the developer who wrote it.