Lighthouse - Optimize your website, Part 4: Accessibility

Every part of the Lighthouse report is important if you want a great and well performing website. But to the users using your website, the Accessibility part might be one of the most important ones.

Accessibility optimization is crucial for websites to ensure equal access for all users, including those with disabilities. It enhances usability, complies with legal requirements, improves SEO, and broadens audience reach. By making content accessible, websites foster inclusivity, boost user satisfaction, and build a positive brand reputation.

Wow, that sounds awesome! Yeah, guilty as charged — I asked ChatGTP to create a short and precise text explaining why accessibility optimization is important. But it’s not just all talk — it really explains exactly why it’s important to optimize accessibility on your website.

As in the other posts in this series I’ve created a page that scores very low in — this time — the accessibility part. You can see it here: https://test.frontenddesigner.dk/accessibility_low.html

It’s not looking too good:

Low score in Lighthouse, Accessibility.

This time we’re going to split the issues out in 7 parts, like Lighthouse does:

  • Aria

  • Names and labels

  • Best practices

  • Contrast

  • Internationalization & localization

  • Tables and lists

  • Navigation

Aria

ARIA (Accessible Rich Internet Applications) is a set of attributes defined by the W3C to make web content and applications more accessible to people with disabilities.

In the Aria part I’ve got several issues to fix — 5 in all. Let’s have a look at them:

  • Elements with an ARIA [role] that require children to contain a specific [role] are missing some or all of those required children.

  • [role]s are not contained by their required parent element

  • [aria-*] attributes do not have valid values

  • [aria-hidden='true'] elements contain focusable descendants

  • [role] values are not valid

Let’s fix them!

“Elements with an ARIA [role] that require children to contain a specific [role] are missing some or all of those required children” and “[role]s are not contained by their required parent element”

On my page I’ve created a simple tabs functionality. The first tab gets activated on load — and the user can click the other tabs to read content in them instead. Open tabs are closed, when clicking a new tab.

I’ve got an

  • element with a role set to “tablist”:

<ul class="tabs-navigation-list" role="tablist">
	<li>
		<button id="tab-nav-1" role="tab" aria-controls="tab-content-1" aria-selected="false">t1</button>
	</li>
</ul>

When specifying a role=”tablist” we need to have children with the role of “tab”. In my case I haven’t got this — instead I have an li with a button inside. The button has the “tab” role, but that’s not enough. We need to specify that the li element doesn’t have a role. So a simple adjustment would fix both these issues. Actually it also fixes the issue in “Tables and lists”.

<ul class="tabs-navigation-list" role="tablist">
	<li role="none">
		<button id="tab-nav-1" role="tab" aria-controls="tab-content-1" aria-selected="false">t1</button>
	</li>
</ul>

[aria-*] attributes do not have valid values

This issue shows up when you are pointing to non existent elements in aria-* attributes. In my case I’ve got a tab with an aria-controls pointing to an element that doesn’t exist:

<ul class="tabs-navigation-list" role="tablist">
	<li>
		<button role="tab" aria-controls="tab-content-2" aria-selected="false">t2</button>
	</li>
</ul>
<div class="tabs-content">
	<!-- This should be the element controlled by the tab above -->
	<div aria-labeled-by="tab-nav-2" class="tab-content" aria-hidden="true">
		<h2>Tab 2 content</h2>
		<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. <a href="https://frontenddesigner.dk" target="_blank">Quibusdam</a>, amet tempore cupiditate, eos, modi ut temporibus eum maxime quidem ipsam quae. Nostrum voluptatum quibusdam facilis earum repellendus aliquid possimus architecto.</p>
		<p><a href="https://frontenddesigner.dk" target="_blank" class="link">read more.</a></p>
	</div>
</div>

As you can see, the div containing my tab content does not have an ID, so we can fix the issue by just adding the correct ID:

<ul class="tabs-navigation-list" role="tablist">
	<li>
		<button role="tab" aria-controls="tab-content-2" aria-selected="false">t2</button>
	</li>
</ul>
<div class="tabs-content">
	<div id="tab-content-2" aria-labeled-by="tab-nav-2" class="tab-content" aria-hidden="true">
		<h2>Tab 2 content</h2>
		<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. <a href="https://frontenddesigner.dk" target="_blank">Quibusdam</a>, amet tempore cupiditate, eos, modi ut temporibus eum maxime quidem ipsam quae. Nostrum voluptatum quibusdam facilis earum repellendus aliquid possimus architecto.</p>
		<p><a href="https://frontenddesigner.dk" target="_blank" class="link">read more.</a></p>
	</div>
</div>

[aria-hidden='true'] elements contain focusable descendants

When working with tabs, accordions, dialogs and other Dynamic content hidden as default— and shown when clicking an element, you might have links or other focusable elements inside the hidden elements. This will give us this message in the Accessibility part in the Lighthouse report.

The issue pinpoints that we are actually able to focus the element without the element being shown on screen. In my tab component I change the “aria-hidden” property on the content elements, when the content is hidden or shown. So we can fix this issue with some simple CSS like this:

.tab-content[aria-hidden='true'] a {
	visibility: hidden;
}

In this case we just fix it for links inside our content, but it could be other elements as well — so remember to check for other elements as well — it could be input fields, select elements, buttons, elements with “tabindex” attribute etc.

When hiding the content with visibility: hidden, the content won’t be focusable. And by only hiding it when inside the .tab-content when aria-hidden is set to true, we make sure that the content is focusable when the tab is active.

[role] values are not valid

This is a simple one. As with all markup and coding, simple misspelled words can have a big impact. In this case, I’ve simply misspelled “tab”:

<ul class="tabs-navigation-list" role="tablist">
	<li>
		<button role="tabb" aria-controls="tab-content-2" aria-selected="false">t2</button>
	</li>
</ul>

So just remember to spellcheck your code. You’re probably using an editor that autocompletes the role attribute (VS Code does it for me), but still — sometimes you still might end up making a mistake. Let’s fix the misspelled word:

<ul class="tabs-navigation-list" role="tablist">
	<li>
		<button role="tab" aria-controls="tab-content-2" aria-selected="false">t2</button>
	</li>
</ul>

Names and labels

In this part I’ve got 3 issues to fix:

  • Image elements do not have [alt] attributes

  • Document doesn’t have a <title> element

  • Links do not have a discernible name

“Image elements do not have [alt] attributes” and “Links do not have a discernible name”

These two is actually fixed by a single change to our markup, because we have a link surrounding just an image with no alt attribute specified:

<p>
	<a href="https://frontenddesigner.dk" target="_blank">
		<img src="https://placehold.co/150x150" />
	</a>
</p>

You should always add an alt-attribute to your images — this enables screen readers to explain what is in the image for visually impaired users.

Also consider adding a period at the end of alt (and title) attributes. With no period screen readers would just read on as if the alt attribute was connected with the next text in the markup. There’s more great tips & tricks for alt attributes at Siteimprove.

By adding an alt attribute to our image, we also fix the discernible name for the link. We have no title or aria-label for the link — and no text inside the link. But the alt attribute would be considered as the text for the link:

<p>
	<a href="https://frontenddesigner.dk" target="_blank">
		<img src="https://placehold.co/150x150" alt="Frontenddesigner.dk - a blog about frontend development." />
	</a>
</p>

For the alt attribute to make sense for the image, it should have been the logo for my site, but you’ll have to live with a placeholder image 😉

Document doesn’t have a <title> element

Yeah, you should always put a title on your page. Let’s just to that:

<title>Test page for accessibility testing.</title>

Best practices

In this part we’re down to just 2 issues:

  • [user-scalable='no'] is used in the element or the [maximum-scale] attribute is less than 5.

  • Touch targets do not have sufficient size or spacing.

[user-scalable='no'] is used in the element or the [maximum-scale] attribute is less than 5

In my viewport meta tag I’ve set both user-scalable to no and maximum-scale to 3.0:

<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=3.0">

When these are set it makes it impossible for a user on e.g. an iPhone to zoom in and out to be able to read content. This is bad for UX — just let your users be able to zoom if they need to:

<meta name="viewport" content="width=device-width, initial-scale=1.0">

Touch targets do not have sufficient size or spacing

The tab navigation items on my page is named “T1”, “T2” and “T3” and has a small font-size of 12px. This results in some very small tabs that might be hard to hit on touch screens — and that’s why we get this message in the report.

.tabs-navigation-list button {
	padding: 8px 12px;
}

Contrast

We’ve got 2 contrast issues to fix:

  • Background and foreground colors do not have a sufficient contrast ratio.

  • Links rely on color to be distinguishable.

Background and foreground colors do not have a sufficient contrast ratio

Contrast-ratios between text and background is important to enable users to actually read the content on your page. Of course you have to use colors from your company or your customers design, but consider making lighter and darker versions of them, if there’s not enough contrast between the background colors and text colors.

In my case i have a “lightgrey” link color on a white background. It’s simply not readable. You can actually use the Chrome Developer Tools to see what the contrast-ratio is for a given text on a given background:

Detect contrast ration in Chrome Developer Tools.

To fix the issue I just chose a dark grey (#444), but I could actually play around with the colors by changing the light grey slightly bit by bit to see when the ratio was good enough:

a.link {
	color: #444; /* Changed from 'lightgrey' */
}

Links rely on color to be distinguishable

When having links in blocks of text, you have to make sure that the links are distinguishable. In my case, my links are “blue” with text-decoration set to none. The text surrounding the links are black. The blue and black colors doesn’t offer enough contrast to make the links distinguishable:

a {
	color: blue;
	text-decoration: none;
}

We can actually fix this in two ways. The first way to go would be as simple as to remove the text-decoration: none. This would make the links distinguishable because they wouldn’t rely on the color only:

a {
	color: blue;
}

We could also find a color that, together with black, would offer enough contrast-ratio between the text and the links. In this case, a color of #ca1414 does the trick for us. This way, we can keep the text-decoration: none.

a {
	color: #ca1414;
	text-decoration: none;
}

Internationalization and localization

We need to tell our users about which language our website is written in. In my example I’m missing the lang attribute on the html tag:

<!DOCTYPE html>
<html>
	<head></head>
	<body></body>
</html>

We fix it by supplying the html tag with the correct lang attribute and value:

<!DOCTYPE html>
<html lang="en">
	<head></head>
	<body></body>
</html>

Tables and lists

In this part I’ve just got a single issue to fix:

  • List items (<li>) are not contained within <ul>, <ol> or <menu> parent elements.

As mentioned in the “Aria” part this issue is fixed by putting a role=”none” on our li elements inside the ul:

<ul class="tabs-navigation-list" role="tablist">
	<li role="none">
		<button id="tab-nav-1" role="tab" aria-controls="tab-content-1" aria-selected="false">t1</button>
	</li>
	<li role="none">
		<button id="tab-nav-2" role="tab" aria-controls="tab-content-2" aria-selected="false">t2</button>
	</li>
	<li role="none">
		<button id="tab-nav-3" role="tab" aria-controls="tab-content-3" aria-selected="false">t3</button>
	</li>
</ul>

If our ul element didn’t have the role attribute set, we wouldn’t have had this issue. So we don’t need to set role=”none” on all li elements 😉

Navigation

We’re almost finished — but only almost. We need to fix one last issue:

  • Heading elements are not in a sequentially-descending order

Ensure headings are in a logical order… For example, the heading level following an h1 element should be an h2 element, not an h3 element.

To ensure you are writing effective headings, read through the headings on the page and ask yourself if you get a general sense of the page’s contents based only on the information provided by the headings. If the answer is “no”, consider rewriting your headings.

These two quotes are from dequeuniversity.com — Lighthouse links to this page for explaining why correct usage of headings are important: https://dequeuniversity.com/rules/axe/4.10/heading-order

In the Lighthouse case it’s just about the order of the headings, but as you can read at the Deque University website, you also need to consider if what you are marking as a headline actually makes sense as a headline. So keep that in mind before marking text as a headline. It might need to be highlighted visually, but that doesn’t necessarily mean that it’s supposed to be a headline in the markup.

On my test page I’ve got this markup:

<h1>This is just a test page for testing the Lighthouse Accessibility part.</h1>
<h3>This is a h3 element (we have no h2! 😲)</h3>

Later on, I’ve actually got some h2 elements in the tab contents. But the heading tags needs to be in correct order — h1, h2, h3 etc. I can keep the headings in the tab content as h2’s or I can change them to h3’s, but I need to change the h3 after the h1 to an h2:

<h1>This is just a test page for testing the Lighthouse Accessibility part.</h1>
<h2>This is a h2 element</h2>

Final result

100 / 100 score in Lighthouse, Accessibility.

This looks good, right 😉 It’s from my fixed test page.

We did a lot more to fix these issues than in the SEO and Best Practices part of the series. Here’s what we did:

  • We added role=”none” to li elements inside ul’s with the role of “tabpanel” to make sure role attributes and their children are correct

  • We added missing ID’s to tabs and tab-contents, so our aria-controls and aria-labledby attributes are correct

  • We made sure that elements inside hidden content are not focusable

  • We corrected spelling errors in the role attributes of our tab navigation items

  • We added a title attribute to our document

  • We added an alt attribute to an image — also fixing the discernible name for the link surrounding the image

  • We removed user-scalable=no and maximum-scale=3.0 from our viewport meta tag

  • We added padding to our tab navigation items, making sure they are easy to hit on touch screens

  • We changed the lightgrey link color to a darker grey to ensure enough contrast

  • We changed the link color in text blocks to red — or removed the text-decoration: none; when links are blue — to ensure that links are distinguishable

  • We added a “lang” attribute to the html tag, to ensure readers can tell which language our page is written in

  • We corrected headings to be in correct order (h1, h2, h3 etc.)

That’s it for the Accessibility part. Now we’ve got 100 / 100 in this part as well as in the others. Awesome!

If you’ve got any questions regarding this article — or others — please don’t hesitate to reach out at [email protected]. You’re also welcome to reach out if you have further tips & tricks you see missing in my articles. I’ll update them and credit you.