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

— 4 minute read

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.

The https://github.com/nickytonline/a11y-twitter repository on GitHubOpenGraph image for https://github.com/nickytonline/a11y-twitter

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,
)

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);
}

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);
}
});

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!