# Right-click, long-press and swipe actions with no button

Source: https://easeweb.dev/learn/context-menus
Topics: Drag and drop > Right-click and long-press actions are also available from a button (WCAG 2.1.1, 2.5.1)
The fix: Keep the right-click, long press or swipe as a shortcut, and put the same actions on a control every row shows: either an Actions menu button that opens the same actions and moves through them with the arrow keys, or a button for each action on the row. Making the rows focusable so Shift+F10 opens the menu is not enough on its own: nothing shows it, and on a Mac no key opens it without a screen reader.
Test with: keyboard, mouse, touch, screen reader, speech input
References: https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://www.w3.org/WAI/WCAG22/Understanding/pointer-gestures.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F54 https://developer.mozilla.org/en-US/docs/Web/API/Popover_API https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event

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 client portal lists shared documents. Right-click a file and the site's own menu opens at the pointer, in place of the browser's: Download, Make a copy, Delete. On a phone, a long press on the row opens the same menu. The narrow layout adds a second gesture: swipe a row to the left and a Delete button slides out from under it. Press Tab and focus goes past the list, straight to the help link under it. The rows can't take focus, and the menu's buttons exist only while the menu is open.

## Why it fails

The actions have no control of their own. They run only from pointer events: a right-click, a press held for half a second, or a swipe. WCAG 2.1.1 asks for every function to work from a keyboard, and failure F54 names this pattern: pointer-specific event handlers as the only way to run a function. The swipe also fails 2.5.1. It is a path-based gesture, recognised only when the pointer moves left in a mostly straight line, and 2.5.1 asks for a way to do the same with a single pointer and no path, such as a tap on a button. A right-click and a long press are single-pointer actions, so they don't fail 2.5.1 themselves; what fails is that nothing else reaches the actions.

## Who is affected

On many file lists, message threads and saved-item lists, the right-click menu is the only place to download, rename or delete. In a phone layout, where the row's buttons were dropped to save room, the swipe is often all that is left, so the same list can work on a desktop and lock people out on a phone.

## What automation and AI miss

Scanners read the markup, and nothing in it is wrong: list items need no name, and the menu's buttons are real buttons. A `contextmenu` listener leaves no attribute, and a swipe is a pointer listener like any other. Whether the actions can be reached another way only shows when someone tries with the keyboard. Asked to fix it, AI tools often add `tabindex="0"` and `role="button"` to the rows so that Shift+F10 opens the menu. The rows get focus, but nothing shows that a menu is there, Enter does nothing on a "button", on a Mac no key opens it without a screen reader, and touch users still have to know the gesture.

## Before: actions only in a right-click or long-press menu

```html
<ul class="files">
  <li data-file="invoice">Invoice 0412.pdf</li>
  <li data-file="contract">Signed contract.pdf</li>
  <li data-file="brand">Brand guide.pdf</li>
</ul>
<div class="file-menu" hidden>
  <button type="button" data-action="download">Download</button>
  <button type="button" data-action="copy">Make a copy</button>
  <button type="button" data-action="delete">Delete</button>
</div>
```

```js
list.addEventListener('contextmenu', (event) => {
  const row = event.target.closest('li');
  if (!row) return;
  event.preventDefault(); // the site's menu in place of the browser's
  openMenu(row, event.clientX, event.clientY);
});
// iOS Safari fires no contextmenu on a long press, so time the touch too
list.addEventListener('touchstart', (event) => {
  const row = event.target.closest('li');
  timer = setTimeout(() => openMenu(row, …), 500);
});
```

The menu opens only from a right-click or a held touch. The rows can't take focus, so a keyboard user can't even reach the place where Shift+F10 would fire, and nothing on screen says a menu exists.

## Before: a Delete button that only a swipe shows

```html
<li class="file" data-file="invoice">
  <span class="file-name">Invoice 0412.pdf</span>
  <button type="button" class="swipe-delete">Delete</button>
</li>
```

```css
.swipe-delete { visibility: hidden; }
.file.swiped .swipe-delete { visibility: visible; }
```

```js
// A quick swipe to the left, mostly level, shows Delete
row.addEventListener('pointerup', (event) => {
  const dx = event.clientX - start.x;
  const dy = event.clientY - start.y;
  if (dx < -40 && Math.abs(dy) < 20) row.classList.add('swiped');
});
```

The Delete button is a real button, but `visibility: hidden` keeps it out of the Tab order and away from screen readers until a swipe shows it. A swipe needs the pointer to travel a path, so a keyboard, a switch or a single tap never gets there.

## After: an Actions menu button that opens the same actions

```html
<li data-file="invoice">
  <span id="file-invoice">Invoice 0412.pdf</span>
  <button type="button" popovertarget="menu-invoice" aria-haspopup="menu" aria-describedby="file-invoice">
    Actions
  </button>
  <div id="menu-invoice" popover role="menu" aria-labelledby="file-invoice" class="file-menu">
    <button type="button" role="menuitem" tabindex="-1" data-action="download">Download</button>
    <button type="button" role="menuitem" tabindex="-1" data-action="copy">Make a copy</button>
    <button type="button" role="menuitem" tabindex="-1" data-action="delete">Delete</button>
  </div>
</li>
```

```js
// The right-click and long-press menu stays as it was, as a shortcut
for (const menu of document.querySelectorAll('.file-menu')) {
  const items = () => [...menu.querySelectorAll('[role="menuitem"]')];
  // Opening moves focus to the first action
  menu.addEventListener('toggle', (event) => {
    if (event.newState === 'open') items()[0].focus();
  });
  // Up and Down move between actions, Home and End go to the ends, Tab closes the menu
  menu.addEventListener('keydown', (event) => {
    const list = items();
    const at = list.indexOf(document.activeElement);
    const to = { ArrowDown: at + 1, ArrowUp: at - 1, Home: 0, End: list.length - 1 }[event.key];
    if (to !== undefined) {
      event.preventDefault();
      list[(to + list.length) % list.length].focus();
    } else if (event.key === 'Tab') {
      menu.hidePopover();
    }
  });
  menu.addEventListener('click', (event) => {
    const action = event.target.closest('[data-action]');
    if (!action) return;
    menu.hidePopover();
    run(action.dataset.action, menu.closest('li')); // Delete focuses the next row's Actions
  });
}
```

Every row now shows a button that opens the menu with a click, a tap, Enter or Space, and voice control users can say "Click Actions". The menu works like the browser's own context menu, which is what people reach for first: focus moves to the first action, Up and Down Arrow move through the actions, Home and End go to the first and last, Enter runs one, and Tab closes the menu and moves on. The `popover` attribute does the rest: the menu opens above the page, Escape or a click outside closes it, and focus returns to the Actions button. `aria-haspopup="menu"` tells a screen reader that the button opens a menu, and the menu takes its name from the file, so it is read as "Invoice 0412.pdf, menu" when focus enters it. The right-click and long-press menu can stay as it was, so nobody loses the shortcut. Position the menu under its button with CSS anchor positioning or a few lines of script; a popover is centred on the screen until you do.

## After: a button for each action on the row

```html
<li data-file="invoice">
  <span id="file-invoice">Invoice 0412.pdf</span>
  <button type="button" data-action="download" aria-describedby="file-invoice">Download</button>
  <button type="button" data-action="copy" aria-describedby="file-invoice">Make a copy</button>
  <button type="button" data-action="delete" aria-describedby="file-invoice">Delete</button>
</li>
```

Each action is one press away, on every row, and everyone can see what the row can do. The right-click menu and the swipe can stay as shortcuts to the same actions. This suits a list with two or three actions and room for them: with more, the buttons crowd the row, and on a narrow screen they push the file name onto several lines.

## Which option to use

Both pass on their own, for keyboard, pointer, touch and screen reader users. Buttons on the row take one press and show every action at a glance, but each row grows with every action and each button is a Tab stop, so a long list of rows becomes a long walk with Tab; choose them for two or three actions. The Actions button keeps one Tab stop per row and any number of actions in the same space, but takes two presses and hides the actions until it is opened; choose it for more actions or narrow layouts. Many lists mix the two: the action people use most as its own button, and the rest under Actions.

## Implementation decisions

Keep the gesture. A right-click or a swipe is fast for the people who use it, and the criteria ask for another way, not a replacement. Cancel the browser's menu only on the rows, not on the whole page: the browser's menu holds Copy, Open in new tab, Translate and spelling suggestions, and people rely on them.

Making the rows focusable so that Shift+F10 or the Menu key opens the custom menu is a useful extra, not a fix. Those keys fire `contextmenu` on Windows and Linux, but a Mac has no such key, nothing on screen tells anyone to try, and touch users are left where they were.

Some details change with the kind of menu:

- Plain buttons in the popover: a popover of ordinary buttons with no menu roles, reached with Tab, also passes, and is less code. But people who open something that looks like a context menu try the arrow keys first, so if you keep plain buttons, don't announce a menu: leave out `aria-haspopup="menu"` and the menu roles.
- Component libraries: a menu component that sets `role="menu"` must also move focus into the menu and between its items with the arrow keys. Check that it does before you rely on it.
- Icon buttons: an Actions button drawn as "⋯" needs an accessible name, as [icon-only buttons with no accessible name](/learn/icon-buttons) explains. Use words people would say, such as "More actions".
- Open state: browsers report a `popovertarget` button as expanded or collapsed, so it needs no `aria-expanded`. Check it in the screen readers you support.
- Menu styles: a `display` rule on the popover, such as `display: grid` or a `flex` class, beats the browser's rule that hides it, so the menu shows while closed. Set its display under `:popover-open`.
- Swipes that follow the finger: a row that slides with the pointer while it is held is a drag rather than a swipe, and 2.5.7 applies as well. The same buttons fix it; see [drag-and-drop boards that only a mouse can use](/learn/drag-and-drop).
- Selection: a checkbox on each row with a toolbar above the list that acts on the checked rows also passes, and suits lists where people act on several rows at once. It is a larger change, with a toolbar pattern of its own.

Focus needs care when an action removes a row. If focus was on that row's Delete or in its menu, move it to the same control on the next row, or the previous row when it was the last, or the list's heading when the list is empty. Otherwise it falls to the body, and a keyboard user starts again from the top of the page.

## Verify the fix

1. Put the mouse aside. Press Tab and confirm you reach a button on every row, in reading order.
2. From the keyboard, run every action that the right-click menu or the swipe offers, on more than one row. No step may need a right-click, a held press or a swipe.
3. With a screen reader, move to each action and confirm that it, or the menu it is in, says which file it is for.
4. Delete a row from the keyboard and confirm focus lands on the next row's controls, or the list's heading when the list is empty, and Tab continues from there.
5. On a touch screen, run every action with single taps only. With voice control, run one by saying the button's name.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the list component or its menu library changes.

## Limits

This is a starting pattern, not a guarantee for every application. Rows moved by dragging, long-press selection of several items, and gestures on maps and images such as pinch to zoom have their own needs, and are not covered here; for those, see [pinch, drag and tilt gestures with no button](/learn/gestures). Menu libraries differ in how they handle focus, Escape and placement: test the complete journey with your target assistive technology.
