skip to content

According to tradition, each new javascript framework is put through its paces in the implementation of a simple todo list app: an app for creating and deleting todo list entries. The first Angular.js example I ever read was a todo list. Adding and removing items from todo lists demonstrates the immediacy of the single-page application view/model relationship.

TodoMVC compares and contrasts todo app implementations of popular MV* frameworks including Vue.js, Angular.js, and Ember.js. As a developer researching technology for a new project, it enables you to find the most intuitive and ergonomic choice for your needs.

The inclusive design of a todo list interface is, however, framework agnostic. Your user doesn’t care if it’s made with Backbone or React; they just need the end product to be accessible and easy to use. Unfortunately, each of the identical implementations in TodoMVC have some shortcomings. Most notably, the delete functionality only appears on hover, making it an entirely inaccessible feature by keyboard.

In this article, I’ll be building an integrated todo list component from the ground up. But what you learn doesn’t have to apply just to todo lists — we’re really exploring how to make the basic creation and deletion of content inclusive.


Unlike the simple, single element toggle buttons of the previous article, managed lists have a few moving parts. This is what we’re going to make:

A bold heading reading “My Todo List” introduces three todo list items on separate lines. These each have a check box to their left and a bin icon to their right. Underneath them is a text field with a placeholder of “E.g. Adopt an owl” and an Add button. The first todo list item’s checkbox is checked and its text (pick up kids from school) is crossed out to show that it is done.

The heading

A great deal of usability is about labels. The <label> element provides labels to form fields, of course. But simple text nodes provided to buttons and links are also labels: they tell you what those elements do when you press them.

Headings too are labels, giving names to the sections (regions, areas, modules) that make up an interface. Whether you are creating a static document, like a blog post, or an interactive single-page application, each major section in the content of that page should almost certainly be introduced by a heading. Our todo list’s name, “My Todo List” in this case, should be marked up accordingly.

<h1>My Todo List</h1>

It’s a very on the nose way of demarcating an interface, but on the nose is good. We don’t want our users having to do any detective work to know what it is they’re dealing with.

Heading level

Determining the correct level for the heading is often considered a question of importance, but it’s actually a question of belonging. If our todo list is the sole content within the <main> content of the page, it should be level 1, as in the previous example. There’s nothing surrounding it, so it’s at the highest level in terms of depth.

If, instead, the todo list is provided as supplementary content, it should have a level which reflects that. For example, if the page is about planning a holiday, a “Things to pack” todo list may be provided as a supplementary tool.

In the above example, both “Bars” and “Clubs” belong to “Places to get drunk”, which belongs to “Plan for my trip”. That’s three levels of belonging, hence the <h3>s.

Even if you feel that your packing todo list is less important than establishing which bars are good to visit, it’s still on the same level in terms of belonging, so it must have the same heading level.

As well as establishing a good visual hierarchy, the structure described by logically nested sections gives screen reader users a good feel for the page. Headings are also harnessed as navigational tools by screen readers. For example, in JAWS, the 2 key will take you to the next section labeled by an <h2> heading. The generic h key will take you to the next heading of any level.

The list

I talk about the virtues of lists in Inclusive Design Patterns. Alongside headings, lists help to give pages structure. Without headings or lists, pages are featureless and monotonous, making them very difficult to unpick both visually and non-visually.

Not all lists need to be bullet lists, showing a list-style, but there should be some visual indication that the items within the list are similar or equivalent; that they belong together. Non-visually, using the <ul> or <ol> container means the list is identified when encountered and its items are enumerated. For our three-item todo list, screen readers should announce something like, “list of three items”.

A todo list is, as the name suggests, a list. Since our particular todo list component makes no assertions about priority, an unordered list is fine. Here’s the structure for a static version of our todo list (the adding, deleting, and checking functionality has not yet been added):

<section aria-labelledby="todos-label">
  <h1 id="todos-label">My Todo List</h1>
  <ul>
    <li>
      Pick up kids from school
    </li>
    <li>
      Learn Haskell
    </li>
    <li>
      Sleep
    </li>
  </ul>
</section>

Empty state

Empty states are an aspect of UI design which you neglect at your peril. Inclusive design has to take user lifecycles into consideration, and some of your most vulnerable users are new ones. To them your interface is unfamiliar and, without carefully leading them by the hand, that unfamiliarity can be off-putting.

With our heading and “add” input present it may be obvious to some how to proceed, even without example todo items or instructions present. But your interface may be less familiar and more complex than this simple todo list, so let’s add an empty state anyway — for practice.

Revealing the empty state

It’s quite possible, of course, to use our data to determine whether the empty state should be present. In Vue.js, we might use a v-if block:

<div class="empty-state" v-if="!todos.length">
  <p>Either you've done everything already or there are still things to add to your list. Add your first todo &#x2193;</p>
</div>

But all the state information we need is actually already in the DOM, meaning all we need in order to switch between showing the list and showing the empty-state is CSS.

.empty-state, ul:empty {
  display: none;
}

ul:empty + .empty-state {
  display: block;
}

This is more efficient because we don’t have to query the data or change the markup. It’s also screen reader accessible: display: none makes sure the element in question is hidden both visually and from screen reader software.

All pseudo-classes pertain to implicit states. The :empty pseudo-class means the element is in an empty state; :checked means it’s in a checked state; :first-child means it’s positioned at the start of a set. The more you leverage these, the less DOM manipulation is required to add and change state with JavaScript.

Adding a todo item

We’ve come this far without discussing the adding of todos. Let’s do that now. Beneath the list (or empty state if the list is empty) is a text input and “add” button:

A text input with 'e g adopt an owl' placeholder with an 'add' button to its right

Form or no form?

It’s quite valid in HTML to provide an <input> control outside of a <form> element. The <input> will not succeed in providing data to the server without the help of JavaScript, but that’s not a problem in an application using XHR.

But do <form> elements provide anything to users? When users of screen readers like JAWS or NVDA encounter a <form> element, they are automatically entered into a special interaction mode variously called “forms mode” or “application mode”. In this mode, some keystrokes that would otherwise be used as special shortcuts are switched off, allowing the user to interact with the form fields fully.

Fortunately, most input types — including type="text" here — trigger forms mode themselves, on focus. For instance, if I were to type h in the pictured input, it would enter “h” into the field, rather than navigating me to the nearest heading element.

The real reason we need a <form> element is because we’ll want to allow users to submit on Enter, and this only works reliably where a <form> contains the input upon which Enter is being pressed. The presence of the <form> is not just for code organization, or semantics, but affects browser behavior.

<form>
  <input type="text" placeholder="E.g. Adopt an owl">
  <button type="submit">Add</button>
</form>

(Note: Léonie Watson reports that range inputs are non-functional in Firefox + JAWS unless a <form> is employed or forms mode is entered manually, by the user.)

Labeling

Can you spot the deliberate mistake in the above code snippet? The answer is: I haven’t provided a label. Only a placeholder is provided and placeholders are intended for supplementary information only, such as the “adopt an owl” suggestion.

Placeholders are not reliable as labeling methods in assistive technologies, so another method must be provided. The question is: should that label be visible, or only accessible by screen reader?

In almost all cases, a visible label should be placed above or to the left of the input. Part of the reason for this is that placeholders disappear on focus and can be eradicated by autocomplete behavior, meaning sighted users lose their labels. Filling out information or correcting autocompleted information becomes guesswork.

An unlabeled field. The user has forgotten what they are doing and just entered 'erm'.

However, ours is a bit of a special case because the “add” label for the adjacent button is quite sufficient. Those looking at the form know what the input does thanks to the button alone.

All inputs should have labels, because screen reader users don’t know to look ahead in the source to see if the submit button they’ve yet to reach gives them any clues about the form’s purpose. But simple input/submit button pairs like this and search regions can get away without visible labels. That is, so long as the submit button’s label is sufficiently descriptive.

Three examples of the described pattern. The first, fom our todo list, uses an add label for the submit button and is fine. The second example for a search form uses a submit button with search as the label and is also acceptable. The third example has a submit button with just submit as the label and is not advised.
In addition, make sure forms with multiple fields have visible labels for each field. Otherwise the user does not know which field is for what.

There are a number of ways to provide an invisible label to the input for screen reader users. One of the simpler and least verbose is aria-label. In the following example, “Write a new todo item” is the value. It’s a bit more descriptive than just “add”, which also helps to differentiate it from the button label, avoiding confusion when focus is moved between the two elements.

<form>
  <input type="text" aria-label="Write a new todo item" placeholder="E.g. Adopt an owl">
  <button type="submit">Add</button>
</form>

Submission behavior

One of the advantages of using a <form> with a button of the submit type is that the user can submit by pressing the button directly, or by hitting Enter. Even users who do not rely exclusively on the keyboard to operate the interface may like to hit Enter because it’s quicker. What makes interaction possible for some, makes it better for others. That’s inclusion.

If the user tries to submit an invalid entry we need to stop them. By disabling the <button> until the input is valid, submission by click or by Enter is suppressed. In fact, the type="submit" button stops being focusable by keyboard. In addition to disabling the button, we provide aria-invalid="true" to the input. Screen readers will tell their users the input is invalid, letting them know they need to change it.

<form>
  <input type="text" aria-invalid="true" aria-label="Write a new todo item" placeholder="E.g. Adopt an owl">
  <button type="submit" disabled>Add</button>
</form>

Feedback

The deal with human-computer interaction is that when one party does something, the other party should respond. It’s only polite. For most users, the response on the part of the computer to adding an item is implicit: they simply see the item being added to the page. If it’s possible to animate the appearance of the new item, all the better: Some movement means its arrival is less likely to be missed.

For users who are not sighted or are not using the interface visually, nothing would seem to happen. They remain focused on the input, which offers nothing new to be announced in screen reader software. Silence.

Moving focus to another part of the page — the newly added todo, say — would cause that element to be announced. But we don’t want to move the user’s focus because they might want to forge ahead writing more todos. Instead we can use a live region.

The feedback live region

Live regions are elements that tell screen readers to announce their contents whenever those contents change. With a live region, we can make screen readers talk to their users without making those users perform any action (such as moving focus).

Basic live regions are defined by role="status" or the equivalent aria-live="polite". To maximize compatibility with different screen readers, you should use both. It may feel redundant, but it increases your audience.

<div role="status" aria-live="polite">
  <!-- add content to hear it spoken -->
</div>

On the submit event, I can simply append the feedback to the live region and it will be immediately announced to the screen reader user.

var todoName = document.querySelector('[type="text"]').value;

function addedFeedback(todoName) {
  let liveRegion = document.querySelector('[role="status"]');
  liveRegion.textContent = `${todoName} added.`;
}

// example usage
addedFeedback(todoName);

One of the simplest ways to make your web application more accessible is to wrap your status messages in a live region. Then, when they appear visually, they are also announced to screen reader users.

Adopt an owl added status message. White text on green background prefixed by a tick icon
It’s conventional to color code status messages. This “success” message is green, for example. But it’s important to not rely on color, lest you let down color blind users. Hence, a supplemental tick icon is provided.

Inclusion is all about different users getting an equivalent experience, not necessarily the same experience. Sometimes what works for one user is meaningless, redundant, or obstructive to another.

In this case, the status message is not really needed visually because the item can be seen joining the list. In fact, adding the item to the list and revealing a status message at the same time would be to pull the user’s attention in two directions. In other words: the visible appending of the item and the announcement of “[item name] added” are already equivalent.

In which case, we can hide this particular messaging system from view, with a vh (visually hidden) class.

<div role="status" aria-live="polite" class="vh">
  <!-- add content to hear it spoken -->
</div>

This utility class uses some magic to make sure the element(s) in question are not visible or have layout, but are still detected and announced in screen readers. Here’s what it looks like:

.vh {
    position: absolute !important;
    clip: rect(1px, 1px, 1px, 1px);
    padding:0 !important;
    border:0 !important;
    height: 1px !important;
    width: 1px !important;
    overflow: hidden;
}

Checking off todo items

Unlike in the previous toggle button post, this time checkboxes feel like the semantically correct way to activate and deactivate. You don’t press or switch off todo items; you check them off.

Lucky, checkboxes let us do that with ease — the behavior comes out-of-the-box. We just need to remember to label each instance. When iterating over the checkbox data, we can write unique values to each for/id pairing using a for loop’s current index and string interpolation. Here’s how it can be done in Vue.js:

<ul>
  <li v-for="(todo, index) in todos">
    <input type="checkbox" :id="`todo-${index}`" v-model="todo.done"> 
    <label :for="`todo-${index}`">{{todo.name}}</label>     
  </li>
</ul>

(Note: In this example, we imagine that each todo has a done property, hence v-model="todo.done" which automatically checks the checkbox where it evaluates as true.)

The line-through style

Making robust and accessible components is easy when you use semantic elements as they were intended. In my version, I just add a minor enhancement: a line-through style for checked items. This is applied to the <label> via the :checked state using an adjacent sibling combinator.

:checked + label {
  text-decoration: line-through;
}

Once again, I’m leveraging implicit state to affect style. No need for adding and removing class="crossed-out" or similar.

(Note: If you want to style the checkbox controls themselves, WTF Forms gives guidance on doing so without having to create custom elements. In the demo at the end of this article, I use a proxy .tick <span> to do something similar.)

Deleting todo items

Checking off and deleting todo list items are distinct actions. Because sometimes you want to see which items you’ve done, and sometimes you add todo items to your list that you didn’t mean to, or which become non-applicable.

The functionality to delete todos can be provided via a simple button. No need for any special state information — the label tells us everything we need to know. Of course, if the button uses an icon image or font glyph in place of an interpretable text node, an auxiliary label should be provided.

<button aria-label="delete">
  &times;
</button>

In fact, let’s be more specific and include the todo item’s name. It’s always better to provide labels which make sense in isolation. This unique label helps differentiate it from the others. Your JavaScript framework of choice should provide a templating facility to achieve this. In my choice, Vue.js, it looks like this:

<button :aria-label="`delete ${todo.name}`">
  &times;
</button>

In this example, &times; is used to represent a cross symbol. Were it not for the aria-label overriding it, the label would be announced as “times” or “multiplication” depending on the screen reader in question.

Always be wary of how screen readers interpret special unicode characters and symbols. Sometimes, arguably, they are quite helpful, like the down arrow in our empty state message. This will be interpreted as “down pointing arrow” or similar. Since, in our case, the text input is always below the list both visually and in source order, the arrow guides the user toward it.

The empty state, reading either you've done everything already or there are still things to add to your list. Add your first todo, down pointing arrow. The text input and add button appear below.

In our case, a dustbin icon is provided using SVG. SVG is great because it’s an image format that scales without degrading. Many kinds of users often feel the need to scale/zoom interfaces, including the short-sighted and those with motor impairments who are looking to create larger touch or click targets.

<button aria-label="delete">
  <svg>
    <use xlink:href="#bin-icon"></use>
  </svg>
</button>

Three todo list items, each with a dustbin icon to their right for deleting them.

To reduce bloat when using multiple instances of the same inline SVG icon, we employ the <use> element, which refers to a canonical version of the SVG, defined as a <symbol> at the head of the document body:

<body>
  <svg style="display: none">
    <symbol id="bin-icon" viewBox="0 0 20 20">
      <path d="[path data here]">
    </symbol>
  </svg>

A bloated DOM can diminish the experience of many users since many operations will take longer. Assistive technology users especially may find their software unresponsive.

Focus management

When a user clicks the delete button for a todo item, the todo item — including the checkbox, the label, and the delete button itself — will be remove from the DOM. This raises an interesting problem: what happens to focus when you delete the currently focused element?

The first picture shows the Learn Haskell todo item's bin icon/delete button focused with a blue ring. The second picture shows that todo item removed from the list and focus is nowhere to be seen.

Unless you’re careful, the answer is something very annoying for keyboard users, including screen reader users.

The truth is, browsers don’t know where to place focus when it has been destroyed in this way. Some maintain a sort of “ghost” focus where the item used to exist, while others jump to focus the next focusable element. Some flip out completely and default to focusing the outer document — meaning keyboard users have to crawl through the DOM back to where the removed element was.

For a consistent experience between users, we need to be deliberate and focus() an appropriate element, but which one?

One option is to focus the first checkbox of the list. Not only will this announce the checkbox’s label and state, but also the total number of list items remaining: one fewer than a moment ago. All useful context.

document.querySelector('ul input').focus();

(Note: querySelector returns the first element that matches the selector. In our case: the first checkbox in the todo list.)

But what if we just deleted the last todo item in our list and had returned to the empty state? There’s no checkbox we can focus. Let’s try something else. Instead, I want to do two things:

  1. Focus the region’s “My Todo List” heading
  2. Use the live region already instated to provide some feedback

You should never make non-interactive elements like headings focusable by users because the expectation is that, if they’re focusable, they should actually do something. When I’m testing an interface and there are such elements, I would therefore fail it under WCAG 2.4.3 Focus Order.

However, sometimes you need to direct a user to a certain part of the page, via a script. In order to move a user to a heading and have it announced, you need to do two things:

  1. Provide that heading with tabindex="-1"
  2. Focus it using the focus() method in your script
<h1 tabindex="-1">My Todo List</h1>

The -1 value’s purpose is twofold: it makes elements unfocusable by users (including normally focusable elements) but makes them focusable by JavaScript. In practice, we can move a user to an inert element without it becoming a “tab stop” (an element that can be moved to via the Tab key) among focusable elements within the page.

In addition, focusing the heading will announce its text, role, level, and (in some screen readers) contextual information such as “region”. At the very least, you should hear “My Todo List, heading, level 2”. Because it is in focus, pressing tab will step the user back inside the list and onto the first checkbox. In effect, we’re saying, “now that you’ve deleted that list item, here’s the list again.”

I typically do not supply focus styles to elements which are focused programmatically in this way. Again, this is because the target element is not interactive and should not appear to be so.

[tabindex="-1"] { outline: none }

After the focused element (and with it its focus style) has been removed, the heading is focused. A keyboard user can then press Tab to find themselves on that first checkbox or — if there are no items remaining — the text input at the foot of the component.

The feedback

Arguably, we’ve provided enough information for the user and placed them in a perfect position to continue. But it’s always better to be explicit. Since we already have a live region instated, why not use that to tell them the item has been successfully removed?

function deletedFeedback(todoName) {
  let liveRegion = document.querySelector('[role="status"]');
  liveRegion.textContent = `${todoName} deleted.`;
}

// example usage
deletedFeedback(todoName);

I appreciate that you probably wouldn’t be writing this in vanilla JavaScript, but this is basically how it would work.

Now, because we’ve used role="status" (aria-live="polite"), something neat happens in supporting screen readers: “My Todo List, heading, level 2” is read first, followed by “[todo item name] deleted”.

That’s because polite live regions wait until the interface and the user have settled before making themselves known. Had I used role="alert" (aria-live="assertive"), the status message would override (or partially override) the focus-invoked heading announcement. Instead, the user knows both where they are, and that what they’ve tried to do has succeeded.

Working demo

I’ve created a codePen page to demonstrate the techniques in this post. It uses Vue.js, but could have been created with any JavaScript framework. It’s offered for testing with different screen reader and browser combinations.

Conclusion

Counting semantic structure, labeling, iconography, focus management and feedback, there’s quite a lot to consider when creating an inclusive todo list component. If that makes inclusive design seem dauntingly complex, consider the following:

  1. This is new stuff. Don’t worry, it’ll become second nature soon enough.
  2. Everything you’ve learned here is applicable to a wide range of content management components, and many other components.
  3. You only need to build a rock solid component once. Then it can live in your pattern library and be reused indefinitely.

Checklist

Back to components list