|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: 'Common Accessibility Pitfalls for Single Page Apps, and How to Avoid Them' |
| 4 | +author: gwtrev |
| 5 | +tags: |
| 6 | +- react |
| 7 | +- accessibility |
| 8 | +- frontend |
| 9 | +team: web |
| 10 | +--- |
| 11 | + |
| 12 | +[//]: # TODO This snippet needs links which reference other blog posts |
| 13 | + |
| 14 | +> *Preface: Although unrelated to this article, folks at Scribd have already |
| 15 | +> shared some wonderful information about accessibility, varying from the |
| 16 | +> anatomy of an accessible ebook reader to small code snippets that can vastly |
| 17 | +> improve the accessibility of your website with pretty minimal effort. Check |
| 18 | +> those out if you’re interested in learning more!* |
| 19 | +
|
| 20 | +If you’re a front-end developer in today’s world, you’ve probably used the |
| 21 | +various JavaScript tooling available (React, Vue, Webpack, etc) to build a |
| 22 | +single page application (SPA). SPAs are easy, fast, and most importantly, |
| 23 | +provide a concise way to manage your front-end assets. What’s more, you can |
| 24 | +handle your page changing using client-side tools like `react-router-dom`! What |
| 25 | +more could you ask for? |
| 26 | + |
| 27 | +Alongside the myriad benefits that come along with SPAs comes one of the most |
| 28 | +crucial parts of your site taking the biggest hit: accessibility. |
| 29 | + |
| 30 | +Features that came out-of-the-box with more traditional tech stacks and server |
| 31 | +based routing now require special handling, since all or most of the routing |
| 32 | +happens client side. Two big issues that are easy to overlook when working with |
| 33 | +client-side routing of SPAs include: |
| 34 | + |
| 35 | +1. Managing the focus of each page’s main header (h1 tag); and |
| 36 | +1. Providing the correct page title and description between pages (and in general, meta information) |
| 37 | + |
| 38 | +Let’s take a look at some easy ways to handle these issues with React. |
| 39 | + |
| 40 | + |
| 41 | +## Managing the page header |
| 42 | + |
| 43 | +Let’s paint a picture. When a user with assistive software (in particular, a |
| 44 | +screen reader) has arrived at your website, they interact with it like any |
| 45 | +other site at first: navigating links and listening to what is available on the |
| 46 | +page — including links to other pages. |
| 47 | + |
| 48 | +Then the user selects a link to a different “page” on the site and… uh-oh. |
| 49 | +nothing happens! To them, anyway. |
| 50 | + |
| 51 | +To sighted users, clearly the content has changed. But to a user without the |
| 52 | +ability to see the content, and without any handling of how that content has |
| 53 | +changed, they’re left confused. |
| 54 | + |
| 55 | +What’s worse: suppose the anchor link that navigated to this new page isn’t on |
| 56 | +the page anymore. Now focus has moved back to the page <body>. The frustration |
| 57 | +grows! |
| 58 | + |
| 59 | +This isn’t an uncommon issue with SPAs. There are fewer page refreshes when you |
| 60 | +handle routing purely in the client, and thus fewer changes in context unless |
| 61 | +you account for them explicitly. |
| 62 | + |
| 63 | +It’s a jarring user experience, to say the least. |
| 64 | + |
| 65 | +Here’s a simple React component that handles these frustrations for us. Notes |
| 66 | +afterward. |
| 67 | + |
| 68 | +What is this little component doing? The important part is what happens with |
| 69 | +its state. Here’s a quick overview: |
| 70 | + |
| 71 | +1. When the component mounts, it does a check against a prop called |
| 72 | +`lastLocation`. This is provided by a higher order component we’re using called |
| 73 | +`withLastLocation`, from the npm package |
| 74 | +[react-router-last-location](https://www.npmjs.com/package/react-router-last-location). |
| 75 | +The package borrows state from your React Router wrapper to tell us the last |
| 76 | +location that was visited by the user. |
| 77 | + |
| 78 | +1. If `lastLocation` is null, nothing happens and the component renders without |
| 79 | +side effects. This is the behavior expected from first page load. |
| 80 | + |
| 81 | +1. If `lastLocation` provides an object, we can infer the current page the user |
| 82 | +is viewing is not the first page they’ve seen since opening the website. We |
| 83 | +then set the `tabIndex` of the header to -1 and focus the header to set the |
| 84 | +context for the user. Once the header loses focus, we reset the `tabIndex` back |
| 85 | +to `null`. |
| 86 | + |
| 87 | +And that’s it! You can drop this component anywhere — just remember: |
| 88 | + |
| 89 | +1. [WCAG](https://www.w3.org/WAI/standards-guidelines/wcag/) (Web Content |
| 90 | +Accessibility Guidelines) recommends you have only one h1 on any given page. |
| 91 | +If you include this component twice, whichever component renders last will |
| 92 | +command focus, so best to avoid that altogether by only rendering it once. |
| 93 | +1. You’ll need `react-router-dom` (4.x.x and greater) for the above |
| 94 | +implementation to work, as the package `react-router-last-location` requires it |
| 95 | +as a [peerDependency](https://yarnpkg.com/lang/en/docs/dependency-types/). |
| 96 | + |
| 97 | +## Managing the site title and meta description |
| 98 | + |
| 99 | +Similar to the page header, your site title and meta description should update |
| 100 | +across pages if the context of the page has sufficiently changed. Assistive |
| 101 | +technologies will need to know that the page is different at the highest level |
| 102 | +so screen readers can announce it. The perk of this step will be more than |
| 103 | +accessibility, though; it’s also great for SEO (when combined with server side |
| 104 | +rendering). |
| 105 | + |
| 106 | +For this example, we’ll do another simple React component using |
| 107 | +[react-helmet](https://www.npmjs.com/package/react-helmet) to |
| 108 | +modify the content of the document head. Again, notes are after. |
| 109 | + |
| 110 | +The component is pretty straightforward, and can be used similar to the |
| 111 | +`AccessibleHeader` component above. Drop it anywhere on the page, with `title` and `description` |
| 112 | +props defined, and `react-helmet` replaces the existing definitions |
| 113 | +in your document head. Similar to your h1, you should only have one of each |
| 114 | +title and meta description on each page. |
| 115 | + |
| 116 | +Let’s also look at that `titleTemplate` prop that React Helmet accepts — this is |
| 117 | +exactly what it sounds like. It’s a base template that takes a placeholder |
| 118 | +option, `%s`, that is added in addition to whatever your new title text happens |
| 119 | +to be. If your title was `Shopping Cart`, for example, the end result would be `My |
| 120 | +Site | Shopping Cart`. Easy! |
| 121 | + |
| 122 | +As an aside, be aware of titles and descriptions you’ve added to your base |
| 123 | +`index.html` (assuming you’re using `html-webpack-plugin`). Sometimes React Helmet |
| 124 | +will duplicate one or both of these tags, which isn’t desirable. |
| 125 | + |
| 126 | +--- |
| 127 | + |
| 128 | +I hope this was helpful! As discussed, it’s vital to ensure your SPA has proper |
| 129 | +handling of context between pages to provide the most valuable information |
| 130 | +possible to users visiting with assistive technologies. You’re also improving |
| 131 | +the SEO and general user experience of your app by including these changes, so |
| 132 | +it’s a win-win all around! |
| 133 | + |
| 134 | +**If you want to work on changing how the world reads, come join us! Learn more |
| 135 | +at [scribd.com/careers](https://www.scribd.com/careers)!*** |
0 commit comments