# Chat buttons that Tab skips, and close buttons with no name

Source: https://easeweb.dev/learn/chat-launchers
Topics: Dialogs and overlays > Chat launchers and close buttons work with a keyboard and say what they are (WCAG 2.1.1, 4.1.2)
The fix: Make the launcher and the close button native buttons with names in words. Either keep the round icon and give the launcher an aria-label, or put the words "Chat with us" on the launcher beside its icon; either way, the × gets a name such as "Close chat".
Test with: keyboard, screen reader, speech input
References: https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://www.w3.org/WAI/ARIA/apg/patterns/button/ https://www.w3.org/WAI/WCAG22/Techniques/failures/F59 https://www.w3.org/WAI/WCAG22/Techniques/failures/F68

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

Birchwood Home, a furniture shop, has a round chat bubble in the bottom corner of every page. A mouse user clicks it, and a chat panel opens in its place: a heading, Chat with Birchwood, a × to close it, the greeting, and a message field. A keyboard user presses Tab down the page, through Delivery prices, Track an order and Returns, and the next Tab leaves the page for the browser's own controls. The bubble is never reached, and Enter can't open what focus never lands on. A screen reader user who opens the chat some other way and tabs to the × hears "button" and nothing else: not what it does, nor that it closes the chat.

## Why it fails

The bubble is a `div` with a click listener and an icon in it. A `div` takes no focus, so Tab goes past it, and Enter and Space do nothing to it. Success criterion 2.1.1 Keyboard asks that everything a mouse can do on the page can be done with a keyboard, and opening the chat is something only a mouse can do here.

Success criterion 4.1.2 Name, Role, Value asks that every control has a name and a role that assistive technology can read. The bubble has neither: it is not a button, and it has no words, only a drawing. The × is a real `button`, so it has the role, and Tab reaches it, but all it holds is an icon with no text, so its name is empty and a screen reader announces just "button". In a panel with a Send button beside it, there is no telling which is which without pressing them. W3C lists both as failures of 4.1.2: a `div` made into a control by a script, with no role (F59), and a control with no name (F68).

## Who is affected

The chat sits in the same corner on every page, and it is often where a site sends people who are stuck, so the ones who can't open it lose the help that was meant for them.

## What automation and AI miss

A scan sees the bubble as a `div` with a drawing in it. The listener that opens the chat is added by a script, so nothing in the markup says the `div` is a control; only its pointer cursor and its place, fixed in the corner, give it away. The × is easier to catch, because an empty button is a common check, but only while the panel is open, and most chat panels are closed when the page loads and stay closed unless something opens them. Chat widgets are often a vendor's script, sometimes drawn inside a frame from the vendor's domain, so the site's own team rarely reads the code. AI assistants asked for a chat bubble can write a clickable `div`, because it looks and works the same with a mouse.

## Before: a div launcher and an unnamed ×

```html
<div class="chat-launcher">
  <svg class="icon" viewBox="0 0 24 24">
    <use href="#icon-chat"></use>
  </svg>
</div>
<section id="chat" class="chat-panel" aria-labelledby="chat-title" hidden>
  <div class="chat-header">
    <h2 id="chat-title">Chat with Birchwood</h2>
    <button type="button" class="chat-close">
      <svg class="icon" viewBox="0 0 24 24">
        <use href="#icon-close"></use>
      </svg>
    </button>
  </div>
  <div class="chat-messages" role="log" aria-label="Messages">
    <p class="message from-us">Hi! How can we help with your order?</p>
  </div>
  <form class="chat-form">
    <label for="chat-input" class="visually-hidden">Message</label>
    <input id="chat-input" autocomplete="off">
    <button>Send</button>
  </form>
</section>
```

```js
const launcher = document.querySelector('.chat-launcher');
const panel = document.getElementById('chat');
const input = document.getElementById('chat-input');

// The panel opens in the launcher's place.
function openChat() {
  launcher.hidden = true;
  panel.hidden = false;
  input.focus();
}
function closeChat() {
  panel.hidden = true;
  launcher.hidden = false;
  launcher.focus();
}

launcher.addEventListener('click', openChat);
panel.querySelector('.chat-close').addEventListener('click', closeChat);
panel.addEventListener('keydown', (event) => {
  // An input method uses Escape to cancel a conversion; leave that key to it.
  if (event.isComposing || event.keyCode === 229) return;
  if (event.key === 'Escape') closeChat();
});
```

The click listener on the `div` works for a mouse only. The × is a `button` with nothing in it but the `svg`, which has no title, so the button has no name. Even `launcher.focus()` in `closeChat()` does nothing to a `div`, and focus falls back to the page.

## After: named buttons, the round icon kept

```html
<button type="button" class="chat-launcher" aria-label="Chat with us">
  <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
    <use href="#icon-chat"></use>
  </svg>
</button>
<section id="chat" class="chat-panel" aria-labelledby="chat-title" hidden>
  <div class="chat-header">
    <h2 id="chat-title">Chat with Birchwood</h2>
    <button type="button" class="chat-close" aria-label="Close chat">
      <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
        <use href="#icon-close"></use>
      </svg>
    </button>
  </div>
  <div class="chat-messages" role="log" aria-label="Messages">
    <p class="message from-us">Hi! How can we help with your order?</p>
  </div>
  <form class="chat-form">
    <label for="chat-input" class="visually-hidden">Message</label>
    <input id="chat-input" autocomplete="off">
    <button>Send</button>
  </form>
</section>
```

```js
const launcher = document.querySelector('.chat-launcher');
const panel = document.getElementById('chat');
const input = document.getElementById('chat-input');

// The panel opens in the launcher's place.
function openChat() {
  launcher.hidden = true;
  panel.hidden = false;
  input.focus();
}
function closeChat() {
  panel.hidden = true;
  launcher.hidden = false;
  launcher.focus();
}

launcher.addEventListener('click', openChat);
panel.querySelector('.chat-close').addEventListener('click', closeChat);
panel.addEventListener('keydown', (event) => {
  // An input method uses Escape to cancel a conversion; leave that key to it.
  if (event.isComposing || event.keyCode === 229) return;
  if (event.key === 'Escape') closeChat();
});
```

The launcher is now a `button`: Tab reaches it, Enter and Space press it, the same click listener runs, and closing the chat puts focus back on it. `aria-label` gives the launcher the name "Chat with us" and the × the name "Close chat", so they are announced as "Chat with us, button" and "Close chat, button", and `aria-hidden` keeps the icons out of what is read. The script doesn't change. The button keeps the `div`'s class, so it keeps its look; if the browser's own border or background shows through, clear them in that class. Visually hidden text inside each button names it just as well, as [icon-only buttons with no accessible name](/learn/icon-buttons) shows.

## After: a launcher with its name on it

```html
<button type="button" class="chat-launcher">
  <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
    <use href="#icon-chat"></use>
  </svg>
  Chat with us
</button>
<section id="chat" class="chat-panel" aria-labelledby="chat-title" hidden>
  <div class="chat-header">
    <h2 id="chat-title">Chat with Birchwood</h2>
    <button type="button" class="chat-close" aria-label="Close chat">
      <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
        <use href="#icon-close"></use>
      </svg>
    </button>
  </div>
  <div class="chat-messages" role="log" aria-label="Messages">
    <p class="message from-us">Hi! How can we help with your order?</p>
  </div>
  <form class="chat-form">
    <label for="chat-input" class="visually-hidden">Message</label>
    <input id="chat-input" autocomplete="off">
    <button>Send</button>
  </form>
</section>
```

```js
const launcher = document.querySelector('.chat-launcher');
const panel = document.getElementById('chat');
const input = document.getElementById('chat-input');

// The panel opens in the launcher's place.
function openChat() {
  launcher.hidden = true;
  panel.hidden = false;
  input.focus();
}
function closeChat() {
  panel.hidden = true;
  launcher.hidden = false;
  launcher.focus();
}

launcher.addEventListener('click', openChat);
panel.querySelector('.chat-close').addEventListener('click', closeChat);
panel.addEventListener('keydown', (event) => {
  // An input method uses Escape to cancel a conversion; leave that key to it.
  if (event.isComposing || event.keyCode === 229) return;
  if (event.key === 'Escape') closeChat();
});
```

The launcher becomes a pill with the words "Chat with us" beside the icon. The words are its name, so it is announced as "Chat with us, button", and a voice control user can say what they see. People who don't read a speech bubble as "chat" can read the word. The × stays an icon, named "Close chat" with `aria-label`, because a × in the corner of a panel is widely understood and there is little room in the header.

## Which option to use

Both make the launcher and the × buttons that Tab reaches, and both name them, so both pass 2.1.1 and 4.1.2. A hidden name keeps the round bubble as drawn, and takes the least space on a phone, where the launcher sits over the page. A visible label costs width, but everyone sees what the button is for: a voice control user says the words on it rather than guessing its hidden name, and a person who doesn't know the icon reads the word. Choose the visible label when the design has room for it, and the hidden name when the bubble must stay small.

## When the chat comes from a third party

Most chat widgets are a vendor's script, and the launcher and panel are often drawn inside a frame from the vendor's domain, where your own CSS and scripts can't reach. What you can do:

- Test the widget before you choose it, with the steps under "Verify the fix", on the vendor's own demo page or a test page of yours.
- Check the widget's settings. Some let you show a text label on the launcher, and some let you set the names of its buttons; set both, in each language your site uses.
- Ask the vendor whether the launcher and the close button are native buttons with names, and which screen readers they tested with. Ask too for the frame's title, which is the subject of [iframes with a missing or generic title](/learn/frame-titles).
- Report what fails to the vendor, with the steps to reproduce it. Until it is fixed, list it as a known issue in your [accessibility statement](/learn/accessibility-statement), and give another way to ask for help, such as an email address, on the page where the chat is offered.

Vendor scripts change without a deploy on your side, so repeat the checks after each vendor update and each change to the widget's settings.

## Implementation decisions

**`role="button"` on the div.** Adding the role makes a screen reader say "button", but the `div` still takes no focus and ignores Enter and Space until you add `tabindex="0"` and a key handler for both. The native `button` does all of that already; [div buttons that can't be reached with Tab](/learn/native-buttons) shows why the role on its own is not a fix.

**Name it after what it does.** "Chat with us", "Help" or "Ask a question" says what happens; "Chat icon", "Bubble" or the vendor's product name doesn't. The × is "Close chat", not "×" or "Close icon". If the launcher shows a count of unread replies, put it in the name too, "Chat with us, 1 new message", as [chat widgets that never announce their messages](/learn/chat-widgets) suggests for replies that arrive while the panel is closed.

**A panel that also has a minimise button.** Some panels have both a minimise and a close button. Name each after what it does ("Minimise chat", "End chat"), since two unnamed icons side by side are worse than one.

**Where the launcher sits.** A launcher pinned to the corner can cover the control that has focus, as in [floating chat buttons that cover part of keyboard focus](/learn/focus-not-covered), and a panel written after the footer is reached last, as in [floating buttons and panels that keyboard focus reaches last](/learn/floating-order). A panel that holds focus and leaves the keyboard no way out, with no close control that Tab reaches and no Escape, is a keyboard trap: see [pop-ups that can't be closed with a keyboard](/learn/closing-pop-ups).

## Verify the fix

1. Put the mouse aside and Tab down the page: the launcher is reached, with a visible focus ring, before Tab leaves the page.
2. Press Enter on it, and in another run Space: the chat opens, and focus is in the message field.
3. With a screen reader running, Tab to the launcher and to the panel's ×: they are announced as "Chat with us, button" and "Close chat, button".
4. Press Enter on the ×: the chat closes and focus is back on the launcher. Open it again and press Escape: the same happens.
5. With voice control, say "click Chat with us": the chat opens. Say "click Close chat": it closes.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the chat widget, its vendor's script or its settings change.

## Limits

This is a starting pattern for a launcher and a panel drawn by the page, not a guarantee for every widget. Whether a vendor's widget can be changed at all depends on the vendor; inside a frame from another domain, only its settings and the vendor can change it. Announcing the chat's messages is a separate failure, covered in [chat widgets that never announce their messages](/learn/chat-widgets), and a chat that is only a bot, with no way through to a person, in [help chatbots with no way to reach a person](/learn/human-help). Test the complete journey with the assistive technology your visitors use.

---

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