a11y-twitter: a browser extension for making Tweets more accessible

Nick Taylor - Jul 2 '22 - - Dev Community

Just over a year ago, I made a post on Twitter, and I realized that I had forgotten to add alternate text (alt text) to the image. I did what I usually do. I quickly copied the Tweet, deleted it, rewrote it, and added the image back to the Tweet, along with the alt text, and Tweeted it out.

So first off, why does this matter? For folks who cannot view images, alternate text can describe what the picture is. Providing alt text is also great for SEO, if that's your jam.

As someone familiar with browser extensions (I used to work on a password manager browser extension), I created a browser extension to avoid making this mistake again. I call it a11y-twitter.

GitHub logo nickytonline / a11y-twitter

Small changes to how you use Twitter to promote Tweeting in an accessible manner. For now, it will only prompt once per Tweet to add alt text to an attachment before you Tweet. Simple but effective. 😎

a11y-twitter 🐦

Chrome extension - Firefox extension - Edge extension

Small changes to how you use Twitter to promote Tweeting in an accessible manner. For now, it will only prompt once per Tweet to add alt text to an attachment before you Tweet.

Simple but effective. 😎

The extension is available for Chrome and Chromium-based browsers that support the Chrome Web Store. It's coming soon to Firefox. It works, you just need to submit it to the add-on store. 😎

The a11y Twitter extension in action prompting a user to add alt text to their images before Tweeting.

Install the extension unpacked

  1. Run yarn to install the required dependencies.
  2. Run yarn build
  3. Load the browser extension unpacked from the dist folder.
  1. Navigate to Twitter and have fun!

Installation for development

The browser extension is cross-browser for Chromium-based browsers (Google Chrome, Microsoft Edge etc.) and Firefox. When in…




The extension doesn't do much, but it does one thing well. It checks if you've added alt text before Tweeting. In its current form, I decided to not bug the user too much, so it only prompts you to add alt text once during a Tweet. That may change in the future, but for now, I thought this made sense as it makes folks aware of alt text, but does not nag them.

When you create a browser extension, you hijack a page to some degree. Content scripts (JavaScript) and additional CSS can alter the look and interactivity of the page you're on. If you've ever used a password manager, that's what's happening. They inject images and JavaScript to the page to allow you to access your credentials.

The 1Password browser extension active on the Twitter login page

In the case of the a11y-twitter extension, finding the correct elements to perform the check for missing alt text proved interesting. Twitter for the web is built with React, uses some form of CSS in JS library, and has no ID attributes to select the elements. CSS classes would make sense in terms of a selector. Still, since the CSS classes are autogenerated from the CSS in JS library, that's impossible.

I'm not positive, but at Twitter they are most likely using Cypress for End to End (E2E) Testing as some elements on the page have data-testid attributes. And that's how I find the Tweet button.

!['tweetButtonInline', 'tweetButton'].includes(
  potentialTweetButton.dataset.testid,
)
Enter fullscreen mode Exit fullscreen mode

data-testid attributes are for testing only, but I highly doubt they'll be removed because the E2E tests have the same problem I have. How to find the Tweet button? Once found, I check if it's disabled. If it's disabled, it means the person hasn't typed anything to Tweet out. If they have, though, that's when I check for alt text.

if (tweetButton && tweetButton.ariaDisabled !== 'true') {
  a11yCheck(event);
}
Enter fullscreen mode Exit fullscreen mode

The a11y-twitter browser extension notifying a Twitter user that al text is missing

And that's pretty much it!

As of the date this blog post was initially published, this is the entire magic sauce to make this all happen.

// TODO: This would need to support other languages than English.
const ADD_DESCRIPTIONS_MESSAGE =
  'You have attachments without descriptions. You can make these attachments more accessible if you add a description. Would you like to do that right now before you Tweet?';
const ADD_DESCRIPTION_LABEL = 'Add description';
const ADD_DESCRIPTIONS_LABEL = 'Add descriptions';
let askedOnce = false;

function a11yCheck(event) {
  // For v1, don't badger folks every time for the current Tweet.
  // v2 can have an option for this.
  if (askedOnce) {
    // Resetting for the next Tweet.
    askedOnce = false;
    return;
  }

  // Check to see if there is at least one missing description for an attachment.
  const attachments = document.querySelector('[data-testid="attachments"]');

  // Need to check for one or more descriptions.
  attachments.querySelectorAll('[role="link"][aria-label="Add description"]');
  const mediaAltTextLinks = attachments
    ? attachments.querySelectorAll(
        `[role="link"][aria-label="${ADD_DESCRIPTION_LABEL}"], [role="link"][aria-label="${ADD_DESCRIPTIONS_LABEL}"]`,
      )
    : [];

  const [missingAltTextLink] = [...mediaAltTextLinks].filter((link) => {
    const linkTextElement = link.querySelector('[data-testid="altTextLabel"]');

    // Need to check for one or more descriptions.
    return (
      linkTextElement.innerText === ADD_DESCRIPTION_LABEL ||
      linkTextElement.innerText === ADD_DESCRIPTIONS_LABEL
    );
  });

  if (!missingAltTextLink) {
    // Resetting for the next Tweet.
    askedOnce = false;
    return;
  }

  const shouldAddDescriptions = confirm(ADD_DESCRIPTIONS_MESSAGE);

  if (shouldAddDescriptions) {
    askedOnce = true;
    event.preventDefault();
    event.stopPropagation();
    missingAltTextLink.click();
  } else {
    askedOnce = false;
  }
}

function findTweetButton(element) {
  let potentialTweetButton = element;

  while (
    !['tweetButtonInline', 'tweetButton'].includes(
      potentialTweetButton.dataset.testid,
    )
  ) {
    if (potentialTweetButton === document.body) {
      potentialTweetButton = null;
      break;
    }

    potentialTweetButton = potentialTweetButton.parentElement;
  }

  return potentialTweetButton;
}

document.body.addEventListener('mousedown', (event) => {
  const { target } = event;
  const tweetButton = findTweetButton(target);

  if (tweetButton && tweetButton.ariaDisabled !== 'true') {
    a11yCheck(event);
  }
});

Enter fullscreen mode Exit fullscreen mode

I may add more features to the extension in the future, but for now, it's serving its purpose for myself and other folks. Also, if you end up using it, consider starring it on GitHub! 😎

If you'd like to learn more about creating a browser extension, here are some handy resources:

P.S.: It'd be neat to create a Safari extension, but the process is a lot more painful, and I haven't had time to dedicate to this. If you're interested, though, pull requests are welcome!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .