Read more

Accessibility: Making non-standard elements interactive

Michael Leimstädtner
March 05, 2024Software engineer at makandra GmbH

A common cause of non-accessible web pages are elements that were made interactive via JavaScript but cannot be focused or activated with anything but the mouse.

❌ Bad example

Illustration money motivation

Opscomplete powered by makandra brand

Save money by migrating from AWS to our fully managed hosting in Germany.

  • Trusted by over 100 customers
  • Ready to use with Ruby, Node.js, PHP
  • Proactive management by operations experts
Read more Show archive.org snapshot

Let's take a look at a common example:

<form filter>
  <input filter--query name="query" type="text">
  <span filter--reset>Clear Search</span>
</form>

The HTML above is being activated with an Unpoly compiler Show archive.org snapshot like this:

up.compiler('[filter]', function(filterForm) {
  const resetButton = filterForm.querySelector('[filter--reset]')
  const queryInput = filterForm.querySelector('[filter--query]')

  function resetQuery() {
    queryInput.value = ''
    queryInput.focus()
  }

  up.on('click', resetQuery)
})

The Clear search button has three issues:

  • It cannot be focused with the keyboard
  • It cannot be pressed with the keyboard
  • A screen reader will not announce it as a button

✔️ Good example

To make the reset button focusable with the keyboard, add the [tabindex] attribute to the element that usually receives the click event.
Also set an [role=button] attribute so screen readers will announce it as a "button" when focused:

<form filter>
  <input filter--query name="query" type="text">
  <span filter--reset tabindex="0" role="button">Clear Search</span>
</form>

Now change your Unpoly compiler Show archive.org snapshot to activate the button on keydown in addition to click:

up.compiler('[filter]', function(filterForm) {
  const resetButton = filterForm.querySelector('[filter--reset]')
  const queryInput = filterForm.querySelector('[filter--query]')

  function resetQuery(event) {
    evt.preventDefault()
    queryInput.value = ''
    queryInput.focus()
  }

  up.on('click', resetQuery)
  up.on('keydown', (evt) => {
    // ignore any other keys like tabbing or pressing shift etc.
    if (evt.code === 'Space') {
      resetQuery()
    }
  })
})

Elements that are interactive by default, such as an <a> or <button>, do not need additional code:

  • They can be focused without an additional [tabindex] attribute
  • They can be activated with the keyboard without custom JavaScript. They will emit a click event after keyboard activation.

By using a <button> we can reduce the example to this:

<form filter>
  <input filter--query name="query" type="text">
  <button type="button" filter--reset>Clear Search</button>
</form>

Shorter solution with [up-clickable]

If you cannot use an element like <button>, you may use the [up-clickable] attribute to make elements interactive. Elements with this attribute gain the following behavior:

This reduces the code to the following:

<form filter>
  <input filter--query name="query" type="text">
  <span filter--reset up-clickable>Clear Search</span>
</form>
up.compiler('[filter]', function(filterForm) {
  const resetButton = filterForm.querySelector('[filter--reset]')
  const queryInput = filterForm.querySelector('[filter--query]')

  function resetQuery(event) {
    event.preventDefault()
    queryInput.value = ''
    queryInput.focus()
  }

  up.on('up:click', resetQuery)
})
Michael Leimstädtner
March 05, 2024Software engineer at makandra GmbH
Posted by Michael Leimstädtner to makandra dev (2024-03-05 14:57)