How I built an accessible version of Wordle

Recreating Wordle without the myriad of accessibility issues that plague the original version

ยท

8 min read

How I built an accessible version of Wordle

Last month I just had this desire to build my own Wordle clone, since all the cool kids were doing it. But I wanted to put my own spin on it, so I decided to make it a simpler, prettier and more accessible variant. The result? See for yourself: wordel.app. If you like it, you can even replace Wordle with it in your daily routune because the word list and answers are synced between them!

In this post, I will be going over some of the interesting decisions I made when building wordel. Are you excited? Because I am.

But first...

The problem with Wordle

Wordle is delightful at its best, but it is famously inaccessible in pretty much every way. Even the built-in "high contrast mode" has extremely poor color contrast ๐Ÿ˜ฌ

1.92 score on the letter background in high contrast mode. Yikes!

The worst part is that the inaccessibility of Wordle extends beyond the player. I'm talking, of course, about the emoji grid that gets dumped on social media. It is painful for screen reader users to go through those tweets full of emojis.

Anna E Cook wrote a wonderful piece going over this in more detail, which I highly recommend reading if you like sharing your results on social media.

And those are just two of the most obvious problems with Wordle. There's a lot more, but I'd rather talk about what I did than complain about The New York Times not using a fraction of their millions of dollars to make this game more inclusive.

Enter wordel

Let's get the name out of the way first. It's not a typo. I just like writing wordel. All lower case. Pronounced the same as Wordle (even by screen readers). Don't ask me why.

Now for the fun parts.

Accessible right from the beginning

When building something from scratch, you have the opportunity to consider accessibility at every step of the way. And that's exactly what I did!

Note: I spent a fair bit of time testing all these things through screen readers, keyboard navigation, emulations, etc. While it all worked fine in my testing, I may have still missed something. If you find any issues, I'd love to hear from you so I can fix them

Colors

Choosing accessible colors is a relatively easy task. I tried to meet a contrast score of 7:1 (AAA) in most places. The WebAim Contrast Checker is a great website for comparing colors and for learning about color contrast in general.

Additionally, it is important to make sure the app is accessible to folks with color vision deficiencies. So I added small checkmarks next to the correct (green) letters.

screenshot of letter grid showing checkmarks on correctly guessed letters

Lastly, I should also mention Windows forced high contrast mode. The linked article does a great job explaining how to support this mode, but it basically just involves making sure borders are present around elements, and using forced-color-adjust: none in some places (e.g. the letter backgrounds).

Semantic HTML

Building from scratch, without any component libraries, gives you complete control over your markup. That means I was able to use semantically correct HTML elements in every component, instead of creating a div soup. Here are a few examples:

  • The page layout is split into <header> and <main>.
  • The word guess uses a real <input> element that can be typed into using the device keyboard.
  • For the guess distribution, I use the <dl>, <dt> and <dd> element, which creates semantic name-value pairs.
  • I make use of the <output> element in a few places (e.g. toast notifications), which automatically announces updates to screen readers.
  • I use <button> for a button and <a> for a hyperlink (revolutionary, I know), even if they look exactly the same.
  • I use the native <dialog> element which has automatic focus management, speaking of which...

Focus management

It is important to make sure focus behaves properly in your app for it to be accessible to keyboard users (which often includes sighted users too). This usually just means having a sensible tab order with visible focus styles, but sometimes involves manually moving focus around.

Using semantic elements, like I did, can get you pretty damn far. So it's really impressive (and not in a good way) that the original Wordle has literally no tabbable elements!

Anyway, there are two places I had to manage focus:

  • I mentioned I'm using a real <input> element for the guess, but it is visually hidden (and replaced with the letter grid which shows the typed value from the input in big bold letters). Normally you shouldn't visually hide focusable elements, but I think it's fine here because this is the only focusable element on the main page โ€” well, there are multiple of these <input> elements, but only the current guess is active โ€” and the page is not scrollable. So the only thing left is to make sure this input gets focused, which I'm doing manually by calling .focus().
  • I also mentioned that the native <dialog> has automatic focus management, i.e. it automatically moves and traps focus inside the dialog. But my dialog has two screens, so I need to manually move focus when the screen changes.

Emoji grid

What to do with the infamous emojis-as-results? Inaccessible to screen reader users, but sighted users love them. Well, we can get the best of both worlds.

I'm showing the emoji grid but disabling text selection on it, and not providing any easy way to copy the emoji text. Instead, I have a button which copies the "alt text" that describes the results of the game. The expectation is that the player will screenshot this emoji grid and share it on social media (or IMs) with the alt text that's already in their clipboard.

the result dialog showing emojis mimicking the final state of the game. At the top, there's a note saying "For accessibility reasons, instead of sharing the emojis as plain text, you should take a screenshot of this grid and share it with the provided alt text." At the bottom there is a button which copies alt text describing the results in plain English

Tech stack

Let's talk tech! I wanted this to be lightweight, fast and have a good development experience, which is why I chose the following technologies:

  • Preact: I keep hearing how awesome Preact is, so I went with it. I'm super comfortable with React, but it would have been overkill for this little project, so Preact was a great fit. Why not something like Svelte? Well, I didn't want to learn a new framework. ๐Ÿคท

  • TypeScript: Many influential people will tell you to stay away from TypeScript for small, fun projects. But the thing is, I really enjoy using TypeScript (usually). It leads to fewer bugs, it offers better developer experience, but above all, it makes me feel... safe. ๐Ÿฅบ

  • PostCSS: These days you can get very far with just vanilla CSS. There is no need for styled-components in a small project like this. I didn't use any component libraries because I didn't want new dependencies, and I was actively avoiding Tailwind because I enjoy writing real CSS syntax. So the only thing left is PostCSS, for nice things like autoprefixer. And with postcss-preset-env, I even get to use nesting (without Sass!) ๐Ÿ˜ฎ

  • Vite: This one's a no-brainer. Vite has an official template for Preact + TypeScript, and it provides a super fast development server with esbuild. Plus, I was able to easily add vite-plugin-pwa to generate a service worker for free! ๐Ÿฅณ

  • eslint + prettier: Both of these are are worth mentioning, even though they are part of literally every project I work on. It took me longer than it should have to get eslint working because TS+preact+prettier requires a very specific combination of rules in the eslintconfig. But it's worth it, even if only for two things: enforcing Rules of Hooks and auto formatting on save. ๐Ÿ‘

Performance

Since the app and the components are so small, it's hard to have any performance issues.

Worth mentioning though, is the bundle size which is around 60KB, including the huge list of words and all assets. This is all thanks to using Preact and avoiding any other dependencies.

Just for fun, here's what the network tab looks like on a fresh load:

Chrome network tab showing only 5 resources, totaling 60KB and all loading under 100ms

And here it is on subsequent loads, with all the service worker magic:

Chrome network tab showing all resources loading from the service worker cache, within 10ms

Polyfill

I didn't go over any code snippets anywhere, but I should show you how I'm conditionally loading the polyfill for the <dialog> element only in older browsers.

(async () => {
  if (typeof HTMLDialogElement !== 'function') {
    const css = document.createElement('link');
    css.href = 'https://unpkg.com/dialog-polyfill@0.5.6/dist/dialog-polyfill.css';
    document.head.appendChild(css).rel = 'stylesheet';

    await import('https://unpkg.com/dialog-polyfill@0.5.6/dist/dialog-polyfill.js');
    dialogPolyfill.registerDialog(document.querySelector('dialog'));
  }
})();

That's a dynamic import from a CDN. Browser support for dynamic imports is surprisingly good, so I can just throw that in a <script> tag in my index.html. Pretty neat.

Wrapping up

And there you have it. An accessible, and dare I say beautiful, version of the word game we all know and love. Because there is no reason a word game shouldn't be inclusive.

You can play wordel at wordel.app and find the source code at github.com/mayank99/wordel.

Until next time ๐Ÿ‘‹