aria – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Wed, 14 Dec 2022 14:08:21 +0000 en-US hourly 1 https://wordpress.org/?v=6.1.1 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 aria – CSS-Tricks https://css-tricks.com 32 32 45537868 Unchain My Inaccessibly-Labelled Heart https://css-tricks.com/unchain-my-inaccessibly-labelled-heart/ https://css-tricks.com/unchain-my-inaccessibly-labelled-heart/#comments Wed, 14 Dec 2022 14:08:20 +0000 https://css-tricks.com/?p=375952 Suzy Naschansky from the HTMHell Advent Calendar:

<h2 id="article1-heading"All About Dragons</h2<pI like dragons. Blah blah blah blah blah.</p<p<a id="article1-read-more" aria-labelledby="article1-read-more article1-heading"Read more</a</p

See that aria-labelledby attribute? It chains two IDs from the …


Unchain My Inaccessibly-Labelled Heart originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Suzy Naschansky from the HTMHell Advent Calendar:

<h2 id="article1-heading">All About Dragons</h2>  
<p>I like dragons. Blah blah blah blah blah.</p>  
<p>
  <a id="article1-read-more" aria-labelledby="article1-read-more article1-heading">Read more</a>  
</p>  

See that aria-labelledby attribute? It chains two IDs from the markup, one for the heading (#article1-heading) and one for the link (#article1-read-more). What happens there is a screenreader will replace the existing semantic label between the link tags and use the content from both elements and announce them together as a single string of text:

Read more All About Dragons

I’m always sheepish when realizing there’s something I think I should know but don’t. This is definitely one of those cases and I’m thankful as all heck that Suzy shared it.

I was actually in a situation just recently where I could’ve should’ve done this. I always try to avoid a bunch of “Read more” links on the same page but coming up with different flavors of the same thing is tough when you’re working with something like a loop of 15 posts (even though there are resources to help). And if we need to keep labels short for aesthetic reasons — design requirements and whatnot — it’s even more challenging. The aria-labelledby attribute gives me exactly what I want: consistent visual labels and more contextual announcements for assistive tech.

And this is only a thing when the text you want to use for the accessible label already exists on the page. Otherwise, you’d want to go with aria-label and with the caveat that it’s purely for interactive elements that are unable to label things accessibly with semantic HTML.

If you are working in a CMS like WordPress (which I am), you might need to do a little extra work. Like when I drop a Button block on the page, these are the options I have to work with:

Some nice options in there, but nothing to do with accessible labelling. If you’re wondering what’s buried in that Advanced panel:

Close, but no cigar.

Instead, you’ll need to edit the button in HTML mode:

But before doing that, you gotta add an ID to the heading you want to use. The Heading block has the same Advanced panel setting for adding an anchor, which’ll inject an ID on the element:

Then you can go edit the Button block in HTML mode and add the accessible-labels ID as well as an ID for the button itself. This is an example of the edited markup:

<div class="wp-block-buttons">
  <!-- wp:button -->
  <div class="wp-block-button">
    <a id="read-more-button" aria-labelledby="read-more-button heading-accessible-labels" class="wp-block-button__link wp-element-button" href="https://webaim.org/projects/million/">
      Read Report
    </a>
  </div>
  <!-- /wp:button -->
</div>

Great! But WordPress just ain’t cool with that:

You can try to resolve the issue:

Le sigh. The Button block has to be converted to a Custom HTML block. Kinda defeats the whole visual editing thing that WordPress is so good at. I did a super quick search for a plugin that might add ARIA labelling options to certain blocks, but came up short. Seems like a ripe opportunity to make one or submit PRs for the blocks that could use those options.


Unchain My Inaccessibly-Labelled Heart originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/unchain-my-inaccessibly-labelled-heart/feed/ 2 375952
Newer Things to Know About Good Ol’ HTML Lists https://css-tricks.com/newer-things-to-know-about-good-ol-html-lists/ https://css-tricks.com/newer-things-to-know-about-good-ol-html-lists/#comments Mon, 28 Nov 2022 14:05:11 +0000 https://css-tricks.com/?p=375273 HTML lists are boring. They don’t do much, so we don’t really think about them despite how widely used they are. And we’re still able to do the same things we’ve always done to customize them, like removing markers, reversing …


Newer Things to Know About Good Ol’ HTML Lists originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
HTML lists are boring. They don’t do much, so we don’t really think about them despite how widely used they are. And we’re still able to do the same things we’ve always done to customize them, like removing markers, reversing order, and making custom counters.

There are, however, a few “newer” things — including dangers — to know when using lists. The dangers are mostly minor, but way more common than you might think. We’ll get to those, plus some new stuff we can do with lists, and even new ways to approach old solutions.

To clarify, these are the HTML elements we’re talking about:

  • Ordered lists <ol>
  • Unordered lists <ul>
  • Description lists <dl>
  • Interactive lists <menu>

Ordered lists, unordered lists, and interactive lists contain list items (<li>) which are displayed according to what kind of list we’re dealing with. An ordered list (<ol>) displays numbers next to list items. Unordered lists (<ul>) and menu elements (<menu>) displays bullet points next to list items. We call these “list markers” and they can even be styled using the ::marker pseudo-element. Description lists use description terms (<dt>) and description details (<dd>) instead of <li> and don’t have list markers. They‘re supposed to be used to display metadata and glossaries, but I can’t say I’ve ever seen them in the wild.

Let’s start off with the easy stuff — how to correctly (at least in my opinion) reset list styles. After that, we’ll take a look at a couple of accessibility issues before shining a light on the elusive <menu> element, which you may be surprised to learn… is actually a type of list, too!

Resetting list styles

Browsers automatically apply their own User Agent styles to help with the visual structure of lists right out of the box. That can be great! But if we want to start with a blank slate free of styling opinions, then we have to reset those styles first.

For example, we can remove the markers next to list items pretty easily. Nothing new here:

/* Zap all list markers! */
ol, ul, menu {
  list-style: none;
}

But modern CSS has new ways to help us target specific list instances. Let’s say we want to clear markers from all lists, except if those lists appear in long-form content, like an article. If we combine the powers of newer CSS pseudo-class functions :where() and :not(), we can isolate those instances and allow the markers in those cases:

/* Where there are lists that are not articles where there are lists... */
:where(ol, ul, menu):not(article :where(ol, ul, menu)) {
  list-style: none;
}

Why use :where() instead of :is()? The specificity of :where() is always zero, whereas :is() takes the specificity of the most specific element in its list of selectors. So, using :where() is a less forceful way of overriding things and can be easily overridden itself.

UA styles also apply padding to space a list item’s content from its marker. Again, that’s a pretty nice affordance right out of the box in some cases, but if we’re already removing the list markers like we did above, then we may as well wipe out that padding too. This is another case for :where():

:where(ol, ul, menu) {
  padding-left: 0; /* or padding-inline-start */
}

OK, that’s going to prevent marker-less list items from appearing to float in space. But we sort of tossed out the baby with the bathwater and removed the padding in all instances, including the ones we previously isolated in an <article>. So, now those lists with markers sorta hang off the edge of the content box.

Notice that UA styles apply an extra 40px to the <menu> element.

So what we want to do is prevent the list markers from “hanging” outside the container. We can fix that with the list-style-position property:

Or not… maybe it comes down to stylistic preference?

Newer accessibility concerns with lists

Unfortunately, there are a couple of accessibility concerns when it comes to lists — even in these more modern times. One concern is a result of applying list-style: none; as we did when resetting UA styles.

In a nutshell, Safari does not read ordered and unordered lists styled with list-style: none as actual lists, like when navigating content with a screen reader. In other words, removing the markers also removes the list’s semantic meaning. The fix for this fix it to apply an ARIA list role on the list and a listitem role to the list items so screen readers will pick them up:

<ol style="list-style: none;" role="list">
  <li role="listItem">...</li>
  <li role="listItem">...</li>
  <li role="listItem">...</li>
</ol>

<ul style="list-style: none;" role="list">
  <li role="listItem">...</li>
  <li role="listItem">...</li>
  <li role="listItem">...</li>
</ul>

Oddly, Safari considers this to be a feature rather than a bug. Basically, users would report that screen readers were announcing too many lists (because developers tend to overuse them), so now, only those with role="list" are announced by screen readers, which actually isn’t that odd after all. Scott O’Hara has a detailed rundown of how it all went down.

A second accessibility concern isn’t one of our own making (hooray!). So, you know how you’re supposed to add an aria-label to <section> elements without headings? Well, it sometimes makes sense to do the same with a list that doesn’t contain a heading element that helps describe the list.

<!-- This list is somewhat described by the heading -->
<section>
  <h2>Grocery list</h2>
  <ol role="list">
     <!-- ... -->
  </ol>
</section>

<!-- This list is described by the aria-label -->
<ol role="list" aria-label="Grocery list">
  <!-- ... -->
</ol>

You absolutely don’t have to use either method. Using a heading or an ARIA label is just added context, not a requirement — be sure to test your websites with screen readers and do what offers the best user experience for the situation.

In somewhat related news, Eric Bailey wrote up an excellent piece on why and how he considers aria-label to be a code smell.

Wait, <menu> is a list, too?

OK, so, you’re likely wondering about all of the <menu> elements that I’ve been slipping into the code examples. It’s actually super simple; menus are unordered lists except that they’re meant for interactive items. They’re even exposed to the accessibility tree as unordered lists.

In the early days of the semantic web, I mistakenly believed that menus were like <nav>s before believing that they were for context menus (or “toolbars” as the spec says) because that’s what early versions of the HTML spec said. (MDN has an interesting write-up on all of the deprecated stuff related to <menu> if you’re at all interested.)

Today, however, this is the semantic way to use menus:

<menu aria-label="Share article">
  <li><button>Email</button></li>
  <li><button>Twitter</button></li>
  <li><button>Facebook</button></li>
</menu>

Personally, I think there are some good use-cases for <menu>. That last example shows a list of social sharing buttons wrapped up in a labeled <menu> element, the notable aspect being that the “Share article” label contributes a significant amount of context that helps describe what the buttons do.

Are menus absolutely necessary? No. Are they HTML landmarks? Definitely not. But they’re there if you enjoy fewer <div>s and you feel like the component could use an aria-label for additional context.

Anything else?

Yes, there’s also the aforementioned <dl> (description list) element, however, MDN doesn’t seem to consider them lists in the same way — it’s a list of groups containing terms — and I can’t say that I’ve really seen them in use. According to MDN, they’re supposed to be used for metadata, glossaries, and other types of key-value pairs. I would just avoid them on the grounds that all screen readers announce them differently.

But let’s not end things on a negative note. Here’s a list of super cool things you can do with lists:


Newer Things to Know About Good Ol’ HTML Lists originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/newer-things-to-know-about-good-ol-html-lists/feed/ 18 375273
A Deep Dive on Skipping to Content https://css-tricks.com/a-deep-dive-on-skipping-to-content/ https://css-tricks.com/a-deep-dive-on-skipping-to-content/#comments Tue, 03 Aug 2021 14:35:48 +0000 https://css-tricks.com/?p=345154 While most people browsing the web on a computer use a mouse, many rely on their keyboard instead. Theoretically, using a web page with the keyboard should not be a problem — press the TAB key to move the keyboard …


A Deep Dive on Skipping to Content originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
While most people browsing the web on a computer use a mouse, many rely on their keyboard instead. Theoretically, using a web page with the keyboard should not be a problem — press the TAB key to move the keyboard focus from one focusable element to the next then press ENTER to activate, easy! However, many (if not most) websites tend to have a menu of links at the top of the page, and it can sometimes take a lot of presses to get to the content that you want. Take the homepage of 20min for example, one of the biggest news sites here in Switzerland. You want to read the top story? That’ll be the best part of 40 key presses — not the best best use of anyone’s time.

So, if you want keyboard users to actually use your website rather than getting bored and going somewhere else, you need to do a bit of work behind the scenes to make skipping straight to your main content quicker and easier. You can find all sorts of techniques for this scattered across the web (including here at CSS-Tricks) but most are missing a trick or two and many recommend using outdated or deprecated code. So, in this article, I’m going to take a deep dive into skipping to content and cover everything in a 2021-friendly fashion.

Two types of keyboard users

Although there are numerous differences in the exact type of keyboard or equivalent switch device that people use to navigate, from a coding point of view, we only need to consider two groups:

  • People who use the keyboard in conjunction with a screen reader — like NVDA or JAWS on a PC, or VoiceOver on a Mac — that reads the content of the screen out loud. These devices are often used by people with more severe visual impairments.
  • All other keyboard users.

Our skip-to-content techniques need to cater to both these groups while not getting in the way of all the mouse users. We will use two complementary techniques to get the best result: landmarks and skip links.

Look at a basic example

I created an example website I’m calling Style Magic to illustrate the techniques we’re covering. We’ll start off with it in a state that works fine for a mouse user but is a bit of a pain for those using a keyboard. You can find the base site and the versions for each of the techniques in this collection over at CodePen and, because testing keyboard navigation is a little tricky on CodePen, you can also find standalone versions here.

Try using the TAB key to navigate this example. (It’s easier on the standalone page; TAB to move from one link to the next, and SHIFT+TAB to go backwards.) You will find that it’s not too bad, but only because there aren’t many menu items.

If you have the time and are on Windows then as I’d also encourage you to download a free copy of the NVDA screen reader and try all the examples with that too, referring to WebAIM’s overview for usage. Most of you on a Mac already have the VoiceOver screen reader available and WebAIM has a great intro to using it as well.

Adding landmarks

One of the things that screen reading software can do is display a list of landmarks that they find on a web page. Landmarks represent significant areas of a page, and the user can pull up that list and then jump straight to one of those landmarks.

If you are using NVDA with a full keyboard, you hit INS+F7 to bring up the “Elements List” then ALT+d to show the landmarks. (You can find a similar list on VoiceOver by using the Web Item Rotor.) If you do that on example site, though, you will only be presented with an unhelpful empty list.

An open dialog with a UI for viewing different types of content on the page in a screen reader, including links, headings, form fields, buttons, and landmarks. Landmarks is selected and there are no results in the window.
A disappointingly empty list of landmarks in NVDA

Let’s fix that first.

Adding landmarks is incredibly easy and, if you are using HTML5, you might already have them on your website without realizing it, as they are directly linked to the HTML5 semantic elements (<header>, <main>, <footer>, and so on).

Here’s a before and after of the HTML used to generate the header section of the site:

<div class="bg-dark">
  <div class="content-width flex-container">
    <div class="branding"><a href="#">Style Magic</a></div>
    <div class="menu-right with-branding">
      <a href="#">Home</a>
      <!-- etc. -->
      </div>
  </div>
</div>

Becomes

<div class="bg-dark">
 <header class="content-width flex-container">    
    <section class="branding"><a href="#">Style Magic</a></section>
    <nav aria-label="Main menu" class="menu-right with-branding">
      <a href="#">Home</a>
      <!-- etc. -->
    </nav>
  </header>
</div>

The classes used remain the same, so we don’t need to make any changes at all to the CSS.

Here’s a full list of the changes we need to make in our example site:

  • The <div> denoting the header at the top of the page is now a <header> element.
  • The <div> containing the branding is now a <section> element.
  • The two <div>s containing menus have been replaced with <nav> elements.
  • The two new <nav> elements have been given an aria-label attribute which describes them: “Main menu” for the menu at the top of the page, and “Utility menu” for the menu at the bottom of the page.
  • The <div> containing the main content of the page is now a <main> element.
  • The <div> denoting the footer at the bottom of the page is now a <footer> element.

You can see the full updated HTML on CodePen.

Let’s try that landmark list trick in NVDA again (INS+F7 then ALT+d — here’s the link to the standalone page so you can test yourself):

Open screen reader dialog window showing the landmarks on the current page, including "banner," "main," and "content info."
Hurrah for landmarks!

Great! We now have the banner landmark (mapped to the <header> element), Main menu; navigation (mapped to the top <nav> element, and displaying our aria-label), main (mapped to <main>) and content info (mapped to footer). From this dialog I can use TAB and the cursor keys to select the main landmark and skip straight to the content of the page, or even better, I can just press the D key when browsing the page to jump from one landmark role directly to the next. Users of the JAWS screen reader have it even easier — they can simply press Q when browsing to jump straight to the main landmark.

As an added bonus, using semantic elements also help search engines understand and index your content better. That’s a nice little side benefit of making a site much more accessible.

I expect you’re sitting back thinking “job done” at this point. Well, I’m afraid there’s always a “but” to consider. Google did some research way back in 2011 on the use of CTRL+f to search within a web page and found that a startling 90% of people either didn’t know it existed, or have never used it. Users with a screen reader behave in much the same way when it comes to landmarks — a large portion of them simply do not use this feature even though it’s very useful. So, we’re going to add a skip link to our site to help out both groups as well as all those keyboard users who don’t use a screen reader.

The basic requirements for what makes a good skip link are:

  • It should be perceivable to all keyboard users (including screen reader users) when it is needed.
  • It should provide enough information to the keyboard user to explain what it does.
  • It should work on as wide a range of current browsers as possible.
  • It should not interfere with the browsing of a mouse user.

Step 1: Improving the keyboard focus appearance

First up, we’re going to improve the visibility of the keyboard focus across the site. You can think of the keyboard focus as the equivalent to the position of the cursor when you are editing text in a word processor. When you use the TAB key to navigate the keyboard focus moves from link to link (or form control).

The latest web browsers do a reasonable job of showing the position of the keyboard focus but can still benefit from a helping hand. There are lots of creative ways to style the focus ring, though our goal is making it stand out more than anything.

We can use the :focus pseudo-class for our styling (and it’s a good idea to apply the same styles to :hover as well, which we’ve already done on the example site — CodePen, live site). That’s the very least we can do, though it’s common to go further and invert the link colors on :focus throughout the page.

Here’s some CSS for our :focus state (a copy of what we already have for :hover):

a:focus { /* generic rule for entire page */
  border-bottom-color: #1295e6;
}
.menu-right a:focus,
.branding a:focus {
  /* inverted colors for links in the header and footer */
  background-color: white;
  color: #1295e6;
}

Step 2: Adding the HTML and CSS

The last change is to add the skip link itself to the HTML and CSS. It consists of two parts, the trigger (the link) and the target (the landmark). Here’s the HTML that I recommend for the trigger, placed right at the start of the page just inside the <header> element:

<header class="content-width flex-container">
  <a href="#skip-link-target" class="text-assistive display-at-top-on-focus">Skip to main content.</a>
  <!-- etc. -->
</header>

And here’s the HTML for the target, placed directly before the start of the <main> content:

<a href="#skip-link-target" class="text-assistive display-at-top-on-focus" id="skip-link-target">Start of main content.</a>

<main class="content-width">
  <!-- etc. -->
</main>

Here’s how the HTML works:

  • The skip link trigger links to the skip link target using a standard page fragment (href="#skip-link-target") which references the id attribute of the target (id="skip-link-target"). Following the link moves the keyboard focus from the trigger to the target.
  • We link to an anchor (<a>) element rather than adding the id attribute directly to the <main> element for two reasons. First, it avoids any issues with the keyboard focus not moving correctly (which can be a problem in some browsers); secondly, it means we can provide clear feedback to the user to show that the skip link worked.
  • The text of the two links is descriptive so as to clearly explain to the user what is happening.

We now have a functioning skip link, but there’s one problem: it’s visible to everyone. We’ll use CSS to hide it from view by default, which keeps it out of the way of mouse users, then have it appear only when it receives the keyboard focus. There are lots of ways to do this and most of them are okay, but there’s a couple of wrong ways that you should avoid:

  • Do: use clip-path to make the link invisible, or use transform: translate or position: absolute to position it off screen instead.
  • Don’t: use display: none, visibility: hidden, the hidden attribute, or set the width or height of the skip link to zero. All of these will make your skip link unusable for one or both classes of keyboard users.
  • Don’t: use clip as it is deprecated.

Here’s the code that I recommend to hide both links. Using clip-path along with its prefixed -webkit- version hits the spot for 96.84% of users at time of writing, which (in my opinion) is fine for most websites and use cases. Should your use case require it, there are a number of other techniques available that are detailed on WebAIM.

.text-assistive {
  -webkit-clip-path: polygon(0 0, 0 0, 0 0, 0 0);
  clip-path: polygon(0 0, 0 0, 0 0, 0 0);
  box-sizing: border-box;
  position: absolute;
  margin: 0;
  padding: 0;
}

And to show the links when they have focus, I recommend using a version of this CSS, with colors and sizing to match your branding:

.text-assistive.display-at-top-on-focus {
  top: 0;
  left: 0;
  width: 100%;  
}
.text-assistive.display-at-top-on-focus:focus {
  -webkit-clip-path: none;
  clip-path: none;
  z-index: 999;
  height: 80px;
  line-height: 80px;
  background: white;
  font-size: 1.2rem;
  text-decoration: none;
  color: #1295e6;
  text-align: center;
}
#skip-link-target:focus {
  background: #084367;
  color: white;
}

This provides for a very visible display of the trigger and the target right at the top of the page where a user would expect to see the keyboard focus directly after loading the page. There is also a color change when you follow the link to provide clear feedback that something has happened. You can fiddle with the code yourself on CodePen (shown below) and test it with NVDA on the standalone page.

Taking this further

Skip links aren’t just for Christmas your main menu, they are useful whenever your web page has a long list of links. In fact, this CodePen demonstrates a good approach to skip links within the content of your pages (standalone page here) using transform: translateY() in CSS to hide and show the triggers and targets. And if you are in the “lucky” position of needing to support older browsers, then you here’s a technique for that on this CodePen (standalone page here).

Let’s check it out!

To finish off, here are a couple of short videos demonstrating how the skip links work for our two classes of keyboard user.

Here’s how the finished skip link works when using the NVDA screen reader:

Here it is again when browsing using the keyboard without a screen reader:


We just looked at what I consider to be a modern approach for accessible skip links in 2021. We took some of the ideas from past examples while updating them to account for better CSS practices, an improved UI for keyboard users, and an improved experience for those using a screen reader, thanks to an updated approach to the HTML.


A Deep Dive on Skipping to Content originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/a-deep-dive-on-skipping-to-content/feed/ 14 345154
Making Disabled Buttons More Inclusive https://css-tricks.com/making-disabled-buttons-more-inclusive/ https://css-tricks.com/making-disabled-buttons-more-inclusive/#comments Wed, 12 May 2021 14:31:07 +0000 https://css-tricks.com/?p=339960 Let’s talk about disabled buttons. Specifically, let’s get into why we use them and how we can do better than the traditional disabled attribute in HTML (e.g. <button disabled> ) to mark a button as disabled.

There are lots of …


Making Disabled Buttons More Inclusive originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Let’s talk about disabled buttons. Specifically, let’s get into why we use them and how we can do better than the traditional disabled attribute in HTML (e.g. <button disabled> ) to mark a button as disabled.

There are lots of use cases where a disabled button makes a lot of sense, and we’ll get to those reasons in just a moment. But throughout this article, we’ll be looking at a form that allows us to add a number of tickets to a shopping cart.

This is a good baseline example because there’s a clear situation for disabling the “Add to cart” button: when there are no tickets to add to the cart.

But first, why disabled buttons?

Preventing people from doing an invalid or unavailable action is the most common reason we might reach for a disabled button. In the demo below, we can only add tickets if the number of tickets being added to the cart is greater than zero. Give it a try:

Allow me to skip the code explanation in this demo and focus our attention on what’s important: the “Add to cart” button.

<button type="submit" disabled="disabled">
  Add to cart
</button>

This button is disabled by the disabled attribute. (Note that this is a boolean attribute, which means that it can be written as disabled or disabled="disabled".)

Everything seems fine… so what’s wrong with it?

Well, to be honest, I could end the article right here asking you to not use disabled buttons because they suck, and instead use better patterns. But let’s be realistic: sometimes disabling a button is the solution that makes the most sense.

With that being said, for this demo purpose, we’ll pretend that disabling the “Add to cart” button is the best solution (spoiler alert: it’s not). We can still use it to learn how it works and improve its usability along the way.

Types of interactions

I’d like to clarify what I mean by disabled buttons being usable. You may think, If the button is disabled, it shouldn’t be usable, so… what’s the catch? Bear with me.

On the web, there are multiple ways to interact with a page. Using a mouse is one of the most common, but there are others, like sighted people who use the keyboard to navigate because of a motor impairment.

Try to navigate the demo above using only the Tab key to go forward and Tab + Shift to go backward. You’ll notice how the disabled button is skipped. The focus goes directly from the ticket input to the “dummy terms” link.

Using the Tab key, it changes the focus from the input to the link, skipping the “Add to cart” button.

Let’s pause for a second and recap the reason that lead us to disable the button in the first place versus what we had actually accomplished.

It’s common to associate “interacting” with “clicking” but they are two different concepts. Yes, click is a type of interaction, but it’s only one among others, like hover and focus.

In other words…

All clicks are interactions, but not all interactions are clicks.

Our goal is to prevent the click, but by using disabled, we are preventing not only the click, but also the focus, which means we might be doing as much harm as good. Sure, this behavior might seem harmless, but it causes confusion. People with a cognitive disability may struggle to understand why they are unable to focus the button.

In the following demo, we tweaked the layout a little. If you use a mouse, and hover over the submit button, a tooltip is shown explaining why the button is disabled. That’s great!

But if you use only the keyboard, there’s no way of seeing that tooltip because the button cannot be focused with disabled. The same thing happens in touch devices too. Ouch!

Using the mouse, the tooltip on the “Add to cart” button is visible on hover. But the tooltip is missing when using the Tab key.

Allow me to once again skip past the code explanation. I highly recommend reading “Inclusive Tooltips” by Haydon Pickering and “Tooltips in the time of WCAG 2.1” by Sarah Higley to fully understand the tooltip pattern.

ARIA to the rescue

The disabled attribute in a <button> is doing more than necessary. This is one of the few cases I know of where a native HTML attribute can do more harm than good. Using an ARIA attribute can do a better job, allowing us to instruct screen readers how to interpret the button, but do so consistently to create an inclusive experience for more people and use cases.

The disabled attribute correlates to aria-disabled="true". Give the following demo a try, again, using only the keyboard. Notice how the button, although marked disabled, is still accessible by focus and triggers the tooltip!

Using the Tab key, the “Add to cart” button is focused and it shows the tooltip.

Cool, huh? Such tiny tweak for a big improvement!

But we’re not done quite yet. The caveat here is that we still need to prevent the click programmatically, using JavaScript.

elForm.addEventListener('submit', function (event) {
  event.preventDefault(); /* prevent native form submit */

  const isDisabled = elButtonSubmit.getAttribute('aria-disabled') === 'true';

  if (isDisabled || isSubmitting) {
    // return early to prevent the ticket from being added
    return;
  }

  isSubmitting = true;
  // ... code to add to cart...
  isSubmitting = false;
})

You might be familiar with this pattern as a way to prevent double clicks from submitting a form twice. If you were using the disabled attribute for that reason, I’d prefer not to do it because that causes the temporary loss of the keyboard focus while the form is submitting.

The difference between disabled and aria-disabled

You might ask: if aria-disabled doesn’t prevent the click by default, what’s the point of using it? To answer that, we need to understand the difference between both attributes:

Feature / Attributedisabledaria-disabled="true"
Prevent click
Prevent hover
Prevent focus
Default CSS styles
Semantics

The only overlap between the two is semantics. Both attributes will announce that the button is indeed disabled, and that’s a good thing.

Contrary to the disabled attribute, aria-disabled is all about semantics. ARIA attributes never change the application behavior or styles by default. Their only purpose is to help assistive technologies (e.g. screen readers) to announce the page content in a more meaningful and robust way.

So far, we’ve talked about two types of people, those who click and those who Tab. Now let’s talk about another type: those with visual impairments (e.g. blindness, low vision) who use screen readers to navigate the web.

People who use screen readers, often prefer to navigate form fields using the Tab key. Now look an how VoiceOver on macOS completely skips the disabled button.

The VoiceOver screen reader skips the “Add to cart” button when using the Tab key.

Once again, this is a very minimal form. In a longer one, looking for a submit button that isn’t there right away can be annoying. Imagine a form where the submit button is hidden and only visible when you completely fill out the form. That’s what some people feel like when the disabled attribute is used.

Fortunately, buttons with disabled are not totally unreachable by screen readers. You can still navigate each element individually, one by one, and eventually you’ll find the button.

The VoiceOver screen reader is able to find and announce the “Add to cart” button.

Although possible, this is an annoying experience. On the other hand, with aria-disabled, the screen reader will focus the button normally and properly announce its status. Note that the announcement is slightly different between screen readers. For example, NVDA and JWAS say “button, unavailable” but VoiceOver says “button, dimmed.”

The VoiceOver screen reader can find the “Add to cart” button using Tab key because of aria-disabled.

I’ve mapped out how both attributes create different user experiences based on the tools we just used:

Tool / Attributedisabledaria-disabled="true"
Mouse or tapPrevents a button click.Requires JS to prevent the click.
TabUnable to focus the button.Able to focus the button.
Screen readerButton is difficult to locate.Button is easily located.

So, the main differences between both attributes are:

  • disabled might skip the button when using the Tab key, leading to confusion.
  • aria-disabled will still focus the button and announce that it exists, but that it isn’t enabled yet; the same way you might perceive it visually.

This is the case where it’s important to acknowledge the subtle difference between accessibility and usability. Accessibility is a measure of someone being able to use something. Usability is a measure of how easy something is to use.

Given that, is disabled accessible? Yes. Does it have a good usability? I don’t think so.

Can we do better?

I wouldn’t feel good with myself if I finished this article without showing you the real inclusive solution for our ticket form example. Whenever possible, don’t use disabled buttons. Let people click it at any time and, if necessary, show an error message as feedback. This approach also solves other problems:

  • Less cognitive friction: Allow people to submit the form at any time. This removes the uncertainty of whether the button is even disabled in the first place.
  • Color contrast: Although a disabled button doesn’t need to meet the WCAG 1.4.3 color contrast, I believe we should guarantee that an element is always properly visible regardless of its state. But that’s something we don’t have to worry about now because the button isn’t disabled anymore.

Final thoughts

The disabled attribute in <button> is a peculiar case where, although highly known by the community, it might not be the best approach to solve a particular problem. Don’t get me wrong because I’m not saying disabled is always bad. There are still some cases where it still makes sense to use it (e.g. pagination).

To be honest, I don’t see the disabled attribute exactly as an accessibility issue. What concerns me is more of a usability issue. By swapping the disabled attribute with aria-disabled, we can make someone’s experience much more enjoyable.

This is yet one more step into my journey on web accessibility. Over the years, I’ve discovered that accessibility is much more than complying with web standards. Dealing with user experiences is tricky and most situations require making trade-offs and compromises in how we approach a solution. There’s no silver bullet for perfect accessibility.

Our duty as web creators is to look for and understand the different solutions that are available. Only then we can make the best possible choice. There’s no sense in pretending the problems don’t exist.

At the end of the day, remember that there’s nothing preventing you from making the web a more inclusive place.

Bonus

Still there? Let me mention two last things about this demo that I think are worthy:

1. Live Regions will announce dynamic content

In the demo, two parts of the content changed dynamically: the form submit button and the success confirmation (“Added [X] tickets!”).

These changes are visually perceived, however, for people with vision impairments using screen readers, that just ain’t the reality. To solve it, we need to turn those messages into Live Regions. Those allow assistive technologies to listen for changes and announce the updated messages when they happen.

There is a .sr-only class in the demo that hides a <span> containing a loading message, but allows it to be announced by screen readers. In this case, aria-live="assertive" is applied to the <span> and it holds a meaningful message after the form is submitting and is in the process of loading. This way, we can announce to the user that the form is working and to be patient as it loads. Additionally, we do the same to the form feedback element.

<button type="submit" aria-disabled="true">
  Add to cart
  <span aria-live="assertive" class="sr-only js-loadingMsg">
     <!-- Use JavaScript to inject the the loading message -->
  </span>
</button>

<p aria-live="assertive" class="formStatus">
  <!-- Use JavaScript to inject the success message -->
</p>

Note that the aria-live attribute must be present in the DOM right from the beginning, even if the element doesn’t hold any message yet, otherwise, Assistive Technologies may not work properly.

Form submit feedback message being announced by the screen reader.

There’s much more to tell you about this little aria-live attribute and the big things it does. There are gotchas as well. For example, if it is applied incorrectly, the attribute can do more harm than good. It’s worth reading “Using aria-live” by Ire Aderinokun and Adrian Roselli’s “Loading Skeletons” to better understand how it works and how to use it.

2. Do not use pointer-events to prevent the click

This is an alternative (and incorrect) implementation that I’ve seen around the web. This uses pointer-events: none; in CSS to prevent the click (without any HTML attribute). Please, do not do this. Here’s an ugly Pen that will hopefully demonstrate why. I repeat, do not do this.

Although that CSS does indeed prevent a mouse click, remember that it won’t prevent focus and keyboard navigation, which can lead to unexpected outcomes or, even worse, bugs.

In other words, using this CSS rule as a strategy to prevent a click, is pointless (get it?). ;)


Making Disabled Buttons More Inclusive originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/making-disabled-buttons-more-inclusive/feed/ 25 https://css-tricks.com/wp-content/uploads/2021/05/40df467a4f094300a7275b9222304185-1618767462271.mp4 aria Archives - CSS-Tricks nonadult 339960
What’s New in WCAG 2.1: Label in Name https://css-tricks.com/whats-new-in-wcag-2-1-label-in-name/ https://css-tricks.com/whats-new-in-wcag-2-1-label-in-name/#comments Tue, 15 Dec 2020 15:47:52 +0000 https://css-tricks.com/?p=330805 WCAG 2.1 Recommendations rolled out in 2018. It’s been a couple years now and there are some new Success Criterion. In this article, I will discuss Label in Name, which is how we visually label components. We’ll take a …


What’s New in WCAG 2.1: Label in Name originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
WCAG 2.1 Recommendations rolled out in 2018. It’s been a couple years now and there are some new Success Criterion. In this article, I will discuss Label in Name, which is how we visually label components. We’ll take a look at what some failure states look like, how to fix them, and examples of how to do them correctly.

You lost me at Success Criterion…

A Success Criterion is a testable statement that isn’t technology-specific. It’s the baseline from which we determine whether our work is “accessible.” In this case, “Label in Name” is the thing being evaluated, which is among a whole slew of other criteria. WCAG 2.1 is the current version of the spec and “Label in Name” is item 2.5.3, indicating it is in the second category (“Operable”) of criterion, under the fifth section (“Input Modalities”) of that category, marked as the third item in the section.

WCAG 2.1 is backwards-compatible with WCAG 2.0, meaning it’s an extension of WCAG 2.0. Further, the releases of WCAG 2.1 and 2.2 are in conjunction with each other and they all work together.

Label in Name

So, getting back to things, 2.5.3 Label in Name (Level A) is new and defined in the WCAG 2.1 Success Criterion. Here’s how it’s described:

For user interface components with labels that include text or images of text, the name contains the text that is presented visually.

The intent of this Success Criterion (SC) is to ensure the words which a label has visually on the component are also included in the words that are associated with the component programatically. This helps ensure that anyone — whether it’s someone using voice recognition software or someone who is visually abled — can rely on labels to describe the intent of a component, or how to interact with it. The visual text label and the programmatic name do not have to be exact, mind you, but they should contain a common work that associates them (e.g., “Submit” with “Submit Now”).

The point is that there isn’t confusion, because of a discrepancy, between what is read and what is seen.

Assistive technology in action

Let’s use the example of an HTML contact form. A user may use voice recognition software to fill out a form and come to the end where the form is submitted and the form is sent.

Say the label of the button and the visual text in the button are inconsistent:

<form>
  <label>
    Message:
    <textarea name="message"></textarea>
  </label>

  <button aria-label="Submit">Send</button>
</form>

In the above example, the button doesn’t function properly for assistive technology. The button contains the text “Send” but its aria-label is “Submit.” This is where the failure lies. The visual label (“Send”) is inconsistent with the programmatic name (“Submit”), providing no association between the two.

When these match or have a common term, users of speech recognition software can navigate by speaking the visible text labels of components such as links, buttons, and menus. In this case, we could fix it by matching the label and the text. But since the aria-label adds no value, removing it altogether is a better fix:

<form>
  <label>
    Message:
    <textarea name="message"></textarea>
  </label>

  <button>Send</button>
</form>

Sighted users that use screen readers will also have a better experience if the text they hear is the text that’s similar to the text they see on the screen.

When the label and visual text don’t match, speech-input users attempting to use the visible text label as a means of navigation (e.g. “move to First Name”) or selection will get stuck because the visible label spoken by the user does not match or is not part of the accessible name that is enabled as part of the speech-input command.

Also, when the accessible name is different from the visible label, it may function as a hidden command that can be activated accidentally by speech-input users. SC does not apply where a visible text label does not exist for a component.

Code in action

Here are three different failure states.

Again, these are all examples of poor practices, according to the 2.5.3 Label in Name SC.

In 2020 the WebAIM Million project evaluated 4.2 million form inputs and found that 55% were improperly unlabeled, either via <label>, aria-label, or aria-labelledby.

When working with forms, most of us are pretty used to pairing a <label> with an <input> or some other form control. That’s awesome and a great way to indicate what the control does, but there’s also the control’s programmatic name, which is also known as the “accessible name” using an aria-label.

We get a better user experience when the name of the <label> can be associated with the programmatic (or accessible) name in the aria-label. For example, if we’re using “First Name” for an input’s <label>, then we probably want our aria-label to be “First Name” or something to that effect as well. A failure to draw a connection between programmatic names and visible labels can be more of a challenge for users with cognitive challenges. It requires additional cognitive load for speech-input users who must remember to say a speech command that is different from the visible label they see on a control. Extra cognitive load is also created when a text-to-speech user needs to absorb and understand speech output that can’t be connected to the visible label. These forms will submit, but it comes at a cost to accessibility and disabled users.

Here are those three examples from above fixed up!

Text in Label specifics

Per the WCAG SC, text should not be considered a visible label if it is used in a symbolic manner instead of expressing it directly in human language. A rich text editor is a good example of this because an editor might use images as text (which is included in 1.4.5 Images of Text).

To match the label text and accessible name with one another, it is important to determine which text should be considered the label for any component for any given control. There are often multiple text strings in a user interface that may be relevant to a control. There are reasons why the label in close proximity should be considered the text label. It’s about establishing a pattern of predictability for users interacting with a component. Those reason suggest that visible labels should be positioned:

  • immediately to the left of text inputs, dropdown boxes, and other widgets or components.
  • immediately to the right of checkboxes and radio buttons.
  • inside buttons or tabs or immediately below icons serving as buttons.
Labels to the left of inputs and dropdown select menus

Labels to the right of checkbox and radio buttons
Labels inside or below a button, depending on the symbol

Punctuation and capitalization may also be considered optional if used in a symbolic manner. For example, “First Name” is just fine instead of “First Name:” and “Next” is okay instead of “Next…” and so on.

Another thing to consider: components without a visual label are not considered by the WCAG SC.

Proper labeling has its perks

The core benefit of matching a component’s labels with its corresponding accessible name is that it gives speech-input users the ability to activate controls on a page without having to change focus or make guesses between two different terms.

In the end, using clear, consistent terminology between what is seen and what is spoken provides a more enjoyable user experience — for everyone — because the labels that get read by assistive technologies match the visible labels that can also be seen and read. This is what we talk about with inclusive design — everyone wins and no one is left out.

Summary

We just broke down some of the finer points of the WCAG 2.5.3 Success Criterion for labels in names. It sounds like a simple thing to follow. But as we’ve seen, there are situations where it’s not so clear-cut.

The goal of adhering to this criterion is, of course, to make our work accessible and inclusive for all people. The WCAG helps us know if we’re successful not only by providing guidelines, but by settings grades of compliance (A, AA, AAA, where AAA is the highest). Text in Label falls into the A grade, meaning it’s a base level of compliance. To earn the grade, the WCAG is looking for:

[…] user interface components with labels that include text or images of text, the name contains the text that is presented visually.

We can test and make certain that our code is complete and correct by looking at the source code of the site, using a browser’s DevTools, such as Chrome or Firefox, or running an accessibility audit using such tools as the WAVE browser extension (Chrome and Firefox) and Axe from Deque Systems (Chrome).

In short, there are real people on the other side of the glass and there are things we can do in our code and designs to help them enjoy interacting with the components we make. Text in Label is just one of many criteria outlined in the WCAG and, while it may seem like a small detail, adhering to it can make a big impact on our users.


What’s New in WCAG 2.1: Label in Name originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/whats-new-in-wcag-2-1-label-in-name/feed/ 9 330805
ARIA in CSS https://css-tricks.com/aria-in-css/ https://css-tricks.com/aria-in-css/#comments Tue, 10 Nov 2020 23:45:00 +0000 https://css-tricks.com/?p=325332 Jeremey reacting to Sara’s tweet, about using [aria-*] selectors instead of classes when the styling you are applying is directly related to the ARIA state.

… this is my preferred way of hooking up CSS and JavaScript interactions. Here’s


ARIA in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Jeremey reacting to Sara’s tweet, about using [aria-*] selectors instead of classes when the styling you are applying is directly related to the ARIA state.

… this is my preferred way of hooking up CSS and JavaScript interactions. Here’s [an] old CodePen where you can see it in action

Which is this classic matchup:

[aria-hidden='true'] {
  display: none;
}

There are plenty of more opportunities. Take a tab design component:

Since these tabs (using Reach UI) are already applying proper ARIA states for things like which tab is active, they don’t even bother with class name manipulation. To style the active state, you select the <button> with a data attribute and ARIA state like:

[data-reach-tab][aria-selected="true"] {
  background: white;
}

The panels with the content? Those have an ARIA role, so are styled that way:

[role="tabpanel"] {
  background: white;
}

ARIA is also matches up with variations sometimes, like…

[aria-orientation="vertical"] {
  flex-direction: column;
}

If you’re like, wait, what’s ARIA? Heydon’s new show Webbed Briefs has a funny introduction to ARIA as the pilot episode.

To Shared LinkPermalink on CSS-Tricks


ARIA in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/aria-in-css/feed/ 2 325332
Weekly Platform News: Mozilla’s AV1 Encoder, Samsung One UI CSS, DOM Matches Method https://css-tricks.com/weekly-platform-news-mozillas-av1-encoder-samsung-one-ui-css-dom-matches-method/ https://css-tricks.com/weekly-platform-news-mozillas-av1-encoder-samsung-one-ui-css-dom-matches-method/#comments Fri, 21 Jun 2019 15:10:50 +0000 http://css-tricks.com/?p=291856 In this week's weekly roundup, Vimeo and Mozilla partner up on a video encoding format, how to bind instructions to to form fields using aria labels, the DOM has a matching function, and Samsung is working on its own CSS library.


Weekly Platform News: Mozilla’s AV1 Encoder, Samsung One UI CSS, DOM Matches Method originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Šime posts regular content for web developers on webplatform.news.

In this week’s weekly roundup, Vimeo and Mozilla partner up on a video encoding format, how to bind instructions to to form fields using aria labels, the DOM has a matching function, and Samsung is working on its own CSS library.


Vimeo partners with Mozilla to use their rav1e encoder

Vittorio Giovara: AV1 is a royalty-free video codec designed by the Alliance for Open Media and the the most anticipated successor of H.264. Vimeo is contributing to the development of Mozilla’s AV1 encoder.

In order for AV1 to succeed, there is a need of an encoder like x264, a free and open source encoder, written by the community, for the community, and available to everyone: rav1e. Vimeo believes in what Mozilla is doing.

Use aria-describedby to bind instructions to form fields

Raghavendra Satish Peri: If you provide additional instructions for a form field, use the aria-describedby attribute to bind the instruction to the field. Otherwise, assistive technology users who use the Tab key might miss this information.

<label for="dob">Date of Birth</label>
<input type="text" aria-describedby="dob1" id="dob" />
<span id="dob1">Use DD/MM/YY</span>

Samsung Internet announces One UI CSS

Diego González: Samsung is experimentally developing a CSS library based on its new One UI design language. The library is called One UI CSS and includes styles for common form controls such as buttons, menus, and sliders, as well as other assets (web fonts, SVG icons, polyfills).

Some of the controls present in One UI CSS.

DOM elements have a matches method

Sam Thorogood: You can use the matches method to test if a DOM element has a specific CSS class, attribute or ID value. This method accepts a CSS selector and returns true if the element matches the given selector.

el.classList.has('foo')  /* becomes */ el.matches('.foo');
el.hasAttribute('hello') /* becomes */ el.matches('[hello]');
el.id === 'bar'          /* becomes */ el.matches('#bar');

Weekly Platform News: Mozilla’s AV1 Encoder, Samsung One UI CSS, DOM Matches Method originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/weekly-platform-news-mozillas-av1-encoder-samsung-one-ui-css-dom-matches-method/feed/ 4 291856
Why, How, and When to Use Semantic HTML and ARIA https://css-tricks.com/why-how-and-when-to-use-semantic-html-and-aria/ https://css-tricks.com/why-how-and-when-to-use-semantic-html-and-aria/#comments Tue, 07 May 2019 14:22:15 +0000 http://css-tricks.com/?p=286856 Semantic HTML and Accessible Rich Internet Applications (ARIA) help create interfaces that work for everyone in the most performant, robust, and simple way possible. They add essential meaning to your content, which lets web browsers, search engines, screen readers, RSS …


Why, How, and When to Use Semantic HTML and ARIA originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Semantic HTML and Accessible Rich Internet Applications (ARIA) help create interfaces that work for everyone in the most performant, robust, and simple way possible. They add essential meaning to your content, which lets web browsers, search engines, screen readers, RSS readers, and ultimately users understand it.

And yet, many people still don’t use them. I wanted to know why, so I set up a Twitter poll. The most common reason people gave was a lack of awareness and understanding of the benefits of using semantic HTML and ARIA.

Let’s look over the benefits of using HTML and ARIA, why starting with semantic HTML is the way to go, and why ARIA ought to come in as a last resort.

Starting with raw text

The <body> element of an HTML document contains the main content a user sees on a page. If content is put inside the body without any additional elements, the browser has no way of differentiating between different types of content, like paragraphs and headings.

<body>
A Study of Butterflies

Butterflies are little bugs with cute wings.

Butterfly Habitats

Butterflies live in flower houses and hang out at dank coffeeshops.
</body>

If the browser can’t differentiate between pieces of content, then it can’t present that content to the user in a meaningful way. That means:

  • We can’t style the headings differently from paragraphs.
  • It’s harder for search engines to interpret the content, meaning it’s likely to rank poorly and be difficult for users to find.
  • Screen readers and other assistive technology can’t communicate it properly to the user.

Not to mention, it’s more than a bit awkward visually:

A screenshot of the HTML rendered on the front end, which displays as a single line of text.

Adding some structure with HTML

To provide some structure we could wrap the lines of text here in divs like this:

<div>A Study of Butterflies.</div>
<div>Butterflies are little bugs with cute wings.</div>
<div>Butterfly Habitats</div>
<div>Butterflies live in flower houses and hang out at dank coffeeshops.</div>

This is slightly better because each piece of content is displayed in the browser on its own line instead of one long unbroken line of text.

A screenshot of the HTML rendered on the front-end showing the content on four lines, one for each div.

But there’s still a distinct lack of meaning.

Which are headings and which are paragraphs? It’s hard to tell, and impossible for assistive technology to determine. That’s because the headings and paragraphs are wrapped in divs which are meaningless on their own. In this example, browsers, CSS, search engines and screen readers are still none the wiser.

Communicating meaning with styles

We could add styling to the divs because they can be targetted with CSS. This lets us improve the visual appearance to provide meaning, context, and hierarchy.

See the Pen
Non-Semantic HTML Demo
by Geoff Graham (@geoffgraham)
on CodePen.

Here the CSS targets the first and third divs to apply heading styles. This isn’t maintainable because another paragraph added afterward, for example, would get styled as a heading.

We could give each div a unique attribute such as an ID or class name, to give us more styling control, like this:

<div class="heading1">A Study of Butterflies</div>
<div class="paragraph">Butterflies are little bugs with cute wings.</div>
<div class="heading2">Butterfly Habitats</div>
<div class="paragraph">Butterflies live in flower houses and hang out at dank coffeeshops.</div>

I explain why you should use classes instead of IDs for styling in my online book, MaintainableCSS.

Now we can target the different elements with CSS like this:

.heading1 { /* styles here */ }
.paragraph { /* styles here */ }
.heading2 { /* styles here */ }

While this approach is a little better, it only communicates meaning for sighted users. It doesn’t provide meaning to search engines, RSS readers and screen readers. In other words, it’s not semantic and not very accessible as a result.

Introducing semantic HTML

HTML provides many elements that are designed to give meaning to content, including elements for headings and paragraphs. So, instead of relying on divs with classes named by the developer, we can use predefined HTML elements instead.

<h1>A Study of Butterflies</h1>
<p>Butterflies are little bugs with cute wings.</p>
<h2>Butterfly Habitats</h2>
<p>Butterflies live in flower houses and hang out at dank coffeeshops.</p>

Much better! With semantic HTML like this, the content will inherit default styles from the browser (aka User Agent). We can even use other semantic HTML elements, like <b> which tells the browser to “bring to attention” by making text bold.

A screenshot of the HTML rendered on the front end showing the content with a clearer hierarchy of a Heading 1, paragraph, Heading 2 and paragraph, thanks to semantic HTML.

Crucially, using semantic HTML also means:

  • We can use CSS to add our own styling.
  • Search engines can index the content so that it ranks well enough that users can find it.
  • RSS readers can parse and style the elements appropriately.
  • Screen readers and other assistive technologies can communicate elements properly to the user.

While it’s not massively important in these short examples, the code is also more concise which makes a big difference when considering an entire site.

Semantic HTML is standards-based and stable. This means any HTML processor in the future will be able to understand it and present it correctly to users. It will also help subsequent code authors if they need to make changes.

Additional benefits of semantic HTML

In addition to the benefits we’ve covered so far, some browsers add useful enhancements to semantic HTML for free.

For example, using the HTML telephone input (<input type="tel">) will give users a telephone-specific keypad on some mobile browsers.

Identifying a form input as a telephone field will produce a telephone-specific keypad in iOS. Careful though, because it’s just for telephone numbers and not meant for any number, like credit cards.

Other browsers give users the option to switch to a simplified view of the page, like Safari’s Reader Mode. Without semantic HTML, Reader Mode would produce something much like the one-line string of text we started with. But, by using semantic HTML, we get a clean reading experience without any additional styling on our end:

The About page on my personal website viewed with Safari’s Reader Mode, comparing unsemantic HTML (left) with semantic HTML (right).

You can read more about this in Mandy Michael’s article on building websites for Safari Reader Mode and other reading apps.

When ARIA makes things better

Like semantic HTML, ARIA is a W3 standard that helps make interfaces more accessible to people who use screen readers and other assistive technologies to consume content.

Error messages are a good example. If a user leaves a required form field blank, the HTML for the error might look like this:

<label for="first-name">First name</label>
<span>Enter your first name</span> 
<input type="text" name="first-name" id="first-name">

A sighted user will be able to see the error above the field. But when a screen reader focuses on the input, the error won’t be announced because the error message isn’t linked to the input.

ARIA can be used to associate the error with the input like this:

<label for="first-name">First name</label>
<span id="first-name-error">Enter your first name</span>
<input type="text" name="first-name" id="first-name" aria-describedby="first-name-error">

Now the error message is announced when the input is in focus.

Using ARIA and JavaScript together

ARIA is most useful when JavaScript is involved. JavaScript is usually required to create more complex and dynamic interactions like hiding, showing and changing elements without a page refresh. Think toggle menus, accordions, tabs, auto-completes, sortable tables, loading content and saving, sending or getting data. Enhancing interfaces like this often breaks the experience for screen reader users.

Take a button that, when selected, reveals other content. In the original state, a sighted user will initially see a button and no content and, when the button is clicked, the content appears.

A visually-impaired user with a screen reader, however, usually relies on spoken cues as they navigate through an interface. But when a screen reader focuses on the button, there’s nothing to tell it if the content is currently in view and needs to be read.

The aria-expanded attribute can be added to the button, and JavaScript can toggle its value between true (content is showing) and false (content is hidden). This helps to meet Inclusive Design Principle #1, provide a comparable experience to screen reader users.

<button aria-expanded="false">Toggle content</button>
<div hidden>Some content</div>

Try to avoid using ARIA to fix unsemantic HTML

ARIA attributes can be used to make unsemantic HTML more accessible to screen reader users. For example, a developer who is struggling to style a native checkbox across multiple browsers might decide to use a div and some JavaScript to emulate one.

We can add a role of checkbox to make the div identity itself as a checkbox to screen reader users:

<div role="checkbox"></div>

We must also use the aria-checked attribute to indicate whether or not the checkbox is checked like this:

<div role="checkbox" aria-checked="false"></div>

But, this still isn’t enough to make it behave like a checkbox because divs aren’t focusable by keyboards like <input type="checkbox"> is. We could make them focusable by adding tabindex="0":

<div role="checkbox" aria-checked="false" tabindex="0"></div>

But even then, a real checkbox, when submitted as part of a form, will send its value. Because this isn’t an actual a checkbox, it won’t submit its value without using JavaScript.

And if that weren’t enough already, users can check or un-check a real checkbox by pressing the Space key. And, the form the checkbox belongs to can be submitted by pressing Enter when the checkbox is in focus. But the div-version checkbox won’t do this without even more JavaScript.

Not only is this more work and more code, but the approach only actually works for people who use technology that understands these particular ARIA attributes. That’s a lot of effort, a lot of code and a lot of problems that we can avoid entirely if we just use semantic HTML:

<input type="checkbox">

There’s no denying that ARIA is useful in certain situations, but starting with semantic, accessible HTML where possible is infinitely simpler and more reliable. That’s why the first rule of ARIA is not to use it.

Conclusion

Inclusive design is about providing the best possible experience to the broadest range of users. Semantic HTML helps technologies and therefore users understand content on the web. Enhancements offered by some browsers and devices mean that users get an even better experience baked in.

And when semantic HTML alone is not enough on its own, ARIA can provide more context for users of assistive technologies, but use it with caution. It’s not a hard and fast cure for unsemantic markup and can become complicated, as we saw in that last example.

In short, do the hard work to make things inclusive. It’s a win for you and a win for the web.


Why, How, and When to Use Semantic HTML and ARIA originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/why-how-and-when-to-use-semantic-html-and-aria/feed/ 8 286856
Short note on what CSS display properties do to table semantics https://css-tricks.com/short-note-on-what-css-display-properties-do-to-table-semantics/ Wed, 14 Mar 2018 20:14:19 +0000 http://css-tricks.com/?p=268207 We’ve blogged about responsive tables a number of times over the years. There’s a variety of techniques, and which you choose should be based on the data in the table and the UX you’re going for. But many of them …


Short note on what CSS display properties do to table semantics originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
We’ve blogged about responsive tables a number of times over the years. There’s a variety of techniques, and which you choose should be based on the data in the table and the UX you’re going for. But many of them rely upon resetting a table element’s natural display value to something else, for example display: block. Steve Faulkner warns us:

When CSS display: block or display: grid or display: flex is set on the table element, bad things happen. The table is no longer represented as a table in the accessibility tree, row elements/semantics are no longer represented in any form.

He argues that the browser is making a mistake here by altering those semantics, but since they do, it’s good to know it’s fixable with (a slew of) ARIA roles.

Here’s more from Adrian Roselli including a demo with proper markup.

To Shared LinkPermalink on CSS-Tricks


Short note on what CSS display properties do to table semantics originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
268207
ARIA is Spackle, Not Rebar https://css-tricks.com/aria-spackle-not-rebar/ https://css-tricks.com/aria-spackle-not-rebar/#comments Wed, 08 Nov 2017 15:05:51 +0000 http://css-tricks.com/?p=262085 Much like their physical counterparts, the materials we use to build websites have purpose. To use them without understanding their strengths and limitations is irresponsible. Nobody wants to live in an poorly-built house. So why are poorly-built websites acceptable?

In …


ARIA is Spackle, Not Rebar originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Much like their physical counterparts, the materials we use to build websites have purpose. To use them without understanding their strengths and limitations is irresponsible. Nobody wants to live in an poorly-built house. So why are poorly-built websites acceptable?

In this post, I’m going to address WAI-ARIA, and how misusing it can do more harm than good.

Materials as technology

In construction, spackle is used to fix minor defects on interiors. It is a thick paste that dries into a solid surface that can be sanded smooth and painted over. Most renters become acquainted with it when attempting to get their damage deposit back.

Rebar is a lattice of steel rods used to reinforce concrete. Every modern building uses it—chances are good you’ll see it walking past any decent-sized construction site.

Technology as materials

HTML is the rebar-reinforced concrete of the web. To stretch the metaphor, CSS is the interior and exterior decoration, and JavaScript is the wiring and plumbing.

Every tag in HTML has what is known as native semantics. The act of writing an HTML element programmatically communicates to the browser what that tag represents. Writing a button tag explicitly tells the browser, “This is a button. It does buttony things.”

The reason this is so important is that assistive technology hooks into native semantics and uses it to create an interface for navigation. A page not described semantically is a lot like a building without rooms or windows: People navigating via a screen reader have to wander around aimlessly in the dark and hope they stumble onto what they need.

ARIA stands for Accessible Rich Internet Applications and is a relatively new specification developed to help assistive technology better communicate with dynamic, JavaScript-controlled content. It is intended to supplement existing semantic attributes by providing enhanced interactivity and context to screen readers and other assistive technology.

Using spackle to build walls

A concerning trend I’ve seen recently is the blind, mass-application of ARIA. It feels like an attempt by developers to conduct accessibility compliance via buckshot—throw enough of something at a target trusting that you’ll eventually hit it.

Unfortunately, there is a very real danger to this approach. Misapplied ARIA has the potential to do more harm than good.

The semantics inherent in ARIA means that when applied improperly it can create a discordant, contradictory mess when read via screen reader. Instead of hearing, “This is a button. It does buttony things.”, people begin to hear things along the lines of, “This is nothing, but also a button. But it’s also a deactivated checkbox that is disabled and it needs to shout that constantly.”

If you can use a native HTML element or attribute with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so.
First rule of ARIA use

In addition, ARIA is a new technology. This means that browser support and behavior is varied. While I am optimistic that in the future the major browsers will have complete and unified support, the current landscape has gaps and bugs.

Another important consideration is who actually uses the technology. Compliance isn’t some purely academic vanity metric we’re striving for. We’re building robust systems for real people that allow them to get what they want or need with as little complication as possible. Many people who use assistive technology are reluctant to upgrade for fear of breaking functionality. Ever get irritated when your favorite program redesigns and you have to re-learn how to use it? Yeah.

The power of the Web is in its universality. Access by everyone regardless of disability is an essential aspect.
– Tim Berners-Lee

It feels disingenuous to see the benefits of the DRY principal of massive JavaScript frameworks also slather redundant and misapplied attributes in their markup. The web is accessible by default. For better or for worse, we are free to do what we want to it after that.

The fix

This isn’t to say we should completely avoid using ARIA. When applied with skill and precision, it can turn a confusing or frustrating user experience into an intuitive and effortless one, with far fewer brittle hacks and workarounds.

A little goes a long way. Before considering other options, start with markup that semantically describes the content it is wrapping. Test extensively, and only apply ARIA if deficiencies between HTML’s native semantics and JavaScript’s interactions arise.

Development teams will appreciate the advantage of terse code that’s easier to maintain. Savvy developers will use a CSS-Trick™ and leverage CSS attribute selectors to create systems where visual presentation is tied to semantic meaning.

input:invalid,
[aria-invalid] {
  border: 4px dotted #f64100;
}

Examples

Here are a few of the more common patterns I’ve seen recently, and why they are problematic. This doesn’t mean these are the only kinds of errors that exist, but it’s a good primer on recognizing what not to do:

<li role="listitem">Hold the Bluetooth button on the speaker for three seconds to make the speaker discoverable</li>

The role is redundant. The native semantics of the li element already describe it as a list item.

<p role="command">Type CTRL+P to print</p>

command is an Abstract Role. They are only used in ARIA to help describe its taxonomy. Just because an ARIA attribute seems like it is applicable doesn’t mean it necessarily is. Additionally, the kbd tag could be used on “CTRL” and “P” to more accurately describe the keyboard command.

<div role="button" class="button">Link to device specifications</div>

Failing to use a button tag runs the risk of not accommodating all the different ways a user can interact with a button and how the browser responds. In addition, the a tag should be used for links.

<body aria-live="assertive" aria-atomic="true">

Usually the intent behind something like this is to expose updates to the screen reader user. Unfortunately, when scoped to the body tag, any page change—including all JS-related updates—are announced immediately. A setting of assertive on aria-live also means that each update interrupts whatever it is the user is currently doing. This is a disastrous experience, especially for single page apps.

<div aria-checked="true"></div>

You can style a native checkbox element to look like whatever you want it to. Better support! Less work!

<div role="link" tabindex="40">
  Link text
</div>

Yes, it’s actual production code. Where to begin? First, never use a tabindex value greater than 0. Secondly, the title attribute probably does not do what you think it does. Third, the anchor tag should have a destination—links take you places, after all. Fourth, the role of link assigned to a div wrapping an a element is entirely superfluous.

<h2 class="h3" role="heading" aria-level="1">How to make a perfect soufflé every time</h2>

Credit is where credit’s due: Nicolas Steenhout outlines the issues for this one.

Do better

Much like content, markup shouldn’t be an afterthought when building a website. I believe most people are genuinely trying to do their best most of the time, but wielding a technology without knowing its implications is dangerous and irresponsible.

I’m usually more of a honey-instead-of-vinegar kind of person when I try to get people to practice accessibility, but not here. This isn’t a soft sell about the benefits of developing and designing with an accessible, inclusive mindset. It’s a post about doing your job.

Every decision a team makes affects a site’s accessibility.
Laura Kalbag

Get better at authoring

Learn about the available HTML tags, what they describe, and how to best use them. Same goes for ARIA. Give your page template semantics the same care and attention you give your JavaScript during code reviews.

Get better at testing

There’s little excuse to not incorporate a screen reader into your testing and QA process. NVDA is free. macOS, Windows, iOS and Android all come with screen readers built in. Some nice people have even written guides to help you learn how to use them.

Automated accessibility testing is a huge boon, but it also isn’t a silver bullet. It won’t report on what it doesn’t know to report, meaning it’s up to a human to manually determine if navigating through the website makes sense. This isn’t any different than other usability testing endeavors.

Build better buildings

Universal Design teaches us that websites, like buildings, can be both beautiful and accessible. If you’re looking for a place to start, here are some resources:


ARIA is Spackle, Not Rebar originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/aria-spackle-not-rebar/feed/ 3 262085
Managing State in CSS with Reusable JavaScript Functions – Part 2 https://css-tricks.com/managing-state-css-reusable-javascript-functions-part-2/ https://css-tricks.com/managing-state-css-reusable-javascript-functions-part-2/#comments Tue, 30 May 2017 12:48:48 +0000 http://css-tricks.com/?p=255301 In my previous article, which shall now retroactively be known as Managing State in CSS with Reusable JavaScript Functions – Part 1, we created a powerful reusable function which allows us to quickly add, remove and toggle stateful classes …


Managing State in CSS with Reusable JavaScript Functions – Part 2 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
In my previous article, which shall now retroactively be known as Managing State in CSS with Reusable JavaScript Functions – Part 1, we created a powerful reusable function which allows us to quickly add, remove and toggle stateful classes via click.

One of the reasons I wanted to share this approach was to see what kind of response it would generate. Since then I’ve received some interesting feedback from other developers, with some raising valid shortcomings about this approach that would have never otherwise occurred to me.

In this article, I’ll be providing some solutions to these shortcomings, as well as baking in more features and general improvements to make our reusable function even more powerful.

Article Series:

  1. Original article
  2. Managing State in CSS with Reusable JavaScript Functions (You are here!)

For reference, here’s the JavaScript from Part 1 for our reusable function as it stands:

// Grab all elements with required attributes
var elems = document.querySelectorAll("[data-class][data-class-element]");

// closestParent helper function
closestParent = function(child, match) {
  if (!child || child == document) {
    return null;
  }
  if (child.classList.contains(match) || child.nodeName.toLowerCase() == match) {
    return child;
  }
  else {
    return closestParent(child.parentNode, match);
  }
}

// Loop through if any are found
for(var i = 0; i < elems.length; i++){
  // Add event listeners to each one
  elems[i].addEventListener("click", function(e){

    // Prevent default action of element
    e.preventDefault();

    // Grab classes list and convert to array
    var dataClass = this.getAttribute('data-class');
    dataClass = dataClass.split(", ");

    // Grab linked elements list and convert to array
    var dataClassElement = this.getAttribute('data-class-element');
    dataClassElement = dataClassElement.split(", ");

    // Grab data-class-behaviour list if present and convert to array
    if(this.getAttribute("data-class-behaviour")) {
      var dataClassBehaviour = this.getAttribute("data-class-behaviour");
      dataClassBehaviour = dataClassBehaviour.split(", ");
    }

    // Grab data-scope list if present and convert to array
    if(this.getAttribute("data-class-scope")) {
      var dataClassScope = this.getAttribute("data-class-scope");
      dataClassScope = dataClassScope.split(", ");
    }

    // Loop through all our dataClassElement items
    for(var b = 0; b < dataClassElement.length; b++) {
      // Grab elem references, apply scope if found
      if(dataClassScope && dataClassScope[b] !== "false") {
        // Grab parent
        var elemParent = closestParent(this, dataClassScope[b]),

        // Grab all matching child elements of parent
        elemRef = elemParent.querySelectorAll("." + dataClassElement[b]);

        // Convert to array
        elemRef = Array.prototype.slice.call(elemRef);

        // Add parent if it matches the data-class-element and fits within scope
        if(dataClassScope[b] === dataClassElement[b] && elemParent.classList.contains(dataClassElement[b])) {
          elemRef.unshift(elemParent);
        }
      }
      else {
        var elemRef = document.querySelectorAll("." + dataClassElement[b]);
      }
      // Grab class we will add
      var elemClass = dataClass[b];
      // Grab behaviour if any exists
      if(dataClassBehaviour) {
        var elemBehaviour = dataClassBehaviour[b];
      }
      // Do
      for(var c = 0; c < elemRef.length; c++) {
        if(elemBehaviour === "add") {
          if(!elemRef[c].classList.contains(elemClass)) {
            elemRef[c].classList.add(elemClass);
          }
        }
        else if(elemBehaviour === "remove") {
          if(elemRef[c].classList.contains(elemClass)) {
            elemRef[c].classList.remove(elemClass);
          }
        }
        else {
          elemRef[c].classList.toggle(elemClass);
        }
      }
    }

  });    
}

Going forward, this is going to serve as the base for our improvements.

Let’s get started!

Accessibility

The most common piece of feedback I’ve received from other developers in response to Part 1 was the approach’s lack of consideration for accessibility. More specifically, it’s lack of support for ARIA attributes (or ARIA states if you prefer) and its failure to provide keyboard events for triggering our reusable function.

Let’s see how we can integrate both.

ARIA attributes

ARIA attributes form part of the WAI-ARIA specification. In the words of the specification they…

…are used to support platform accessibility APIs on various operating system platforms. Assistive technologies may access this information through an exposed user agent DOM or through a mapping to the platform accessibility API. When combined with roles, the user agent can supply the assistive technologies with user interface information to convey to the user at any time. Changes in states or properties will result in a notification to assistive technologies, which could alert the user that a change has occurred.

Revisiting the accordion example from part 1, an aria-expanded attribute set to true when the component is expanded, and vice versa when at its default state, would allow assistive technologies such as screen readers to better assess the component.

In addition to providing these benefits, as Ben Frain explores in his article, we can drop stateful classes and instead rely on ARIA attributes as our CSS hooks for styling some component state:

Adopting this approach results in what is (cringingly) referred to as a “Win Win” situation. We get to improve the accessibility of our web application, while also gaining a clearly defined, well-considered lexicon for communicating the states we need in our application logic.

For example, instead of:

.c-accordion.is-active .c-accordion__content {
  [...]
}

We would have:

.c-accordion[aria-expanded="true"] .c-accordion__content {
  [...]
}

Coming back to our reusable function, we’ll build in support so the data-class attribute can also accept an ARIA attribute reference. Since we’re now manipulating attributes rather than just classes, it would make sense semantically to rename data-class and all of its associated attributes to data-state:

<div class="c-mycomponent" data-state="aria-expanded" data-state-element="c-mycomponent" aria-expanded="false" tabindex="0">

In the above example, clicking c-mycomponent should toggle aria-expanded on itself. Whilst in the below example, in addition to the previous behaviour my-class would be removed from c-myothercomponent.

<div class="c-mycomponent" data-state="aria-expanded, my-class" data-state-element="c-mycomponent, c-myothercomponent" data-state-behaviour="toggle, remove" aria-expanded="false" tabindex="0">

In addition to aria-expanded, other examples of how ARIA attributes could be used instead of stateful classes are:

  • aria-disabled="true" instead of is-disabled
  • aria-checked="true" instead of is-checked
  • aria-pressed="true" or aria-selected="true" instead of is-active

Here’s a handy ARIA cheatsheet which came in handy whilst researching this article.

Implementation

Our reusable function currently assumes that everything passed to it via our newly renamed data-state attribute is a class. It then acts accordingly based on either what’s defined in data-state-behaviour or its default toggle behaviour:

// Cycle through target elements
for(var c = 0; c < elemRef.length; c++) {
  if(elemBehaviour === "add") {
    if(!elemRef[c].classList.contains(elemClass)) {
      elemRef[c].classList.add(elemClass);
    }
  }
  else if(elemBehaviour === "remove") {
    if(elemRef[c].classList.contains(elemClass)) {
      elemRef[c].classList.remove(elemClass);
    }
  }
  else {
    elemRef[c].classList.toggle(elemClass);
  }
}

Let’s tweak this slightly:

// Cycle through target elements
for(var c = 0; c < elemRef.length; c++) {
    // Find out if we're manipulating aria-attributes or classes
    var toggleAttr;
    if(elemRef[c].getAttribute(elemState)) {
        toggleAttr = true;
    }
    else {
        toggleAttr = false;
    }
    if(elemBehaviour === "add") {
        if(toggleAttr) {
            elemRef[c].setAttribute(elemState, true);
        }
        else {
            elemRef[c].classList.add(elemState);
        }
    }
    else if(elemBehaviour === "remove") {
        if(toggleAttr) {
            elemRef[c].setAttribute(elemState, false);
        }
        else {
            elemRef[c].classList.remove(elemState);
        }
    }
    else {
        if(toggleAttr) {
            if(elemRef[c].getAttribute(elemState) === "true") {
                elemRef[c].setAttribute(elemState, false);
            }
            else {
                elemRef[c].setAttribute(elemState, true);
            }
        }
        else {
            elemRef[c].classList.toggle(elemState);
        }
    }
}

To support ARIA attributes, we’ve simply added a check to first see if the given ARIA attribute exists on the element, and if not, assume it’s a class and process it as before. This way we can support both ARIA attributes and classes to cover all eventualities. Also removed are the classList.contains() checks, as in current spec classList.add() and classList.remove() are smart enough to account for this.

Keyboard Events

For a website to be considered accessible, it’s important that it can be easily navigated and interacted with through just the use of a keyboard. As far as the developer is concerned, this often involves the usage of the tabindex attribute and leveraging keyboard events.

In most browsers, elements such as the anchor already have these properties by default. You can tab to them and when in focus they can be activated on the press of the enter key. However for many components built with a combination of semantic elements and divs, this is not the case.

Let’s make our reusable function pick up this slack by writing logic to automatically add a keyboard event to the trigger element so it can be activated – like an anchor – with a press of the enter key.

Implementation

At the moment, as the function logic is triggered by clicking an element with data-state and data-state-element attributes, everything is wrapped in a click event listener:

elems[i].addEventListener("click", function(e){
  // Function logic
});

As a press of the enter key is going to need to trigger the same function logic as a click, it makes sense to seperate out this logic into it’s own function so it can be triggered from either. We’ll call it processChange():

// Assign click event
elem.addEventListener("click", function(e){
    // Prevent default action of element
    e.preventDefault(); 
    // Run state function
    processChange(this);
});
// Add keyboard event for enter key to mimic anchor functionality
elem.addEventListener("keypress", function(e){
    // e.which refers to the key pressed, 13 being the enter key.
    if(e.which === 13) {
        // Prevent default action of element
        e.preventDefault();
        // Run state function
        processChange(this);
    }
});

In addition to the existing click event listener, we’ve added an extra listener to react when the enter key is pressed. When a matching keypress event occurs on a focused trigger element, it’s just a matter of running our new processChange() function and passing the element.

You will also notice there’s no logic to automatically add a tabIndex attribute. This is because it may conflict with any tabIndex hierarchy already defined on the page and interfere with developer intent.

Example

Here’s a modified version of the accordion example from part 1, but fully updated to leverage ARIA attributes and keyboard events to make it a more accessible component. You can see the full reusable function as it now stands in the JavaScript panel.

See the Pen #7) Accessibility example by Luke Harrison (@lukedidit) on CodePen.

Accounting for elements added to DOM later

In Part 1, an issue was raised in the comments section:

I think this would have some issues for elements added to the DOM at a later point. In that case you would need to repeat assigning the click event. Am I correct?

That is correct! Any elements with data-state and data-state-element attributes added after the initial render of the DOM won’t have any event listeners assigned to them. So when they are clicked or swiped, nothing will happen.

Why? This is because in our Javascript, once the initial round of assigning event listeners to elements with data-state and data-state-element is complete, there’s no functionality in place to say “Hey! Watch out for any new elements withdata-state and data-state-element attributes and make them work.”

Implementation

To fix this, we’ll leverage something called MutationObservers. Whilst they can be explained much better in David Walsh’s great overview of the API, MutationObservers essentially allow us to track any nodes added or removed from the DOM (also known as “DOM mutations”).

We can setup one up like so:

// Setup mutation observer to track changes for matching elements added after initial DOM render
var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
        for(var d = 0; d < mutation.addedNodes.length; d++) {
            // Check if we're dealing with an element node
            if(typeof mutation.addedNodes[d].getAttribute === 'function') {
                if(mutation.addedNodes[d].getAttribute("data-state") && mutation.addedNodes[d].getAttribute("data-state-element")) {
                     // Create click and keyboard event listeners etc
                }
            }
        }
    });  
});

// Define type of change our observer will watch out for
observer.observe(document.body, {
  childList: true,
  subtree: true
});

This is what our MutationObserver is doing:

  1. Recording any DOM mutation to the body element which are immediate children childList: true or its decendents subtree: true
  2. Checking if that DOM mutation is a new element node, rather than a text node
  3. If true, then check if the new element node has data-state and data-state-element attributes

The next step, assuming these 3 checks pass, would be to setup our click and keypress event listeners. Like with the implementation of keyboard events, let’s seperate out this setup logic into its own function so we can reuse it both on page load and when an element with data-state and data-state-element attributes is detected by our MutationObserver.

We’ll call this new function initDataState().

// Init function
initDataState = function(elem){
  // Add event listeners to each one
  elems.addEventListener("click", function(e){
    // Prevent default action of element
    e.preventDefault();
    // Run state function
    processChange(this);
  });    
  // Add keyboard event for enter key to mimic anchor functionality
  elems.addEventListener("keypress", function(e){
      if(e.which === 13) {
          // Prevent default action of element
          e.preventDefault();
          // Run state function
          processChange(this);
      }
  });
}

Then it’s just a matter of hooking everything up correctly:

// Run when DOM has finished loading
document.addEventListener("DOMContentLoaded", function() {

  // Grab all elements with required attributes
  var elems = document.querySelectorAll("[data-state][data-state-element]");

  // Loop through if any are found
  for(var a = 0; a < elems.length; b++){
    initDataState(elems[a]);
  }

  // Setup mutation observer to track changes for matching elements added after initial DOM render
  var observer = new MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
          for(var d = 0; d < mutation.addedNodes.length; d++) {
              // Check if we're dealing with an element node
              if(typeof mutation.addedNodes[d].getAttribute === 'function') {
                  if(mutation.addedNodes[d].getAttribute("data-state")) {
                       initDataState(mutation.addedNodes[d]);
                  }
              }
          }
      });  
  });

  // Define type of change our observer will watch out for
  observer.observe(document.body, {
      childList: true,
      subtree: true
  });
});

Example

Click the “Add” button to insert more elements into the page (example Pen below):

See the Pen #8) Correctly set up new data-class elements when they are added to the DOM by Luke Harrison (@lukedidit) on CodePen.

Swipe support

At the moment, our reusable function is using click and keyboard events to trigger state logic. This is fine at desktop level, but on touch devices for some UI components (Such as closing a sliding navigation menu for instance) it’s often more useful to have this logic trigger on detection of a swipe.

Let’s build in optional swipe support for our reusable function. This will require adding a new data attribute to compliment our existing set:

data-state-swipe

The purpose of this new attribute is to allow us to define the swipe direction which should trigger our state logic. These directions should be:

  • up
  • right
  • down
  • left

Let’s also build in the option to specify if or not the swipe event should replace the click event, or if both should coexist. We can add a comma separated boolean to data-state-swipe to trigger this behaviour:

  • true – Swipe event listener replaces click event listener
  • false – Swipe event listener and click event listener are both added (Default)

So for example, when the div below detects a left swipe, the aria-expanded attribute on js-elem would be changed to true. The swipe event listener would also in this instance replace the click event listener, as we’re passing true in data-state-swipe:

<div data-state="aria-expanded" data-state-element="js-elem" data-state-swipe="left, true" data-state-behaviour="add">

Now let’s make the changes.

Implementation

Swipe is managed in just the same way as you would manage clicks and keyboard input – via event listeners. To keep the article focused on our reusable function, I’ll be using a helper function called swipeDetect() which will handle all the calculations required for accurate swipe detection. However feel free to use your own preferred method of detecting swipe direction in place of it.

We’re building swipe into our reusable function as another way of triggering function logic, so it makes sense that it should sit with the click and keyboard event listeners in initDateState() and then trigger processChange() once our requirements for a desired swipe direction are met.

Though we also have to account for the behaviour flag passed in data-state-swipe that determines if swipe should replace click. Let’s refactor initDataState() to add some scaffolding to properly support all of this:

// Init function
initDataState = function(elem){
    // Detect data-swipe attribute before we do anything, as its optional
    // If not present, assign click event like before
    if(elem.getAttribute("data-state-swipe")){
        // Grab swipe specific data from data-state-swipe   
        var elemSwipe = elem.getAttribute("data-state-swipe"),
              elemSwipe = elemSwipe.split(", "),
              swipeDirection = elemSwipe[0],
              elemSwipeBool = elemSwipe[1],
              currentElem = elem;

        // If the behaviour flag is set to "false", or not set at all, then assign our click event
        if(elemSwipeBool === "false" || !elemSwipeBool) {
            // Assign click event
            elem.addEventListener("click", function(e){
                // Prevent default action of element
                e.preventDefault(); 
                // Run state function
                processChange(this);
            });
        }
        // Use our swipeDetect helper function to determine if the swipe direction matches our desired direction
        swipeDetect(elem, function(swipedir){
            if(swipedir === swipeDirection) {
                // Run state function
                processChange(currentElem);
            }
        })
    }
    else {
        // Assign click event
        elem.addEventListener("click", function(e){
            // Prevent default action of element
            e.preventDefault(); 
            // Run state function
            processChange(this);
        });
    }
    // Add keyboard event for enter key to mimic anchor functionality
    elem.addEventListener("keypress", function(e){
        if(e.which === 13) {
            // Prevent default action of element
            e.preventDefault();
            // Run state function
            processChange(this);
        }
    });
};

These amends to initDataState now give it 3 different outcomes:

  1. If there’s a data-state-swipe attribute on the trigger element, and its behaviour boolean is set to true, then only swipe and keyboard events are assigned.
  2. If there’s a data-state-swipe attribute on the trigger element, but its behaviour boolean is set to false, then swipe, click and keyboard events are all assigned.
  3. If there’s no data-state-swipe attribute on the trigger element altogether, only click and keyboard event listeners are assigned.

Example

Here’s a very barebones example of the new swipe functionality in practice. Click the button to toggle the menu and then swipe right on the menu whilst on a touch device (or your preferred browser inspector) to close it. Simple.

See the Pen #9) Adding swipe support to our reusable function by Luke Harrison (@lukedidit) on CodePen.

Function Refinements

Finally, we’ll be looking into ways we can refine our reusable function to make it more efficient and easier to use.

Targeting the trigger element

Say for example I have an element named c-btn which on click would need to toggle aria-pressed on itself. With our reusable function as it stands, the HTML would look something like this:

<button class="c-btn" data-state="aria-pressed" data-state-element="c-btn" aria-pressed="false">

The problem here is that on click, aria-pressed would be toggled on all instances of c-btn everywhere, which isn’t the behaviour we’re looking for.

This was the problem which data-state-scope was created to resolve. By scoping our data-state instance to the nearest c-btn (which in this case would be itself) then we are creating the desired toggle behaviour.

<button class="c-btn" data-state="aria-pressed" data-state-element="c-btn" data-class-scope="c-btn" aria-pressed="false">

Whilst the above snippet works fine, it’s a bit jarring having all these attributes all referencing the same c-btn element. Ideally if data-state-element and data-state-scope aren’t defined, then the function should default to the element triggering it. This would allow easy targeting of our trigger element. Like so:

<button class="c-btn" data-state="aria-pressed" aria-pressed="false">

Implementation

data-scope-element is currently a required attribute. If it isn’t present, the function will not be able to assign any event listeners. This is because in our reusable function as it stands, the initial scan of the document is looking for elements with both the data-scope and data-scope-element attributes:

// Grab all elements with required attributes
var elems = document.querySelectorAll("[data-state][data-state-element]");

We need to tweak this so we’re only looking for elements with data-state, as data-state-element will shortly be relegated to an optional attribute.

// Grab all elements with required attributes
var elems = document.querySelectorAll("[data-state]");

In addition, we need to add an if-statement to processChange() which wraps around the retrieval of the data-state-element value, as if it’s not present, the function will throw an error when trying to call getAttribute() on something which doesn’t exist.

// Grab data-state-element list and convert to array
if(elem.getAttribute("data-state-element")) {
  var dataStateElement = elem.getAttribute("data-state-element");
  dataStateElement = dataStateElement.split(", ");
}

Next, let’s implement the logic which makes data-state-element and data-state-scope default to the trigger element if they are not explicitly defined. We can build on our previous amends to processChange() and add an else block to our data-state-element check to manually declare our target element and scope.

// Grab data-state-element list and convert to array
// If data-state-element isn't found, pass self, set scope to self if none is present, essentially replicating "this"
if(elem.getAttribute("data-state-element")) {
  var dataStateElement = elem.getAttribute("data-state-element");
  dataStateElement = dataStateElement.split(", ");
}
else {
  var dataStateElement = [];
  dataStateElement.push(elem.classList[0]);
  if(!dataStateScope) {
    var dataStateScope = dataStateElement;
  }
}

Another consequence of making data-state-element no longer required is that in processChange(), it’s length is used in the for loop to make sure all the elements defined in data-state-element receive their changes of state. This is the loop as it stands:

// Loop through all our dataStateElement items
for(var b = 0; b < dataStateElement.length; b++) {
  [...]
}

Thankfully, all we need to do here is swap out our now optional data-state-element attribute for our still required data-state element as the base for this loop.

// Loop through all our dataStateElement items
for(var b = 0; b < dataState.length; b++) {
  [...]
}

This is because in instances where multiple values have been passed to each data-state attribute (For example: <div data-state="is-active, is-disabled" data-state-element="my-elem, my-elem-2" data-state-behaviour="add, remove">) the length of the array which is derived from each of these is always going to match, so we’re always going to get the same amount of loops in the for block.

Simplifying repeated values

Another improvement which we could make relates to assigning similar types of logic in a single data-state use. Consider the below example:

<a data-state="my-state, my-state-2, my-state-3" data-state-element="c-btn, c-btn, c-btn" data-state-behaviour="remove, remove, remove" data-state-scope="o-mycomponent, o-mycomponent, o-mycomponent">

Whilst this a legitimate use of our reusable function, one thing you will notice is that we have a lot of repeated values in many of the data-state attributes. Ideally, if we want to assign similar types of logic many times over, we should be able to just write a value once and have the function interpret that as repeat values.

For example, the below HTML snippet should perform the same action as the one above.

<a data-state="my-state, my-state-2, my-state-3" data-state-element="c-btn" data-state-behaviour="remove" data-state-scope="o-mycomponent">

Here is another example of what should be considered a valid data-state use:

<a data-state="aria-expanded" data-state-element="c-menu, c-other-menu, c-final-menu" data-state-behaviour="remove, add">

Implementation

The first thing we need to consider is the for loop in processChange() which we last amended in the previous section. As it uses the length of data-state as a base for it’s loop amount, implementing these changes would expose a bug in scenarios where we have one class being applied to many elements.

Consider the following:

<a data-state="my-state" data-state-element="c-btn, c-btn-2" data-state-behaviour="remove" data-state-scope="o-mycomponent">

What would happen here is because data-state only has a single value, the for loop in processChange() would only loop a single time, meaning our intended logic for c-btn-2 would never be assigned.

To fix this, we need to compare data-state and data-state-element. Whichever has the most values then becomes the base for our loop. Like so:

// Find out which has the biggest length between states and elements and use that length as loop number
// This is to make sure situations where we have one data-state-element value and many data-state values are correctly setup
var dataLength = Math.max(dataStateElement.length, dataState.length);

// Loop
for(var b = 0; b < dataLength; b++) {
  [...]
}

As for the rest of the implementation, it’s now a matter of adding logic in the for loop for each attribute which says “If a matching value can’t be found, use the last valid one”.

Let’s use data-state value as an example. Currently, the code in the for loop which grabs the state value looks like:

// Grab state we will add
var elemState = dataState[b];

The problem now is if we have 3 data-state-element values, but only 1 data-state value, on loops 2 and 3 elemState would be undefined.

What we need to do is only redefine elemState if we have a value to give it. Like so:

// Grab state we will add
// If one isn't found, keep last valid one
if(dataState[b] !== undefined) {
  var elemState = dataState[b];
}

This would ensure elemState would always have a value, including inheriting any previous values if one can’t initially be found.

Example

Here’s a final example showing all of our function refinements:

See the Pen #10) Allow easier targeting of self & general improvements by Luke Harrison (@lukedidit) on CodePen.

Closing

In this article, we’ve covered how to build upon the reusable function created in Part 1 to make it more accessible and easier to use.

In addition, we’ve also added swipe support for trigger elements and have made sure any data-state elements added after the initial render of the DOM are no longer ignored.

As before, any comments or constructive feedback are welcome. I’ll leave you will the full reusable function which we’ve developed over the last 2 articles:

(function(){

  // SWIPE DETECT HELPER
  //----------------------------------------------

  var swipeDetect = function(el, callback){ 
    var touchsurface = el,
    swipedir,
    startX,
    startY,
    dist,
    distX,
    distY,
    threshold = 100, //required min distance traveled to be considered swipe
    restraint = 100, // maximum distance allowed at the same time in perpendicular direction
    allowedTime = 300, // maximum time allowed to travel that distance
    elapsedTime,
    startTime,
    eventObj,
    handleswipe = callback || function(swipedir, eventObj){}

    touchsurface.addEventListener('touchstart', function(e){
      var touchobj = e.changedTouches[0]
      swipedir = 'none'
      dist = 0
      startX = touchobj.pageX
      startY = touchobj.pageY
      startTime = new Date().getTime() // record time when finger first makes contact with surface
      eventObj = e;
    }, false)

    touchsurface.addEventListener('touchend', function(e){
      var touchobj = e.changedTouches[0]
      distX = touchobj.pageX - startX // get horizontal dist traveled by finger while in contact with surface
      distY = touchobj.pageY - startY // get vertical dist traveled by finger while in contact with surface
      elapsedTime = new Date().getTime() - startTime // get time elapsed
      if (elapsedTime <= allowedTime){ // first condition for awipe met
        if (Math.abs(distX) >= threshold && Math.abs(distY) <= restraint){ // 2nd condition for horizontal swipe met
          swipedir = (distX < 0)? 'left' : 'right' // if dist traveled is negative, it indicates left swipe
        }
        else if (Math.abs(distY) >= threshold && Math.abs(distX) <= restraint){ // 2nd condition for vertical swipe met
          swipedir = (distY < 0)? 'up' : 'down' // if dist traveled is negative, it indicates up swipe
        }
      }
      handleswipe(swipedir, eventObj)
    }, false)
  }


  // CLOSEST PARENT HELPER FUNCTION
  //----------------------------------------------

  closestParent = function(child, match) {
    if (!child || child == document) {
      return null;
    }
    if (child.classList.contains(match) || child.nodeName.toLowerCase() == match) {
      return child;
    }
    else {
      return closestParent(child.parentNode, match);
    }
  }


  // REUSABLE FUNCTION
  //----------------------------------------------

  // Change function
  processChange = function(elem){

    // Grab data-state list and convert to array
    var dataState = elem.getAttribute("data-state");
    dataState = dataState.split(", ");

    // Grab data-state-behaviour list if present and convert to array
    if(elem.getAttribute("data-state-behaviour")) {
      var dataStateBehaviour = elem.getAttribute("data-state-behaviour");
      dataStateBehaviour = dataStateBehaviour.split(", ");
    }

    // Grab data-scope list if present and convert to array
    if(elem.getAttribute("data-state-scope")) {
      var dataStateScope = elem.getAttribute("data-state-scope");
      dataStateScope = dataStateScope.split(", ");
    }

    // Grab data-state-element list and convert to array
    // If data-state-element isn't found, pass self, set scope to self if none is present, essentially replicating "this"
    if(elem.getAttribute("data-state-element")) {
      var dataStateElement = elem.getAttribute("data-state-element");
      dataStateElement = dataStateElement.split(", ");
    }
    else {
      var dataStateElement = [];
      dataStateElement.push(elem.classList[0]);
      if(!dataStateScope) {
        var dataStateScope = dataStateElement;
      }
    }

    // Find out which has the biggest length between states and elements and use that length as loop number
    // This is to make sure situations where we have one data-state-element value and many data-state values are correctly setup
    var dataLength = Math.max(dataStateElement.length, dataState.length);

    // Loop
    for(var b = 0; b < dataLength; b++) {

      // If a data-state-element value isn't found, use last valid one
      if(dataStateElement[b] !== undefined) {
        var dataStateElementValue = dataStateElement[b];
      } 

      // If scope isn't found, use last valid one
      if(dataStateScope && dataStateScope[b] !== undefined) {
        var cachedScope = dataStateScope[b];
      }
      else if(cachedScope) {
        dataStateScope[b] = cachedScope;
      }

      // Grab elem references, apply scope if found
      if(dataStateScope && dataStateScope[b] !== "false") {

        // Grab parent
        var elemParent = closestParent(elem, dataStateScope[b]);

        // Grab all matching child elements of parent
        var elemRef = elemParent.querySelectorAll("." + dataStateElementValue);

        // Convert to array
        elemRef = Array.prototype.slice.call(elemRef);

        // Add parent if it matches the data-state-element and fits within scope
        if(elemParent.classList.contains(dataStateElementValue)) {
          elemRef.unshift(elemParent);
        }
      }
      else {
        var elemRef = document.querySelectorAll("." + dataStateElementValue);
      }
      // Grab state we will add
      // If one isn't found, keep last valid one
      if(dataState[b] !== undefined) {
        var elemState = dataState[b];
      }   
      // Grab behaviour if any exists
      // If one isn't found, keep last valid one
      if(dataStateBehaviour) {
        if(dataStateBehaviour[b] !== undefined) {
          var elemBehaviour = dataStateBehaviour[b];
        }
      }
      // Do
      for(var c = 0; c < elemRef.length; c++) {
        // Find out if we're manipulating aria-attributes or classes
        var toggleAttr;
        if(elemRef[c].getAttribute(elemState)) {
          toggleAttr = true;
        }
        else {
          toggleAttr = false;
        }
        if(elemBehaviour === "add") {
          if(toggleAttr) {
            elemRef[c].setAttribute(elemState, true);
          }
          else {
            elemRef[c].classList.add(elemState);
          }
        }
        else if(elemBehaviour === "remove") {
          if(toggleAttr) {
            elemRef[c].setAttribute(elemState, false);
          }
          else {
            elemRef[c].classList.remove(elemState);
          }
        }
        else {
          if(toggleAttr) {
            if(elemRef[c].getAttribute(elemState) === "true") {
              elemRef[c].setAttribute(elemState, false);
            }
            else {
              elemRef[c].setAttribute(elemState, true);
            }
          }
          else {
            elemRef[c].classList.toggle(elemState);
          }
        }
      }

    }

  },
    // Init function
    initDataState = function(elem){
    // Detect data-swipe attribute before we do anything, as its optional
    // If not present, assign click event like before
    if(elem.getAttribute("data-state-swipe")){
      // Grab swipe specific data from data-state-swipe
      var elemSwipe = elem.getAttribute("data-state-swipe"),
          elemSwipe = elemSwipe.split(", "),
          direction = elemSwipe[0],
          elemSwipeBool = elemSwipe[1],
          currentElem = elem;

      // If the behaviour flag is set to "false", or not set at all, then assign our click event
      if(elemSwipeBool === "false" || !elemSwipeBool) {
        // Assign click event
        elem.addEventListener("click", function(e){
          // Prevent default action of element
          e.preventDefault(); 
          // Run state function
          processChange(this);
        });
      }
      // Use our swipeDetect helper function to determine if the swipe direction matches our desired direction
      swipeDetect(elem, function(swipedir){
        if(swipedir === direction) {
          // Run state function
          processChange(currentElem);
        }
      })
    }
    else {
      // Assign click event
      elem.addEventListener("click", function(e){
        // Prevent default action of element
        e.preventDefault(); 
        // Run state function
        processChange(this);
      });
    }
    // Add keyboard event for enter key to mimic anchor functionality
    elem.addEventListener("keypress", function(e){
      if(e.which === 13) {
        // Prevent default action of element
        e.preventDefault();
        // Run state function
        processChange(this);
      }
    });
  };

  // Run when DOM has finished loading
  document.addEventListener("DOMContentLoaded", function() {

    // Grab all elements with required attributes
    var elems = document.querySelectorAll("[data-state]");

    // Loop through our matches and add click events
    for(var a = 0; a < elems.length; a++){
      initDataState(elems[a]);
    }

    // Setup mutation observer to track changes for matching elements added after initial DOM render
    var observer = new MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
        for(var d = 0; d < mutation.addedNodes.length; d++) {
          // Check if we're dealing with an element node
          if(typeof mutation.addedNodes[d].getAttribute === 'function') {
            if(mutation.addedNodes[d].getAttribute("data-state")) {
              initDataState(mutation.addedNodes[d]);
            }
          }
        }
      });    
    });

    // Define type of change our observer will watch out for
    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  });
}());

Article Series:

  1. Original article
  2. Managing State in CSS with Reusable JavaScript Functions (You are here!)

Managing State in CSS with Reusable JavaScript Functions – Part 2 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/managing-state-css-reusable-javascript-functions-part-2/feed/ 9 255301
(To use or not-to-use) ARIA Tabs https://css-tricks.com/use-not-use-aria-tabs/ Tue, 17 May 2016 13:51:40 +0000 http://css-tricks.com/?p=241681 Looks like a Papa Bear, Mama Bear, Baby Bear situation.

To Shared LinkPermalink on CSS-Tricks


(To use or not-to-use) ARIA Tabs originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Looks like a Papa Bear, Mama Bear, Baby Bear situation.

To Shared LinkPermalink on CSS-Tricks


(To use or not-to-use) ARIA Tabs originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
241681