Make Yourself Accessible: New Rails Apps

  • June 17, 2022
  • Nicholas Marshall
  • 9 min read

In Part 1 of this series, I talked through some of the ethical and moral rationales for making a website accessible to as many people as possible. It's good for your users, and it's good for your code, so it's a good idea. But I didn't actually lay out the process of adding accessibility checks to an application. Or show any code. So that seems like a good thing to cover in this series on adding accessibility checks to an application, huh?

Accessibility in New Projects

So how do we do it? The rest of this post will talk about specific tooling in Ruby and Rails for building and maintaining automated accessibility checking, but there are some general principles we can apply in other languages:

  • Read the docs. Specifically, the Mozilla docs on Accessibility. They're amazing. Read them.
  • Don't guess. Automated accessibility checks can't expect to cover EVERY potential issue in your codebase, but applying a consistent set of rules will be easier if you have a defined set of rules for your test suite to reference. So find a good accessibility standard, and stick to it. That'll probably be some version of the Web Content Accessibility Guidelines (WCAG 2.1), which are used by axe (which I'll talk about in a second).
  • Keep it simple. Use HTML elements for their intended purpose: paragraphs of text go in <p> tags, lists just group <li> tags, buttons use <button> tags, and so on.
  • Automate it. Browsers do a lot of work to implement accessibility standards, and the rules are complicated because humans are complicated. Automation is critical because it (ideally) means that you're constantly comparing your work to a defined body of rules.
  • Don't isolate it. It's always better to check for accessibility issues as part of testing a specific feature. Otherwise, there may be states present in the UI that aren't accessible, and your spec may not catch them. Also, system and view tests are often time-consuming to run, and running an accessibility check in isolation means yet another page load in your test suite.

These guidelines highlight the ultimate goal: make it simple to add accessibility checks and run them when you run your test suite. In a new Rails project, a good initial setup for automating your tests will make it WAY easier to keep your site accessible. You'll need the following:

  • Test coverage on your views i.e. system tests or view tests. Also, test coverage in general. These examples use RSpec and system testing, but you can also use a different testing library and/or view tests to achieve the same result.
  • A library like Selenium or Cuprite that performs automated browser checks. It needs to use JS, unless your site doesn't use any JS. Or won't need to ever, in any case, for any reason or feature. These examples use Capybara system specs, which can be backed by a number of different drivers. See here.
  • An accessibility plugin for your system tests and a reference document. Deque Labs has a lot of excellent resources for implementing accessibility checks that meet the WCAG 2.1 standard. They also maintain axe, which we'll use to run automated checks. The rules reference list for axe version 4.4 can be found here. If you need more context, I've also found the MDN docs for Accessibility to be really, really good. Again, those are here.

System Testing (The quickest, dirtiest version)

System testing is a big, big topic, and beyond the scope of this post. There's a lot of great writing on the subject out there, and I'd encourage you to check some of it out. Start with these (and maybe this, because it's a nice pattern to use).

If you need to set up system tests from scratch, start by installing these gems:

group :system_tests, :test do
  gem 'capybara'
  gem 'selenium-webdriver'

After configuring system testing, add something like the following spec to one of your page specs. We'll use this as our main example, so just imagine something that works for your site layout whenever you see this spec referenced here.

it "can load the root", :system do
  visit root_path

  page_body = find("body")

  expect(page_body).to have_css(".body-class")

In our system, the :system tag tells RSpec that we want to use a headless Selenium Chrome instance as the browser for system specs. We can configure that in a new file: spec/support/system_test_configuration.rb.

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium_chrome_headless

This can also be inferred from the file location by calling infer_spec_type_from_file_location! during RSpec config, if you prefer that. One reason I prefer tagging it explicitly is the ability to tell RSpec to only run a specific tag:

bundle exec rspec --tag type:system

If you're working on an existing app that needs to be accessible, this'll come in REALLY handy in Part 3 (link forthcoming on publication).

If the call to RSpec above did not return any errors and succeeded (green dot in RSpec), all of the following should be true:

  • RSpec recognized your spec as a system spec.
  • Capybara understood that it needed to open a Chrome instance.
  • Capybara did, in fact, open a headless Chrome instance (driven by Selenium in this case).
  • Visiting the page and clicking the expected link correctly brought the user to the page you wanted them to visit.

If it failed (big red X), congratulations! You either found a bug, or my example led you astray and won't work for your application. You'll need to fix it before proceeding.

Running Accessibility Checks

This section will demonstrate a basic approach to checking accessibility in a new project. After configuring system testing, you need a way to add axe to run the Deque checks. Add these gems to the Gemfile (gems also exist for other system test configurations, see here):

group :accessibility, :test do
  gem "axe-core-capybara"
  gem "axe-core-rspec"

If you're writing spec cases that test accessibility, it's generally wise to avoid code that isolates accessibility. As briefly discussed above, the major concerns are A) increasing the number of times we have to interact with Chromedriver (which is slow), and B) missing accessibility errors because we didn't trigger a state that violates our accessibility standards. For instance, the following pattern is still relatively common in some corners of the codebase I currently work on:

it "is accessible", system: true do
  visit users_path

  # This is a matcher provided by axe for accessibility.
  expect(current_page).to be_axe_clean

it "can visit the user creation page from the users index", :system do
  visit users_path

  click("Create New User")

  expect(current_page).to eq new_user_path

Note the use of be_axe_clean. This is a custom RSpec matcher provided by installing our accessibility gems. When you include it in a system spec, axe loads the relevant standard (WCAG 2.1 in this case), and runs a given page through accessibility checks. When dealing with a legacy application, modifying the behavior of this matcher is REALLY important. We'll cover that in Part 3 (link again forthcoming).

While it's relatively unlikely in this example, there may be states and behavior that are not actually accessible. For instance, when I was working on upgrading accessibility in my application, I fought a long, bitter battle with a JS-backed drop-down list that just completely fails to follow accessibility principles. The dropdown menu won. We don't talk about the dropdown menu.

There's no valid reason to treat accessibility as a separate concern from system testing in general. If you're going to do it, do it as part and parcel of the system specs you're already running:

it "can visit the user creation page from the users index", :system do
  visit users_path
  initial_page = current_page

  click("Create New User")

  expect(initial_page).to be_axe_clean
  expect(current_page).to eq new_user_path
  expect(current_page).to be_axe_clean

If this spec succeeds, we can be confident that:

  1. The page doesn't violate accessibility on page load.
  2. The link for creating a new user is visible to axe (because it should raise a violation otherwise).
  3. Clicking that link should be possible with a screen reader.
  4. The result of clicking that link should ALSO create a state accessible to a screen reader.

And that's it. You just add be_axe_clean to specific specs. If it fails, you'll see an error, with details, and can fix it by using the tools provided above.

Using axe gives you really detailed error messages by default. In the example below, axe needs you to wrap a page element within an HTML5 landmark element.

1) Index Page Load
    Failure/Error: expect(page).to be_axe_clean

      Found 1 accessibility violation:

      1) region: All page content should be contained by landmarks (moderate)

          The following 1 node violate this rule:

              Selector: body > span
              HTML: <span style="font-size: 14px;">All</span>
              Fix any of the following:
              - Some page content is not contained by landmarks

      Invocation:{:exclude=>[]}, {}, callback);

    [Screenshot]:  <a path to a screenshot stored on your system>

As long as you consistently add be_axe_clean to your system specs, and don't neglect them (don't neglect them!), you should be able to keep your code up to date with modern accessibility standards.

Next Steps

That, generally, is it. Assuming that you've gotten system testing set up correctly, and can run them without accessibility checks, you can add an accessibility library, run checks at specific points in your test suite, and use the output to guide your code workflow. Automated testing won't get you 100% of the way to an accessible site, but it can surface, document and monitor known issues and help you resolve them BEFORE they impact your users.

But this guide assumes that you're starting from scratch, with a fresh codebase. That's not the situation most of us encounter in our daily lives. In most of the projects and repos I've worked on, I've primarily dealt with code that I didn't write myself. Working in a live codebase means that it's really unlikely that you'll be able to take a sledgehammer to every possible accessibility issue you encounter.

In that scenario, the goal is mitigation and triage, not perfection. Making your codebase fully accessible is completely doable, but slogging through every single HTML file by hand is a recipe for sprint bloat, missed project deadlines, scope creep and stressed-out developers. In Part 3 (link also forthcoming, surprise!), we'll cover a set of helpful tricks you can apply to organize and upgrade your app's adherence to accessibility standards without completely losing your mind at a poor little select tag that REALLY just wants to be invisible.