# Buttons that work only by touch on touchscreen devices

Source: https://easeweb.dev/learn/touch-detection
Topics: Buttons and links > Touch, mouse and keyboard can be switched at any time (WCAG 2.5.6)
The fix: Listen for click on every device, and delete the touch test that chose touchend instead. A tap, a mouse click, Enter and Space on a focused button, a screen reader and voice control all fire click, so one listener serves every way of pressing it.
Test with: touch, mouse, keyboard, speech input, screen reader
References: https://www.w3.org/WAI/WCAG22/Understanding/concurrent-input-mechanisms.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F98 https://w3c.github.io/touch-events/#mouse-events https://developer.chrome.com/blog/300ms-tap-delay-gone-away https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/accessibility/ax_object.cc https://searchfox.org/firefox-main/source/accessible/generic/LocalAccessible.cpp https://searchfox.org/firefox-main/source/modules/libpref/init/StaticPrefList.yaml

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

A shop's list of mugs has a Filters button above it, which opens a panel of colours. Phone browsers used to wait about 300 milliseconds after a tap before sending the click, in case a second tap was coming to zoom the page. To make taps feel instant, the shop's script checks whether the browser supports touch and, if it does, listens for `touchend` in place of `click`. On a phone, Filters opens the moment the finger lifts.

On a laptop with a touchscreen, the same check comes out true. Someone opens Filters with a tap, picks a colour with the trackpad, then clicks Filters to close the panel: nothing happens. Someone who uses the keyboard tabs to Filters and presses Enter: nothing happens. In Chrome or Edge, saying "Click Filters" to voice control does nothing either. Only a finger on the screen opens or closes it.

## Why it fails

WCAG 2.5.6 Concurrent Input Mechanisms, at Level AAA, asks that content does not restrict the input methods the platform offers, unless the restriction is essential, needed for security, or needed to respect the user's settings. A touchscreen laptop offers touch, a trackpad or mouse and a keyboard at once, and the person using it may switch between them at any point. W3C describes this pattern as failure F98: touch-only listeners chosen after detecting a touchscreen, whether through the Touch Events API, the `pointer: coarse` media query or `navigator.maxTouchPoints`. None of the exceptions applies here: nothing about a filter panel needs touch, and the test has nothing to do with security or a setting the person chose.

`'ontouchstart' in window` tells the script only that the browser supports touch events. Chrome and Edge do on a touchscreen laptop, Firefox does on Windows and Linux when it finds a touchscreen, and every browser does on a phone or a tablet, whatever else is plugged in. A mouse sends mouse events and then `click`, never `touchend`, and so do Enter and Space on a focused button. A screen reader or voice control that presses the button through the browser's accessibility interface gets mouse events and a `click` in Chrome and Edge; Firefox sends `touchstart` and `touchend` with them whenever touch events are on, so there those presses still open the panel. So in Chrome and Edge every press but a finger's reaches the page as a click that nothing listens for, and in Firefox the mouse and the keyboard do. The Touch Events specification warns against this exact test: supporting touch events "does not necessarily mean that a touchscreen is the only input mechanism", and listeners bound with "either touch or mouse/keyboard" conditional code make a site touch-exclusive. F98 notes that this commonly fails 2.1.1 Keyboard as well, on the devices where the test is true.

The delay the test works around has gone. Since 2014 in Chrome, and 2016 in Safari on iOS, browsers send the click straight after a tap on any page whose viewport meta tag sets `width=device-width`.

## Who is affected

Touchscreen laptops, 2-in-1 tablets with a keyboard cover, and phones and tablets with a Bluetooth keyboard or mouse all have a touchscreen beside another way in, so this reaches far more than phones.

## What automation and AI miss

The button has a name, a role and an `aria-expanded` state, so every markup check passes. Scans run in a desktop browser with no touchscreen, where the test is false, the page listens for `click`, and every scripted click and key press works. axe has no rule for it. The EaseWeb audit opens each page a second time with a touchscreen emulated and compares the controls: one with a `click` listener of its own on the first visit, and on the second a `touchend` or `touchstart` listener but no `click`, `pointerup` or `mouseup`, is recorded as a lead, and so is one that is shown on the first visit and hidden on the second. It stays a lead, because the click may be handled higher up the page, and listeners a framework puts on the page's root are not seen. The snippet circulates in older answers and tutorials about the tap delay, so code generated from them can carry it too.

## Before: touchend when a touchscreen is found

```html
<button type="button" class="filters-toggle" aria-expanded="false" aria-controls="filters">
  Filters
</button>
<form id="filters" class="filters" hidden>
  …
</form>
```

```js
const toggle = document.querySelector('.filters-toggle');
const filters = document.getElementById('filters');
// Answer taps at once, without the click delay.
const press = 'ontouchstart' in window ? 'touchend' : 'click';

toggle.addEventListener(press, () => {
  const open = toggle.getAttribute('aria-expanded') === 'true';
  toggle.setAttribute('aria-expanded', String(!open));
  filters.hidden = open;
});
```

- On a touchscreen laptop, or a tablet with a keyboard, `'ontouchstart' in window` is true, so the button listens for `touchend` alone.
- A mouse click never sends `touchend`, so clicking Filters does nothing.
- Enter and Space press the button with a click, so they do nothing either, and in Chrome and Edge neither do a screen reader and voice control.

## After: click on every device

```html
<button type="button" class="filters-toggle" aria-expanded="false" aria-controls="filters">
  Filters
</button>
<form id="filters" class="filters" hidden>
  …
</form>
```

```js
const toggle = document.querySelector('.filters-toggle');
const filters = document.getElementById('filters');

toggle.addEventListener('click', () => {
  const open = toggle.getAttribute('aria-expanded') === 'true';
  toggle.setAttribute('aria-expanded', String(!open));
  filters.hidden = open;
});
```

1. Delete the touch test. Nothing on the page decides any more which way of pressing counts.
2. Listen for the button's own `click`. A tap fires it as the finger lifts, and so do a mouse click, Enter and Space on the focused button, a screen reader and voice control, so the panel opens whichever the person uses, and they can switch at any time.

This fixes the Before and passes 2.5.6. The tap is no slower than before, as long as the page keeps its `<meta name="viewport" content="width=device-width">`.

## Implementation decisions

Touch events are fine as an addition. A swipe across a photo strip, or a drag on a slider, can listen for `touchstart` and `touchmove`, as long as buttons beside it do the same job on `click`, and they are shown on every device. The same mistake can be made in CSS: buttons hidden under `@media (pointer: coarse)` or `(hover: none)`, because a swipe is thought enough on touch, are gone for the mouse and keyboard on the same devices. `pointer: coarse` describes only the main pointer, so a tablet with a keyboard and trackpad attached can still match it.

Binding both `touchend` and `click`, to keep the instant tap, makes the action run twice on a tap unless `touchend` calls `preventDefault()`, and even then, a touch that starts on the button and turns into a scroll can still press it, since `touchend` fires on the element the touch began on. A press that runs on `touchstart` acts before the finger lifts, which [buttons that act on press and can't be cancelled](/learn/pointer-cancellation) covers. `pointerup` is no shortcut either: it never fires for the keyboard. Use `click`, or a component library's press event that fires on release and from the keyboard, and keep any check for `isTouch` or `isMobile` out of the code that wires buttons.

## Verify the fix

1. Search the code for `ontouchstart`, `maxTouchPoints`, `TouchEvent`, `pointer: coarse`, `hover: none` and any `isTouch` or `isMobile` flag. None of them chooses which event a control listens for, or hides a control.
2. In Chrome's device toolbar, choose a touch device and reload, so the page finds a touchscreen. Click Filters, which the toolbar sends as a tap, then Tab to it and press Enter and Space. The panel opens and closes each time.
3. On a touchscreen laptop, or a tablet with a keyboard and mouse attached, open Filters with a tap, close it with the mouse and open it again with the keyboard, in any order.
4. On the same device, in Chrome or Edge, say "Click Filters" to voice control, and press Enter on it with a screen reader. The panel opens, and the button is announced as expanded. Firefox sends touch events with these presses, so testing there alone can miss the failure.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the shared button or menu scripts change.

## Limits

This is a starting pattern, not a guarantee for every application. It covers listeners chosen by a touch test. A carousel whose arrows are hidden on touch devices leaves a swipe as the only way, which [pinch, drag and tilt gestures with no button](/learn/gestures) covers. Menus that open on hover once a mouse is found, and on-screen keyboards a page forces in place of the real one, can fail the same criterion and need their own checks. Some restrictions are allowed: a touch-typing tutor may insist on the keyboard, because that is what it teaches. Test on the touchscreen laptops and tablets your users actually have.

---

Get this guide in your coding agent: MCP https://easeweb.dev/mcp (setup at https://easeweb.dev/services/developers)
