惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

G
Google Developers Blog
S
Schneier on Security
The Hacker News
The Hacker News
P
Proofpoint News Feed
Spread Privacy
Spread Privacy
L
LINUX DO - 热门话题
L
Lohrmann on Cybersecurity
I
Intezer
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Schneier on Security
Schneier on Security
Security Latest
Security Latest
AWS News Blog
AWS News Blog
B
Blog RSS Feed
Microsoft Security Blog
Microsoft Security Blog
有赞技术团队
有赞技术团队
博客园 - 叶小钗
The Last Watchdog
The Last Watchdog
O
OpenAI News
月光博客
月光博客
Hacker News: Ask HN
Hacker News: Ask HN
阮一峰的网络日志
阮一峰的网络日志
S
Security @ Cisco Blogs
Google Online Security Blog
Google Online Security Blog
云风的 BLOG
云风的 BLOG
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Latest news
Latest news
P
Palo Alto Networks Blog
Last Week in AI
Last Week in AI
M
MIT News - Artificial intelligence
Google DeepMind News
Google DeepMind News
P
Proofpoint News Feed
C
CERT Recently Published Vulnerability Notes
Apple Machine Learning Research
Apple Machine Learning Research
U
Unit 42
PCI Perspectives
PCI Perspectives
博客园 - 聂微东
SecWiki News
SecWiki News
宝玉的分享
宝玉的分享
Forbes - Security
Forbes - Security
H
Heimdal Security Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Hugging Face - Blog
Hugging Face - Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Troy Hunt's Blog
博客园 - 三生石上(FineUI控件)
Application and Cybersecurity Blog
Application and Cybersecurity Blog
罗磊的独立博客
WordPress大学
WordPress大学
D
Darknet – Hacking Tools, Hacker News & Cyber Security

Ben Myers

Tag, You’re It: Blog Questions 2025 Don’t Use aria-label on Static Text Elements Subtitles, Closed Captions, and Open Captions: What’s the Difference? Lost in Translation: Tips for Multilingual Web Accessibility Build a Blogroll with Eleventy I’m a Spotless Giraffe. How I Write Alt Text for Code Snippets on Social Media The Curious Case of “iff” and Overriding Screenreader Pronunciations The Web Needs a Native .visually-hidden Create Shareable Automatic Captions for Live Online Events with Web Captioner A First Look at the Websites and Software Applications Accessibility Act Bill How I Doubled My Lighthouse Performance Score in One Night How to Fix Your Low-Contrast Text Build a Twitch Chatbot for Sharing Your Content Using Algolia Search Ben’s Humane Guide to Technical Blogging On the ‹dl› Takeaways From “Adapting Comics for Blind and Low Vision Readers: A Roundtable Discussion” Takeaways From Axe-Con 2021 I Finally Understand Eleventy’s Data Cascade. RSS Readers: Yet Another Case for Semantic Markup Implement a Skip Link for Navigation-Heavy Sites aria-label, aria-labelledby, and aria-describedby: What’s the Difference? Out With The Old, In With The New Maintaining Focus Outlines for Windows High Contrast Mode Lexical and Dynamic Scope CSS Can Influence Screenreaders New Year, New Terminal: Alias Your Directories the Unix Way New Year, New Terminal: Alias Your Directories the Windows Way What Is ARIA? The Accessibility Tree How (Not) to Build a Button How Domino’s Could Topple the Accessible Web – Part 1: Public Accommodations
Style with Stateful, Semantic Selectors
Ben Myers · 2022-07-05 · via Ben Myers

Introduction

In web development, we frequently need to style elements to visually indicate some state they’re in. We give form fields red outlines to indicate invalid values. We show disabled or inactive elements in gray. We use any number of colors, icons, borders, and more to indicate what kind of state an element is in. Behind the hood, those visual styles are often handled by toggling CSS classes.

And yet: screenreaders, for instance, don’t expose colors or borders or underlines or most of our other visual styles.note 1 They have no idea what our .is-invalid or .selected classes mean. This can pose an accessibility gap, since we have a discrepancy between visually-conveyed information and information conveyed via assistive technologies. If a state is important enough to indicate visually, it’s probably important enough to expose to assistive technologies.

Let’s take current page indicators for nav links, for instance. It’s a common practice to style the current page’s link in a navbar, maybe coloring it differently or giving it a different border. This usually accomplishes two things:note 2

  • It orients the user as to where they are in the site’s architecture (which is especially useful if they’ve come directly to that page from some external source like Google)
  • It indicates that they probably don’t need to click that particular link lest they reload the page.

Often, you might end up with markup like this:

<nav>
	<a href="/about" class="current-page">About</a>
	<a href="/talks">Talks</a>
	<a href="/projects">Projects</a>
	<a href="/contact">Contact Me</a>
</nav>

There’s a problem here: that current page status would be super useful to screenreader users — after all, if a screenreader user reloads the current page, they might be flooded with announcements as the screenreader begins reading the page anew — and yet, screenreaders have no clue that this extra context exists.

We could address this with an ARIA state attribute — specifically, setting aria-current="page" on the link.note 3 When aria-current="page" is provided on a link, the screenreader will announce something like link, About, current page. The exact announcement may differ depending on which screenreader is used. Now, our markup looks like this:

<nav>
	<a href="/about" aria-current="page" class="current-page">About</a>
	<a href="/talks">Talks</a>
	<a href="/projects">Projects</a>
	<a href="/contact">Contact Me</a>
</nav>

We’ve introduced a new problem, though. Now we have to keep track of two things: the class and the ARIA attribute. In theory, everything should be fine, so long as we always remember to keep these two in sync. But what if they diverge, and we end up with a link that has the .current-page class but not aria-current="page"? It happens — sighted developers are much more likely to remember the visual indication and less so the semantic indication. Or, admittedly less likely, we remember to add aria-current="page" but we accidentally omit our .current-page class? We forget things. Bugs happen.

We can reduce the duplication and the risk of bugs, making impossible states impossible, by instead using the ARIA attribute as our selector:

a[aria-current="page"] {
	border-bottom: 7px solid yellow;	
}

Now, to get the desired visual indication, we have to provide the necessary semantics for assistive technology. We can’t get one without the other. I call this styling with stateful, semantic selectors. In my experience, it makes my code much more robust and ensures I don’t accidentally omit necessary accessible semantics.

There are many ways you can style with stateful, semantic selectors, but I’d like to show you a few more examples I love:

Expand/Collapse Triggers

In this pattern, you have a button which, when clicked, will show or hide some content. Frequently, this button will have some visual indication of whether that content is currently being shown or not — often, it’ll have a caret that’s in one orientation when the content is expanded, and it’s rotated when the content is collapsed.

As before, we could toggle that caret state with a CSS class — let’s call it .is-expanded.

<button class="is-expanded">
	Additional Details
</button>
button::before {
	/* Text for demo's sake */
	/* This should be an SVG */
	content: '❯';
	display: inline-block;
	transition: transform 0.2s;
}

button.is-expanded::before {
	transform: rotate(90deg);
}

If you were to try to use the above button with a screenreader, you’d have an issue. When you press the button, the caret will rotate, but your screenreader will remain silent. It has no context that the button has hidden or revealed some content, so it says nothing. As a screenreader user, you’re left without any feedback, and you may start to wonder whether you even clicked the button at all.

Fortunately, there’s an ARIA state attribute for this exact purpose, called aria-expanded! When a screenreader navigates to a button with aria-expanded="false", it’ll announce the button along with something like collapsed, such as button, Additional Details, collapsed. This tells screenreader users that the button controls toggling some content, and that content is currently hidden. When the attribute is toggled to aria-expanded="true", the screenreader announcement will update to include expanded (or something to that effect), and say something like button, Additional Details, expanded.

Our code might update to something like:

<button class="is-expanded" aria-expanded="true">
	Additional Details
</button>
button::before {
	/* Text for demo's sake */
	/* This should be an SVG */
	content: '❯';
	display: inline-block;
	transition: transform 0.2s;
}

button.is-expanded::before {
	transform: rotate(90deg);
}

Upon every click of the button, a script toggles the .is-expanded class and flips aria-expanded between "true" and "false". However… we’re once again doing two things where we could be doing just one thing, and we’re risking impossible states if aria-expanded and the .is-expanded class fall out of sync with each other.

Instead, let’s use aria-expanded as the source of truth for our styles:

<button aria-expanded="false">
	Additional Details
</button>
button::before {
	/* Text for demo's sake */
	/* This should be an SVG */
	content: '❯';
	display: inline-block;
	transition: transform 0.2s;
}

button[aria-expanded="true"]::before {
	transform: rotate(90deg);
}

Sorted Table Columns

Say you’ve got a sortable table, and you’d like to indicate which column the table is sorted by, and in which direction. No sweat — this time, we can use the aria-sort attribute.

The aria-sort attribute should be applied to at most one column header at a time. For most sortable tables (sor-tables?), you’ll want to apply aria-sort="ascending" to the sorted column’s table header when the column is sorted in ascending order, apply aria-sort="descending" to the sorted column’s table header when the column is sorted in descending order, and remove aria-sort from the table header altogether when the sort is cleared. When a screenreader user navigates to a table where a column header has aria-sort="ascending" or aria-sort="descending", their screenreader will tell them the name of the sorted column and its direction.

Assuming you have buttons inside each of the table headers to let the user sort, your markup might look something like this:

<table>
	<thead>
		<tr>
			<th scope="col" aria-sort="ascending">
				<button aria-describedby="sort-description">
					Title
				</button>
			</th>
			<th scope="col">
				<button aria-describedby="sort-description">
					Author
				</button>
			</th>
			<th scope="col">
				<button aria-describedby="sort-description">
					ISBN-13
				</button>
			</th>
		</tr>
	</thead>
	<tbody>

	</tbody>
</table>

<p id="sort-description" hidden>Sort by column</p>

If we wanted to add some arrows inside the sorted column’s header’s button to indicate the current sort direction, the [aria-sort] attribute selector will see us through:

th[aria-sort="ascending"] button::after {
	/* Text for demo's sake */
	/* This should be an SVG */
	content: '↑';
}

th[aria-sort="descending"] button::after {
	/* Text for demo's sake */
	/* This should be an SVG */
	content: '↓';
}

(Simplified for the sake of demonstration. Check out Adrian Roselli’s sortable tables article for a much more thorough and robust approach)

And More!

These examples aren’t the only times stateful, semantic selectors could prove helpful. Here are just a few more I didn’t get into:

In short, building with accessible semantics from the get-go can give you expressive, meaningful style hooks for free. Leaning on those style hooks in your CSS selectors lets you reduce the number of moving parts in your site or application, and it can prevent accessibility bugs from creeping in down the road.

As with any web development recommendation, use your best judgment. If you find yourself contorting an element’s semantics or markup to get the appearance you want, it’s a sign to take a step back and revisit. Maybe classes are your friend, or maybe you need to revisit your design and determine whether it still makes sense.

I’m definitely not the first to write about this approach. Here are a few other articles on the subject I’d recommend reading:


  1. Visual styles alone typically don’t impact screenreaders or some other assistive technologies. However, there are some exceptions where CSS can impact assistive technologies’ ability to interpret the page. Check out my article CSS Can Influence Screenreaders to learn more. | Back to [1]

  2. Check out Navigation: You Are Here by the Nielsen Norman Group for more context on navigation design. | Back to [2]

  3. "page" is one of a fixed set of allowed values that aria-current can take. Please don’t make up your own values for this attribute — assistive technologies won’t understand them. | Back to [3]