Search code examples
htmlaccessibilitywai-aria

What aria label should I use for 'inactive' items in a list that the user can still interact with?


I have a list of data in a visualisation and I want to make it as accessible as possible. There are two lists next to each other.

The list items has two states. Multiple rows can be active or inactive. A single row could be selected.

Selecting something in one list, will show 'related' items as active, and inactive. See the simplified example below. The user has selected "A 2", which is linked to "B 1" and "B 4", so A2 is aria-selected but there's no aria-active or aria-inactive, I thought to use aria-disabled as demonstrated - BUT does this not indicate that it is not interactable? The user can still click on the disabled item to then select it.

Would it be better to do multiple aria-selected, and a single aria-current=true on the single 'selected' item? Would it be odd that if the user hasn't yet made a selection and everything is 'active', every item will be aria-selected=true?

.container {
  display: grid;
  grid-template-columns: 200px 200px
}

.list div {
  margin: 0.5rem;
  background: grey;
}

.list .selected {
  background: red;
}

.list .inactive {
  opacity: 0.3;
}
<div class="container">
  <div class="list">
    <div role="row">A 1</div>
    <div role="row" class="selected" aria-selected="true">A 2</div>
    <div role="row">A 3</div>
    <div role="row">A 4</div>
  </div>
  <div class="list">
    <div role="row">B 1</div>
    <div role="row" class="inactive" aria-disabled="true">B 2</div>
    <div role="row" class="inactive" aria-disabled="true">B 3</div>
    <div role="row">B 4</div>
  </div>
</div>

Clarifications from comments. Here is how the actual implementation looks with an item selected: enter image description here, it's quite complex so I tried to distil it down to the specific issue.

  1. Like per each list is it one-to-many that may be selected at one time? Only one item can be 'selected', like in the example. Multiple items are 'highlighted' or 'active'. When nothing is selected, they are all 'active'.
  2. How do the lists correlate to one another? How do they correlate to content they may be providing? The items have a spline in between, connecting each other that I've given them a role="presentation". There's actually a timeline inbetween, and things are only 'active' if the items in the timeline share the item in the other list?
  3. If there's content changing to reflect their relationships that may need to consider things like aria-atomic updating dependent upon aria-relevant correlations, etc? I was considering this. The items change only if you scroll, the only thing that changes otherwise is the active/highlight and selected state.

I hadn't considered an aria-label, I think that might be the best solution because the user can click and select the inactive item.


Solution

  • From a semantics and accessibility point of view, I'd consider separating out the controls from the visualization itself—at least in terms of the markup. This will let you use more semantic input elements, like <input type="radio">, which come with their own states that assistive tech understands. And it will keep selection state (which is an input element trait) separate from highlighted state (which is a visualization display trait).

    You can then tie those input elements to the visualization using the aria-controls attribute.

    To denote the state of the items in the visualization itself, instead of using ARIA roles, I would suggest just using text.

    A basic example could work something like this:

    const items = document.querySelectorAll('.item')
    document.querySelectorAll('input').forEach(input => {
      input.addEventListener('change', e => {
        let targets = e.target.dataset.controls.split(' ')
        items.forEach(item => {
          let index = targets.indexOf(item.id)
          if (-1 === index) item.dataset.state = 'inactive'
          else if (0 === index) item.dataset.state = 'selected'
          else item.dataset.state = 'highlighted'
        })
      })
    })
    body {
      display: grid;
      grid-template: auto auto / auto auto;
      grid-auto-flow: column;
    }
    form,
    figure {
      display: grid;
      grid-template: auto/auto auto 1fr;
      gap: 1ch;
    }
    fieldset {
      display: grid;
      grid-template: auto/auto auto;
    }
    input,
    label {
      cursor: pointer;
    }
    figure,
    ul {
      padding: 0;
      margin: 0;
    }
    ul {
      list-style: none;
    }
    li {
      margin: 1ch 0 0 1ch;
      padding: 1ch;
    }
    li[data-state="inactive"] {
      background: #ddd;
    }
    li[data-state="selected"] {
      background: #aaffaa;
    }
    li[data-state="highlighted"] {
      background: #aaaaff;
    }
    li[data-state="inactive"] span,
    li[data-state="highlighted"] span.selected {
      display: none;
    }
    li[data-state="selected"] span,
    li[data-state="highlighted"] span.highlighted {
      display: inline;
    }
    <h2>Controls</h2>
    <form>
      <fieldset>
        <legend>List A</legend>
        <input id="input-A1" type="radio" name="input" aria-controls="graphic" data-controls="item-A1 item-B2 item-B3" />
        <label for="input-A1">A1</label>
        <input id="input-A2" type="radio" name="input" aria-controls="graphic" data-controls="item-A2 item-B1" />
        <label for="input-A2">A2</label>
        <input id="input-A3" type="radio" name="input" aria-controls="graphic" data-controls="item-A3 item-B2 item-B3 item-B4" />
        <label for="input-A3">A3</label>
        <input id="input-A4" type="radio" name="input" aria-controls="graphic" data-controls="item-A4 item-B2" />
        <label for="input-A4">A4</label>
      </fieldset>
      <fieldset>
        <legend>List B</legend>
        <input id="input-B1" type="radio" name="input" aria-controls="graphic" data-controls="item-B1 item-A2 item-A3 item-A4" />
        <label for="input-B1">B1</label>
        <input id="input-B2" type="radio" name="input" aria-controls="graphic" data-controls="item-B2 item-A2 item-B3" />
        <label for="input-B2">B2</label>
        <input id="input-B3" type="radio" name="input" aria-controls="graphic" data-controls="item-B3 item-A1 item-A4" />
        <label for="input-B3">B3</label>
        <input id="input-B4" type="radio" name="input" aria-controls="graphic" data-controls="item-B4 item-A2" />
        <label for="input-B4">B4</label>
      </fieldset>
    </form>
    <h2>Visualization</h2>
    <figure id="graphic" aria-live="polite">
      <ul aria-label="List A">
        <li class="item" id="item-A1" data-state="inactive">A1<span class="selected">, selected</span><span class="highlighted">, highlighted</span></li>
        <li class="item" id="item-A2" data-state="inactive">A2<span class="selected">, selected</span><span class="highlighted">, highlighted</span></li>
        <li class="item" id="item-A3" data-state="inactive">A3<span class="selected">, selected</span><span class="highlighted">, highlighted</span></li>
        <li class="item" id="item-A4" data-state="inactive">A4<span class="selected">, selected</span><span class="highlighted">, highlighted</span></li>
      </ul>
      <ul aria-label="List B">
        <li class="item" id="item-B1" data-state="inactive">B1<span class="selected">, selected</span><span class="highlighted">, highlighted</span></li>
        <li class="item" id="item-B2" data-state="inactive">B2<span class="selected">, selected</span><span class="highlighted">, highlighted</span></li>
        <li class="item" id="item-B3" data-state="inactive">B3<span class="selected">, selected</span><span class="highlighted">, highlighted</span></li>
        <li class="item" id="item-B4" data-state="inactive">B4<span class="selected">, selected</span><span class="highlighted">, highlighted</span></li>
      </ul>
    </figure>

    But you probably want the visualization to be directly manipulatable by clicking on the items themselves. If so, you could keep the same structure as the first example, but visually hide the accessible controls and text, and trigger the controls whenever a user clicks on an item in the visualization.

    Again, this still has the form controls which are responsible for controlling state—they're just visually hidden:

    const items = document.querySelectorAll('.item')
    document.querySelectorAll('input').forEach(input => {
      input.addEventListener('change', e => {
        let targets = e.target.dataset.controls.split(' ')
        items.forEach(item => {
          let index = targets.indexOf(item.id)
          if (-1 === index) item.dataset.state = 'inactive'
          else if (0 === index) item.dataset.state = 'selected'
          else item.dataset.state = 'highlighted'
        })
      })
    })
    items.forEach(item => {
      item.addEventListener('click', e => {
        let inputId = 'input-' + e.target.id.split('-')[1]
        document.getElementById(inputId).click()
      })
    })
    .sr-only {
      border: 0;
      clip: rect(1px, 1px, 1px, 1px);
      -webkit-clip-path: inset(50%);
      clip-path: inset(50%);
      height: 1px;
      margin: -1px;
      overflow: hidden;
      padding: 0;
      position: absolute;
      width: 1px;
      white-space: nowrap;
    }
    figure {
      display: grid;
      grid-template: auto/auto auto 1fr;
      gap: 1ch;
    }
    figure,
    ul {
      padding: 0;
      margin: 0;
    }
    ul {
      list-style: none;
    }
    li {
      margin: 1ch 0 0 1ch;
      padding: 1ch;
      cursor: pointer;
    }
    li:hover {
      opacity: 0.6;
    }
    li[data-state="inactive"] {
      background: #ddd;
    }
    li[data-state="selected"] {
      background: #aaffaa;
    }
    li[data-state="highlighted"] {
      background: #aaaaff;
    }
    li[data-state="inactive"] span,
    li[data-state="highlighted"] span.selected {
      display: none;
    }
    li[data-state="selected"] span,
    li[data-state="highlighted"] span.highlighted {
      display: inline;
    }
    <div class="sr-only">
      <h2>Controls</h2>
      <form>
        <fieldset>
          <legend>List A</legend>
          <input id="input-A1" type="radio" name="input" aria-controls="graphic" data-controls="item-A1 item-B2 item-B3"/>
          <label for="input-A1">A1</label>
          <input id="input-A2" type="radio" name="input" aria-controls="graphic" data-controls="item-A2 item-B1"/>
          <label for="input-A2">A2</label>
          <input id="input-A3" type="radio" name="input" aria-controls="graphic" data-controls="item-A3 item-B2 item-B3 item-B4"/>
          <label for="input-A3">A3</label>
          <input id="input-A4" type="radio" name="input" aria-controls="graphic" data-controls="item-A4 item-B2"/>
          <label for="input-A4">A4</label>
        </fieldset>
        <fieldset>
          <legend>List B</legend>
          <input id="input-B1" type="radio" name="input" aria-controls="graphic" data-controls="item-B1 item-A2 item-A3 item-A4"/>
          <label for="input-B1">B1</label>
          <input id="input-B2" type="radio" name="input" aria-controls="graphic" data-controls="item-B2 item-A2 item-B3"/>
          <label for="input-B2">B2</label>
          <input id="input-B3" type="radio" name="input" aria-controls="graphic" data-controls="item-B3 item-A1 item-A4"/>
          <label for="input-B3">B3</label>
          <input id="input-B4" type="radio" name="input" aria-controls="graphic" data-controls="item-B4 item-A2"/>
          <label for="input-B4">B4</label>
        </fieldset>
      </form>
    </div>
    <h2 class="sr-only">Visualization</h2>
    <figure id="graphic" aria-live="polite">
      <ul aria-label="List A">
        <li class="item" id="item-A1" data-state="inactive">A1<span class="sr-only selected">, selected</span><span class="sr-only highlighted">, highlighted</span></li>
        <li class="item" id="item-A2" data-state="inactive">A2<span class="sr-only selected">, selected</span><span class="sr-only highlighted">, highlighted</span></li>
        <li class="item" id="item-A3" data-state="inactive">A3<span class="sr-only selected">, selected</span><span class="sr-only highlighted">, highlighted</span></li>
        <li class="item" id="item-A4" data-state="inactive">A4<span class="sr-only selected">, selected</span><span class="sr-only highlighted">, highlighted</span></li>
      </ul>
      <ul aria-label="List B">
        <li class="item" id="item-B1" data-state="inactive">B1<span class="sr-only selected">, selected</span><span class="sr-only highlighted">, highlighted</span></li>
        <li class="item" id="item-B2" data-state="inactive">B2<span class="sr-only selected">, selected</span><span class="sr-only highlighted">, highlighted</span></li>
        <li class="item" id="item-B3" data-state="inactive">B3<span class="sr-only selected">, selected</span><span class="sr-only highlighted">, highlighted</span></li>
        <li class="item" id="item-B4" data-state="inactive">B4<span class="sr-only selected">, selected</span><span class="sr-only highlighted">, highlighted</span></li>
      </ul>
    </figure>