What are the ARIA roles & accessible names for HTML Elements?

testing-library/jsdom and testing-library/react expect you to find elements on your page using two things:

  1. An ARIA role
  2. An ARIA accessible name, which can and should be a human-readable piece of text such as Your Name, not an html id like your-name.

This pushes you to use ARIA. But it is said that no ARIA is better than bad ARIA, and in any case it's all very confusing unless you learn a minimum about how HTML Elements get their roles and accessible names.

A Passing Test

Copy and paste this into a react project to see the role and accessible names for a small form. (See testing-library's intro example for how to do it in vanilla html)


import React from 'react';
import {fireEvent, render, screen} from '@testing-library/react';

test('Example role and accessible names', ()=>{

  render(<form data-testid='form1'>
    <fieldset>
      <legend>Form Alpha</legend>
      <label>Search : <input type='search' value="something" /></label>
      <label><input type='checkbox' /> In Stock Only</label>
      <button>The Go Button</button>
    </fieldset>
  </form>)

  const fieldSet= screen.getByRole('group',{name:"Form Alpha"})
  expect(fieldSet).toBeVisible()

  const searchBox=screen.getByRole('searchbox', {name:/Search/i})
  expect(searchBox).toHaveValue('something')

  const checkBox=screen.getByRole('checkbox', {name:/in stock only/i})
  expect(checkBox).not.toBeChecked()
  fireEvent.click(checkBox)
  expect(checkBox).toBeChecked()

  const form=screen.getByTestId('form1')
  expect(form).toBeVisible()
})

Some of this you could have guessed and some of it you couldn't. Here is my take on the minimum you want to know to to take out most of the guesswork.

What Role and what Accessible Name do my HTML elements have?

HTML element roles

You can get a quick idea of roles just by looking at some of the defined ARIA roles:

  • article, banner, button, cell, checkbox, columnheader, combobox, command, comment, complementary, composite, contentinfo, definition, dialog, document, figure, form, generic, grid, gridcell, group, heading, img, input, landmark, link, list, listbox, listitem, log, main, mark, marquee, math, menu, menubar, menuitem, menuitemcheckbox, menuitemradio, meter, navigation, option, presentation, progressbar, radio, radiogroup, range, region, roletype, row, rowgroup, rowheader, scrollbar, search, searchbox, section, sectionhead, select, separator, slider, spinbutton, status, structure, suggestion, switch, tab, table, tablist, tabpanel, term, textbox, timer, toolbar, tooltip, tree, treegrid, treeitem, widget, window

Most of them roughly split into document structure & landmark roles vs widget roles, and you can probably guess a lot of which html elements implicitly have which roles by default, but some you would guess wrong.
Here is a table of the implicit ARIA role given by default to some commonly used html elements, along with the permitted overrides.

Implicit ARIA Roles for some common HTML elements

HTML Element Implicit ARIA Role Permissible explicit Roles
nav navigation none
article article. document, main, presentation &c.
a href="..." link button,checkbox,menuitem &c.
a without href none any
button button tab, checkbox, combobox, link, menuitem, &c
form form but only if it has an accessible name search, none or presentation
fieldset group radiogroup, presentation, none
img alt="..." img button, checkbox, link, menuitem, option, tab &c
img alt="" presentation presentation, none
input[type=button] button checkbox, combobox, link, &c
input[type=checkbox] checkbox button with aria-pressed, &c
input[type=image] button link, menuitem, radio, switch &c
input[type=number] spinbutton
input[type=radio] radio menuitemradio
input[type=range] slider
input[type=reset] button
input[type=submit] button
input[type=search] with no list attribute searchbox none
input[type=search] with list attribute combobox none
input[type=email] with no list attribute textbox none
input[type=email] with list attribute combobox none
input[type=tel] with no list attribute textbox none
input[type=tel] with list attribute combobox none
input[type=text] with no list attribute textbox combobox, searchbox, &c
input[type=text] with list attribute combobox none
input[type=url] with no list attribute textbox none
input[type=url] with list attribute combobox none
ul,ol list directory, group, listbox, menu, menubar, &c
label,legend none none
main main none
menu list directory, group, listbox, menu, menubar, &c
optgroup group none
option option none
select with multiple attribute & no size greater than 1 combobox menu
select otherwise listbox none
textbox textbox none
table table any
td cell, if a descendant of a table any
th columnheader or rowheader any

You can see some patterns:

  • Many elements such as main, article, option, textbox have a fixed semantic meaning which is pretty much the same as their role. They can only have that fixed role and it can't be overridden.
  • Some elements have no implicit Aria role but can be given any explicit role. These elements include div,span,code,blockquote,most of text elements such as em,strong, &c.
  • Invisible elements such as style and script have no aria role and are not permitted an explicit role either.
  • Label and legend are in a sense special elements whose aria job is to label some other element with an accessible name. They can't be given any other role.
  • Some element roles including a,img,select (and of course input) depend on how their attributes are set.
  • See the HTML element reference pages to find the full list of html elements and the full list of permitted explicit roles for those that aren't in the above table.

The table mentions what explicit roles can be given. An explicit role is given to an HTML element by giving it a role="rolename" attribute. But the advice is to only do this when you have to. Work with the native HTML roles.

HTML Element Accessible Names

An accessible name can and should be a human-readable piece of text such as 'Your Name', not an html id like your-name.

To understand what the accessible name of a component is, you must understand the accessible name computation. Here are, roughly, the first 3 steps of the accessible name computation for HTML. This is enough to get you off the ground and calculate most of your accessible names.

The Accessible Name Computation for HTML Elements

  1. The aria-labelledby property is used if present.
  2. If the name is still empty, the aria-label property is used if present.
  3. If the name is still empty, then the 3rd step depends on the element and is described in the w3.org Accessible Name Computation Section By HTML Element. Here is a summary table:
HTML Element Accessible Name Calculation if the element has no aria-label or aria-labelledby
<input type="text">, <input type="password">, <input type="number">, <input type="search">, <input type="tel">, <input type="email">, <input type="url"> and <textarea> Use the associated label element or elements accessible name(s) - if more than one label is associated; concatenate by DOM order, delimited by spaces.
If the accessible name is still empty, then: use the control's title attribute.
Use the control's placeholder value.
<input type="button">, <input type="submit"> and <input type="reset"> Accessible Name Computation Use the associated label element(s) accessible name(s) - if more than one label is associated; concatenate by DOM order, delimited by spaces.
Use the value attribute.
For input type=submit and type=reset: if the prior steps do not yield a usable text string, and the value attribute is unspecified use the implementation defined string respective to the input type.
Otherwise, if the control still has no accessible name use title attribute.
<input type="image"> Accessible Name Computation Use the associated label element(s) accessible name(s) - if more than one label is associated; concatenate by DOM order, delimited by spaces.
Use alt attribute if present and its value is not the empty string.
Use title attribute if present and its value is not the empty string.
Otherwise if the previous steps do not yield a usable text string, use the implementation defined string respective to the input type (an input in the image state represents a submit button). For instance, a localized string of the word "submit" or the words "Submit Query".
<button> Use the associated label element(s) accessible name(s) - if more than one label is associated; concatenate by DOM order, delimited by spaces.
Use the button element subtree.
Use title attribute.
<fieldset> If the accessible name is still empty, then: if the fieldset element has a child that is a legend element, then use the subtree of the first such element.
If the accessible name is still empty, then:, if the fieldset element has a title attribute, then use that attribute.
Otherwise, there is no accessible name.
<output> Use the associated label element or elements accessible name(s) - if more than one label is associated; concatenate by DOM order, delimited by spaces.
Use title attribute.
Other Form Elements Use label element.
Use title attribute.
<summary> If the first summary element, which is a direct child of the details element, has an aria-label or an aria-labelledby attribute the accessible name is to be calculated using the algorithm defined in Accessible Name and Description: Computation and API Mappings.
Use summary element subtree.
Use title attribute.
If there is no summary element as a direct child of the details element, the user agent should provide one with a subtree containing a localized string of the word "details".
If there is a summary element as a direct child of the details element, but none of the above yield a usable text string, there is no accessible name.
<figure> If the accessible name is still empty, then: if the figure element has a child that is a figcaption element, then use the subtree of the first such element.
If the accessible name is still empty, then: if the figure element has a title attribute, then use that attribute.
<img> Use alt attribute, even if its value is the empty string.
NOTE
An img with an alt attribute whose value is the empty string is mapped to the presentation role. It has no accessible name.
Otherwise, if there is no alt attribute use the title attribute.
<table> if the table element has a child that is a caption element, then use the subtree of the first such element.
if the table element has a title attribute, then use that attribute.
tr, td, th Use the title attribute.
<a> Use a element subtree.
Use the title attribute.
<area> Use area element's alt attribute.
Use the title attribute.
<iframe> Use the title attribute.
NOTE
The document referenced by the src of the iframe element gets its name from that document's title element, like any other document
Section and Grouping p, hr, pre, blockquote, ol, ul, li, dl, dt, dd, figure, figcaption, div, main and body, article, section, nav, aside, h1, h2, h3, h4, h5, h6, header, footer, address Use the title attribute.
Text-level Elements: abbr, b, bdi, bdo, br, cite, code, dfn, em, i, kbd, mark, q, rp, rt, ruby, s, samp, small, strong, sub and sup, time, u, var, wbr Use the title attribute.

The third step is the thing to focus on. If you learn what the accessible name is for your most commonly used html elements then you have less need of the aria-label or aria-labelledby attributes. You can see that elements fallback to the title attribute, but you are recommended to avoid this. If this table does not provide a better place to put your accessible name, then use aria-label="my accessible name" or aria-labelledby="id-of-label-element".

I said that this is roughly the computation. Rather than learning this table, you could learn which roles permit name from content, and that would get you to almost the same result.

Read more, with examples: https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions/#cardinalrulesofnaming

Overriding the default name

In almost all cases you can override the default naming scheme by given an element either an aria-label="This is my name" attribute, or an aria-labelledby="id-of-a-labelling-element-with-text-content"

Parting comments

Returning to my original example, you can see that aria is opiniated. The <form> element has no built-in accessible name, whereas the input elements do. This perhaps reflects the fact that a user is much more aware of the interactive elements themselves, not the logical structure they are held in. You can also see that although many aria roles and naming rules look like what you'd expect, the thinking behind ARIA isn't identical to the thinking behind html.

Learning to do it well

A starting point for learning how to create good accessible sites is https://www.w3.org/WAI/ARIA/apg/

Nodes-Actions-Markup : a way to work with VanillaJS in the browser

HTML + CSS + JavaScript is probably the most powerful and flexible user
interface framework ever created so it is unsurprising that people sometimes
argue against the layering of some other framework on top of it
.
What, after all, can a another framework add to what is already the most powerful framework you can ask for? Nothing really.

What might be achieved is to simplify. Html plus Css plus javascript is not
simple. React, Angular, Vue and others do, in the their own way, succeed in
simplifying working with Html-Css-js, at the cost of adding another learning
curve. But we cannot pretend that it possible to be expert in any of them
without a good grip of the underlying technology.

I suggest an alternative approach. Instead of a framework on top of html+css+js
all we really need is a pattern, or a way of working. Nodes-Actions-Markup is a
pattern for working with html+css+js in dynamic web pages. Although dynamic,
the real area of interest is content-driven web pages. Most of the web is
content. If you are aiming to write a desktop-app-in-a-browser, then a framework
is a good choice. If you are aiming to present content on the web, and then
enrich it, the frameworks feel like a backward step: they start by removing
all your content.

Example code: https://github.com/chrisfcarroll/VanillaJS-NodesActionsMarkup
Example small game: https://www.cafe-encounter.net/small-games

Nodes-Actions-Markup

You can write effective program code against html in 3 steps:

  1. Identify the Nodes in the page which code must access
  2. Identify the Actions associated with those nodes
  3. Optionally, be able to add and remove Markup from the page

Nodes

Identify your nodes of interest in the obvious way. For instance for a noughts
and crosses game (aka tic-tac-toe), you likely want to identify the entire game board
and also each of the nine squares:

const gameboardNodeId = "gameboard"
const gameboardNode = ()=> document.getElementById(gameboardNodeId)

const gameSquaresSelector = "div[role=gridcell]"
const gameSquaresNodes = ()=> gameboardNode().querySelectorAll(gameSquaresSelector)

( You might decide there is no need to use functions as I have done here, you can simply use constants:

const gameboardNodeId = "gameboard"
const gameboardNode = document.getElementById(gameboardNodeId)

One trade-off is whether you can test those 2 lines of code in a command-line test runner. The document probably won't be set up when the module defining the const is loaded. The second trade-off is that dynamically-placed nodes may still need a function call to evaluate, so perhaps the developer experience is easier if you just make everything a function call. If performance is an issue, you can memo-ise).

In a line-of-business application with form elements, the nodes you identify will be every element you wish to interact with programmatically, for instance:

const signUpFormSelector ="[role=form].signup"
const signUpFormNode = () => document.querySelector(signUpFormSelector)
const areasOfInterestSelector = ".areas-of-interest input[type=radio]"
const areasOfInterestNodes = () => signUpFormNode.querySelectorAll(areasOfInterestSelector)

How you organise and encapsulate the nodes is your key design decision, but this decision is key whatever framework or not you use for your UI. For a singleton form I might do this:

const signUpForm = {
    nodes: {
        form : signUpFormNode
        areasOfInterestNodes : areasOfInterestNodes,
    }
}

but for multiple instances of a UI element appearing on a single page, I would use a constructor function with some identifier as parameter:

const allGameboardsNodeId = "all-gameboards"
const gameboardNode = (n)=> document.getElementById(allGameboardsNodeId).querySelector(`:nth-child(${n})`)
const gameSquaresNodes = (n)=> gameboardNode(n).querySelectorAll(gameSquaresSelector)

function NoughtAndCrossesBoard(boardNumber){
    this.nodes: {
        gameSquares : () => gameSquaresNodes(boardNumber)
    }
}

or if you prefer ES6 class notation over js constructor functions:


class NoughtAndCrossesBoard {
    constructor(boardNumber){
        this.boardNumber=boardNumber
        this.nodes= {
            gameSquares : () => gameSquaresNodes(boardNumber)
        }
    }
}

Actions

Actions typically depend on Nodes which they are connected to and/or must know about; and on a model which they may update. We'll discuss models more below, when we think about how Nodes-Actions-Markup relates to MVC.

Actions are of two kinds. Event listeners commonly need one-time wire-up and then they work for the lifetime of your page because the browser makes them work. For a singleton UI element with only event listeners, a method call during page load can handle all the wire-up.


wireUpGameBoard(noughtsAndCrossesGameModel){
    const board= new NoughtAndCrossesBoard()
    for(let i=0; i < 9; i++ ){
        const node=board.nodes.gameSquares[i]
        node.addEventListener('click', function(e){
            const whoPlayed=noughtsAndCrossesGameModel.playAt(i)
            e.target.innerHTML = whoPlayed
        })
    }
}

A second kind of action is something that you might programmatically call after page load. This kind of action becomes more important as your UI grows to the point that you must construct it as multiple independent elements which may talk to each other, or if non-UI events can trigger UI changes. So this kind of action should be encapsulated together with its nodes:

function NoughtAndCrossesBoard(boardNumber, noughtsAndCrossesGameModel){
    const gameSquares = () => gameSquaresNodes(boardNumber)
    this.nodes = {
        gameSquares: gameSquares
    }
    // This action can be called from an event listener attached to a 'New Game' button external to this board.
    this.clearBoard = function(){
        for(let square of gameSquares){
            square.innerHTML= unplayedSquareHTML
        }
    }
    // These actions are the one-time setup for event listeners
    for(let i=0; i < 9; i++ ){
        const node=gameSquares[i]
        node.addEventListener('click', function(e){
            const whoPlayed=noughtsAndCrossesGameModel.playAt(i)
            e.target.innerHTML = whoPlayed
        })
    }
}
const unplayedSquareHTML=' '

As in all software, as the project grows you must plan what actions each element will expose to other elements, and how they are coupled, and how they get references to each other. Javascript modules with their import & export commands work well for modularisation, encapsulation, and defining which modules depend on knowledge of other modules. Modern browsers can use modules straight from markup:

<script src="js/NodesAndActions-game-board.js" type="module"></script>

Markup

If all your markup is static, you are done. You have Nodes, Actions and Markup working together. If some of your markup is dynamic, it must be placed in the page before Nodes and Actions can reference it.

wireUpNineSimultaneousGamesOfNoughtAndCross(){
    const container= allGameboardsNode()
    const games= []
    const boards= []
    for(let i=0; i < 9; i++){
        insertGameBoardMarkup(i, container)
        games.push( new NoughtsAndCrossesGameModel() )
        board.push( new NoughtAndCrossesBoard(i, game[i]) )
    }
}

The code to insert markup can be done in a couple of ways. Backticks let you write markup in multiline strings in a function:

function insertGameBoardMarkup(boardNumber, container){

  const templatedContent= `<section class="oxo-board-section">
      <div role="grid" class="oxo-board" aria-label="Board 0" id="board0">
        <div role="gridcell" id="board0-cell-1" aria-labelledby="board0 board0-cell-1">
          <label>top left</label> </div>
        <div role="gridcell" id="board0-cell-2"  aria-labelledby="board0 board0-cell-2">
          <label>top middle</label> </div>
        <div role="gridcell" id="board0-cell-3"  aria-labelledby="board0 board0-cell-3">
          <label>top right</label> </div>
        <div role="gridcell" id="board0-cell-4" aria-labelledby="board0 board0-cell-4">
          <label>middle left</label> </div>
        <div role="gridcell" id="board0-cell-5"  aria-labelledby="board0 board0-cell-5">
          <label>middle square</label> </div>
        <div role="gridcell" id="board0-cell-6"  aria-labelledby="board0 board0-cell-6">
          <label>middle right</label> </div>
        <div role="gridcell" id="board0-cell-7" aria-labelledby="board0 board0-cell-7">
          <label>bottom left</label> </div>
        <div role="gridcell" id="board0-cell-8"  aria-labelledby="board0 board0-cell-8">
          <label>bottom middle</label> </div>
        <div role="gridcell" id="board0-cell-9"  aria-labelledby="board0 board0-cell-9">
          <label>bottom right</label> </div>
      </div>
    </section>`
          .replaceAll('board0','board' + boardNumber)
          .replaceAll('Board 0','Board ' + boardNumber)
          .replaceAll('board 0','cells ' + boardNumber)

  container.insertAdjacentHTML("beforeend", `<section class="oxo-board-section">${templatedContent}</section>`)
  return container
}

Or you can store template markup in html template elements. With html templates you still have to do your own injection of instance-specific markup:

const gameboardTemplateId="gameboard-template"

export function insertGameBoardMarkup(boardNumber, container){
  const template=document.getElementById(gameboardTemplateId)

  const templatedContent= template.content.firstElementChild.innerHTML
          .replaceAll('board0','board' + boardNumber)
          .replaceAll('Board 0','Board ' + boardNumber)
          .replaceAll('board 0','cells ' + boardNumber)

  container.insertAdjacentHTML("beforeend", `<section class="oxo-board-section">${templatedContent}</section>`)
  return container
}

Or, you can get to grips with Web Components. That requires a little more learning to get off the ground though.

Summary

You can work effectively with html+javascript by organising your code as Nodes, which identify the key Html nodes of interest to your code, and Actions, which know about Nodes and also know about the models that your web page exposes to the user.

Nodes-Action-Markup vs Model View Controller

To understand model view controller and how it is a correct way to do a user interface you must understand it at two levels. At the top level, you must understand that the goal of MVC is to support the user-illusion that as the user uses your program they are dealing, not with pixels or HTML or such like, but with “real things” that they can think about and understand. For instance a signup form, or a table top game.

So the model is a key element for any interesting application and
Nodes-Actions-Markup relies on having models to do anything meaningful. A noughts-and-crosses game should have a game model to track game state: who's turn it is, whether the game has been won, what squares have been played. An html form is a special case. With forms, the browser itself knows about and maintains the model for you, so there is probably no need to add any kind of model class in code to represent it.

The view and the controller are how the user interacts with the model. The view and controller should be designed to sustain the user-illusion that the user “reaches through” the interface to manipulate and view the model.

Views have the responsibility of representing the model to the user, usually on-screen, in such a way that the user feels they are seeing the very thing itself. For instance, when a user sees an html signup form, they do not think “I can see the html elements on screen, but where's the form?” They think that the html elements in their browser is the form. And so they fill it in. (As developers, we are also tricked by this illusion bceause the HTML standard uses 'form' for the name of an HTML element! HTML sustains the illusion so successfully that you may have to pause for a moment to realise that an HTML form element is not what a human being thinks a form is). Similarly if the user see a grid of nine squares, they think that is the game.

The controller's responsibility depends on what version of MVC you are using. In some versions, the view is also responsible for letting the user update the model, as well as seeing the model. This pattern works well if you implement two-way data-binding: the view is bound to the model and changing one changes the other. In other versions, the controller is responsible for updating the model, and the view only reads it. This works well with a one-way dataflow approach. Whether you treat Actions as part of the controller or part of the view depends on your approach to MVC.

So Nodes-Actions-Markup helps you to build views and optionally controllers. The View that the user sees is the visible markup. The Nodes part of Nodes-Actions-Markup lets you read and write that view in code. The Actions part lets the user update the model, and lets your code keep the view in sync with the model.

What about the Observer pattern? Implementing the observer pattern in Javascript is not hard, but it's worth knowing that it is not essential to the goal of MVC. Rather, the observer pattern is of most use when you have multiple views on screen simultaneously, in which registering the views as observers of the model is a good way to guarantee they stay in sync.

Summary

In retrospect, one of the things that frameworks on top of HTML each offer us, is a way to organise how we work with HTML. You don't need a framework to organise how you work. A standard way of working will do.

Software: Which Design is Architectural?

“Not all design is architectural” is widely repeated. Non-circular answers to “which design is architectural?” are less common. Here's one:

A design or decision is architectural if it impacts the system as a Whole, not merely in part.

(from @visarch 2004, http://www.bredemeyer.com/pdf_files/WhitePapers/ArchitectureAsBusinessCompetency.PDF )

Model Binding an Array from a Form Post | Asp.Net MVC & Asp.Net Core

if you form post html form fields:

<li>
  <label for="name">Your name<span>*</span></label>
  <input type="text" id="name" name="Name" />
</li>
<li>
  <label for="phoneNumber">Telephone<span>*</span></label>
  <input type="tel" id="phoneNumber" name="PhoneNumber" />
</li>

to a Controller using the [FromForm] attribute, AspNet will modelbind the form fields to your parameters by matching the name= attribute of the input elements to Property names on the parameter class:

class MyController : Controller
{
    public IActionResult Index([FromForm]Contact contact){ ... }
}

// ----
public class Contact
{
    public string Name {get;set;}
    public string PhoneNumber {get;set;}
}

But what about model binding an array — for instance, if you have a list of question inputs, and want to store the answers in a list?

If you name each field as if it were an array element:

 <fieldset id="q1">
     <label class="question">Question 1</label>
     <ul class="form-style">
        <li>
            <input type="radio" id="c1" name="answers[0]" value="@c1"/>
            <label for="c1">@c1</label>
        </li>
        <li>
            <input type="radio" id="c2" name="answers[0]" value="@c2"/>
            <label for="c2">@c2</label>
        </li>
    </ul>
  </fieldset>
    <fieldset id="q2">
        <label class="question">Question 1?</label>
        <ul class="form-style">
            <li>
                <input type="radio" id="b1" name="answers[1]" value="@b1"/>
                <label for="b1">@b1</label>
            </li>
            <li>
                <input type="radio" id="b2" name="answers[1]" value="@b2"/>
                <label for="b2">@b2</label>
            </li>
            <li>
                <input type="radio" id="b3" name="answers[1]" value="@b3"/>
                <label for="b3">@b3</label>
            </li>
            <li>
                <input type="radio" id="b4" name="answers[1]" value="@b4"/>
                <label for="b4">@b4</label>
            </li>
        </ul>
    </fieldset>

then AspNet will bind the submitted fields named answers[0], answers[1], ... to an array Property in your class with the matching name:

public class Questions
{
    public string[] Answers {get;set;}
}