Accessibility pattern · Communication

Mentions

Typing @ opens a list of people above the box; letters after it filter the list, the arrow keys move a highlight, and Enter or Tab puts @Full Name into the text. Focus never leaves the message box. The keys are taken over only while the list is open, a polite status says how many people match, and sent mentions become links.

WCAG criteria
7
Keyboard rules
5
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

launch-team

8 members

  1. Vikram Shetty

    @Asha Rao can you check the hero image on small phones before we ship?

  2. Asha Rao

    On it. Looping in @Neha Kulkarni for the button labels.

Type @ to mention someone. Enter sends; Shift + Enter starts a new line.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
@Opens the list of people; letters typed after it filter the list, and the number found is announced when typing pauses.
Arrow Down or Arrow UpWhile the list is open, moves the highlight, wrapping at the ends. With it closed, they move the text cursor as usual.
Enter or TabWhile the list is open, inserts the highlighted person as @Full Name and a space, and closes the list.
EscapeCloses the list and keeps what is typed; it stays closed until the next @.
EnterWith the list closed, sends the message. Shift+Enter starts a new line.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches the message boxMessage #launch-team, edit text, multi line. Type @ to mention someone. Enter sends; Shift + Enter starts a new line.
Typing @, then a pauseAsha Rao Design lead, 1 of 8. 8 people
Typing "ne"Neha Kulkarni Copywriter, 1 of 1. 1 person
Enter inserts herMentioned Neha Kulkarni
Enter sends the messageMessage sent

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<section class="ap-mentions" data-ap-mentions aria-labelledby="men-name">
  <div class="ap-mentions__top">
    <span class="ap-mentions__hash" aria-hidden="true">#</span>
    <div>
      <h3 class="ap-mentions__name" id="men-name" translate="no">launch-team</h3>
      <p class="ap-mentions__sub">8 members</p>
    </div>
  </div>

  <ol class="ap-mentions__msgs" data-ap-men-msgs>
    <li class="ap-mentions__msg">
      <p class="ap-mentions__meta"><span class="ap-mentions__from">Vikram Shetty</span> <time datetime="2026-10-05T11:40+05:30">11:40</time></p>
      <p class="ap-mentions__text"><a class="ap-mentions__at" href="#mentions-asha">@Asha Rao</a> can you check the hero image on small phones before we ship?</p>
    </li>
    <li class="ap-mentions__msg">
      <p class="ap-mentions__meta"><span class="ap-mentions__from">Asha Rao</span> <time datetime="2026-10-05T11:42+05:30">11:42</time></p>
      <p class="ap-mentions__text">On it. Looping in <a class="ap-mentions__at" href="#mentions-neha">@Neha Kulkarni</a> for the button labels.</p>
    </li>
  </ol>

  <form class="ap-mentions__composer" novalidate>
    <label class="ap-mentions__label" for="men-input">Message #launch-team</label>
    <div class="ap-mentions__field">
      <textarea class="ap-input ap-mentions__input" id="men-input" name="message" rows="2" aria-autocomplete="list" aria-haspopup="listbox" aria-controls="men-list" aria-describedby="men-hint" autocomplete="off" spellcheck="true"></textarea>
      <div class="ap-mentions__popup" data-ap-men-popup hidden>
        <ul class="ap-mentions__list" id="men-list" role="listbox" aria-label="People to mention">
          <li class="ap-mentions__opt" role="option" id="men-p-asha" aria-selected="false" data-en="Asha Rao" data-id="asha"><span class="ap-mentions__face" aria-hidden="true" translate="no">AR</span><span class="ap-mentions__who">Asha Rao</span> <span class="ap-mentions__role">Design lead</span></li>
          <li class="ap-mentions__opt" role="option" id="men-p-arjun" aria-selected="false" data-en="Arjun Nair" data-id="arjun"><span class="ap-mentions__face" aria-hidden="true" translate="no">AN</span><span class="ap-mentions__who">Arjun Nair</span> <span class="ap-mentions__role">Engineering</span></li>
          <li class="ap-mentions__opt" role="option" id="men-p-vikram" aria-selected="false" data-en="Vikram Shetty" data-id="vikram"><span class="ap-mentions__face" aria-hidden="true" translate="no">VS</span><span class="ap-mentions__who">Vikram Shetty</span> <span class="ap-mentions__role">Product manager</span></li>
          <li class="ap-mentions__opt" role="option" id="men-p-neha" aria-selected="false" data-en="Neha Kulkarni" data-id="neha"><span class="ap-mentions__face" aria-hidden="true" translate="no">NK</span><span class="ap-mentions__who">Neha Kulkarni</span> <span class="ap-mentions__role">Copywriter</span></li>
          <li class="ap-mentions__opt" role="option" id="men-p-rohan" aria-selected="false" data-en="Rohan Mehta" data-id="rohan"><span class="ap-mentions__face" aria-hidden="true" translate="no">RM</span><span class="ap-mentions__who">Rohan Mehta</span> <span class="ap-mentions__role">Support</span></li>
          <li class="ap-mentions__opt" role="option" id="men-p-meera" aria-selected="false" data-en="Meera Iyer" data-id="meera"><span class="ap-mentions__face" aria-hidden="true" translate="no">MI</span><span class="ap-mentions__who">Meera Iyer</span> <span class="ap-mentions__role">Design</span></li>
          <li class="ap-mentions__opt" role="option" id="men-p-priya" aria-selected="false" data-en="Priya Sharma" data-id="priya"><span class="ap-mentions__face" aria-hidden="true" translate="no">PS</span><span class="ap-mentions__who">Priya Sharma</span> <span class="ap-mentions__role">Marketing</span></li>
          <li class="ap-mentions__opt" role="option" id="men-p-kabir" aria-selected="false" data-en="Kabir Khan" data-id="kabir"><span class="ap-mentions__face" aria-hidden="true" translate="no">KK</span><span class="ap-mentions__who">Kabir Khan</span> <span class="ap-mentions__role">Quality</span></li>
        </ul>
        <p class="ap-mentions__keys" aria-hidden="true"><kbd>↑</kbd> <kbd>↓</kbd> move <kbd>Enter</kbd> inserts <kbd>Esc</kbd> closes</p>
      </div>
    </div>
    <p class="ap-hint" id="men-hint">Type @ to mention someone. Enter sends; Shift + Enter starts a new line.</p>
    <p class="ap-error" id="men-error" hidden>
      <svg class="ap-mentions__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg>
      <span>Write a message first.</span>
    </p>
    <div class="ap-mentions__foot">
      <p class="ap-mentions__status" role="status"></p>
      <button type="submit" class="ap-btn ap-btn--primary">
        <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 12 20 4l-4 16-4-7-8-1Z"/><path d="m12 13 8-9"/></svg>
        Send
      </button>
    </div>
  </form>
</section>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    The box has a label and a hint, the suggestions are a labelled listbox tied to it with aria-controls, and sent mentions are real links.

  • 2.1.1 Keyboard Level A

    Opening, filtering, moving, inserting, closing and sending all work from the keyboard.

  • 2.1.2 No Keyboard Trap Level A

    Tab inserts only while the list is open; Escape closes it, and Tab then leaves the box as usual, so focus is never held.

  • 2.4.4 Link Purpose (In Context) Level A

    Each sent mention is a link whose text is the person's full name, so a list of links still says who it goes to.

  • 3.2.2 On Input Level A

    Typing filters and highlights but never inserts; only Enter, Tab or a click puts a name in the text.

  • 4.1.2 Name, Role, Value Level A

    The box keeps its text box role and adds aria-autocomplete, aria-haspopup, aria-controls and aria-activedescendant, so the highlighted person is exposed.

  • 4.1.3 Status Messages Level AA

    How many people match, who was mentioned and that the message went are said in a polite status, without moving focus.

Usage

When to use it

Use it

  • Comments and messages where people notify a colleague by name.
  • Any multi-line box where a few known values (people, issues, channels) are inserted inline.

Use something else

  • Choosing people as the main task, like sharing a document: a multi-select with tags shows who is chosen far better.
  • A single-line field that only ever holds one name: use a combobox.
  • Very large directories: search them on a page or in a dialog instead of a popup under the cursor.

Common failures

How it usually goes wrong

  • Moving focus into the list

    If the highlight is real focus, typing stops and the half-written message loses its place. Focus stays in the box; aria-activedescendant points at the person.

  • Arrow keys taken over all the time

    In a multi-line box the arrows move the cursor between lines. They are borrowed only while the list is open, and Escape hands them back.

  • A list that opens inside email addresses

    Every @ in [email protected] opening a popup is maddening. The list opens only for an @ at the start or after a space.

  • Silent filtering

    A list that shrinks without a word leaves screen reader users guessing whether anyone matched. A polite count follows each pause, and No one matches when nobody does.

  • Mentions sent as colored text

    A blue name that is not a link cannot be followed and reads as plain text. Sent mentions here are links named with the person's full name.

  • role=combobox put on a textarea

    ARIA in HTML does not allow it, and checkers such as axe report it. The textarea keeps its own role and gets the autocomplete attributes that a text box may carry.

Notes

Building it

  • A textarea cannot take role=combobox, and a text box cannot carry aria-expanded, so whether the list is open is not exposed as a state. The polite count, and the highlighted person, stand in for it.
  • Screen reader support for aria-activedescendant on a multi-line text box is less consistent than on a single-line combobox; keep the count and the Mentioned message, which work everywhere.
  • The list is anchored to the box rather than to the text cursor: finding the cursor's position in a textarea needs a hidden mirror element, and a fixed place is easier to find when zoomed in.
  • Mentions are plain text, @Full Name, until the message is sent, when each one becomes a link; in a real product, keep the chosen people's ids beside the text in case two share a name.
  • Filtering matches the start of any word in a name, in the visible text and in an English name in data-en, so it keeps working when the page is translated.

Sources: WAI-ARIA Authoring Practices: Combobox · ARIA in HTML: textarea

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction