Accessibility pattern · Communication

Reactions

Each reaction is a toggle button with aria-pressed, named in words with its count, while the emoji itself is hidden from screen readers so it is not read twice. Add reaction opens a grid of emoji that the arrow keys move through and typing a name jumps into; who reacted shows on hover and on keyboard focus.

WCAG criteria
7
Keyboard rules
6
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.

Meera Iyer

Design lead ·

We shipped the new checkout today. Thank you to everyone who tested it on slow networks last week.

Checkout v2: release notesDocs · 4 min read

Add a reaction

Arrow keys move. Type a name to jump.

  • Thumbs up
  • Thumbs down
  • Red heart
  • Laughing
  • Party popper
  • Clapping hands
  • Fire
  • Eyes
  • Rocket
  • Thank you
  • Tea
  • Done

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
TabMoves through the reactions, then to Add reaction; each reaction shows who reacted while it has focus.
Enter or SpaceOn a reaction, adds or removes yours and updates the count. On Add reaction, opens the picker.
Arrow Left or Arrow Right or Arrow Up or Arrow DownIn the picker, moves through the emoji grid by one, or by a row.
A–ZIn the picker, jumps to the next emoji whose name starts with what is typed.
Home or EndIn the picker, moves to the first or last emoji.
EscapeCloses the picker and returns focus to Add reaction; on a reaction, hides who reacted.

Screen readers

What it announces

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

WhenExpected announcement
Tab reaches a reactionThumbs up, 12 reactions, toggle button, not pressed. Asha Rao, Vikram Shetty and 10 others
Enter adds your reactionPressed. Thumbs up, 13 reactions, you reacted
Enter on Add reactionAdd a reaction, dialog. Emoji, list box. Thumbs up, selected, 1 of 12
Typing "ro" in the pickerRocket, selected, 9 of 12
Enter chooses itRocket, 1 reaction, you reacted (focus is back on Add reaction)

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

<article class="ap-reactions" data-ap-reactions aria-labelledby="rx-author">
  <div class="ap-reactions__top">
    <span class="ap-reactions__avatar" aria-hidden="true" translate="no">MI</span>
    <div>
      <h3 class="ap-reactions__author" id="rx-author">Meera Iyer</h3>
      <p class="ap-reactions__meta">Design lead · <time datetime="2026-10-05T10:15+05:30">2 hours ago</time></p>
    </div>
  </div>
  <p class="ap-reactions__text">We shipped the new checkout today. Thank you to everyone who tested it on slow networks last week.</p>
  <a class="ap-reactions__link" href="#reactions-notes">
    <span class="ap-reactions__doc" aria-hidden="true"><svg viewBox="0 0 24 24" focusable="false"><path d="M7 3h7l5 5v13H7Z"/><path d="M14 3v5h5"/><path d="M10 13h6M10 17h4"/></svg></span>
    <span><span class="ap-reactions__doc-name">Checkout v2: release notes</span><span class="ap-reactions__doc-meta">Docs · 4 min read</span></span>
  </a>

  <div class="ap-reactions__bar" role="group" aria-label="Reactions">
    <div class="ap-reactions__item">
      <button type="button" class="ap-reactions__chip" data-ap-rx-chip="Thumbs up" aria-pressed="false" aria-label="Thumbs up, 12 reactions" aria-describedby="rx-tip-thumbs-up">
        <span class="ap-reactions__emoji" aria-hidden="true">👍</span>
        <span class="ap-reactions__n">12</span>
      </button>
      <span class="ap-reactions__tip" role="tooltip" id="rx-tip-thumbs-up" data-off="Asha Rao, Vikram Shetty and 10 others" data-on="You, Asha Rao, Vikram Shetty and 10 others" hidden>Asha Rao, Vikram Shetty and 10 others</span>
    </div>
    <div class="ap-reactions__item">
      <button type="button" class="ap-reactions__chip" data-ap-rx-chip="Red heart" aria-pressed="true" aria-label="Red heart, 8 reactions, you reacted" aria-describedby="rx-tip-red-heart">
        <span class="ap-reactions__emoji" aria-hidden="true">❤️</span>
        <span class="ap-reactions__n">8</span>
      </button>
      <span class="ap-reactions__tip" role="tooltip" id="rx-tip-red-heart" data-off="Neha Kulkarni, Rohan Mehta and 5 others" data-on="You, Neha Kulkarni, Rohan Mehta and 5 others" hidden>You, Neha Kulkarni, Rohan Mehta and 5 others</span>
    </div>
    <div class="ap-reactions__item">
      <button type="button" class="ap-reactions__chip" data-ap-rx-chip="Party popper" aria-pressed="false" aria-label="Party popper, 5 reactions" aria-describedby="rx-tip-party-popper">
        <span class="ap-reactions__emoji" aria-hidden="true">🎉</span>
        <span class="ap-reactions__n">5</span>
      </button>
      <span class="ap-reactions__tip" role="tooltip" id="rx-tip-party-popper" data-off="Rohan Mehta, Asha Rao and 3 others" data-on="You, Rohan Mehta, Asha Rao and 3 others" hidden>Rohan Mehta, Asha Rao and 3 others</span>
    </div>
    <div class="ap-reactions__item">
      <button type="button" class="ap-reactions__chip" data-ap-rx-chip="Eyes" aria-pressed="false" aria-label="Eyes, 2 reactions" aria-describedby="rx-tip-eyes">
        <span class="ap-reactions__emoji" aria-hidden="true">👀</span>
        <span class="ap-reactions__n">2</span>
      </button>
      <span class="ap-reactions__tip" role="tooltip" id="rx-tip-eyes" data-off="Vikram Shetty and Neha Kulkarni" data-on="You, Vikram Shetty and Neha Kulkarni" hidden>Vikram Shetty and Neha Kulkarni</span>
    </div>
    <div class="ap-reactions__item">
      <button type="button" class="ap-reactions__add" data-ap-rx-add aria-haspopup="dialog" aria-expanded="false" aria-controls="rx-picker" aria-label="Add reaction">
        <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M20.5 11.5a8.5 8.5 0 1 1-8-8.5"/><path d="M9 14.5a4 4 0 0 0 6 0"/><path d="M9 9.5h.01M15 9.5h.01"/><path d="M19 2v6M16 5h6"/></svg>
      </button>
      <span class="ap-reactions__tip ap-reactions__tip--add" aria-hidden="true">Add reaction</span>
      <dialog class="ap-reactions__picker" id="rx-picker" aria-labelledby="rx-picker-name">
        <div class="ap-reactions__phead">
          <h4 class="ap-reactions__pname" id="rx-picker-name">Add a reaction</h4>
          <p class="ap-reactions__hint" id="rx-hint">Arrow keys move. Type a name to jump.</p>
        </div>
        <ul class="ap-reactions__grid" role="listbox" aria-label="Emoji" aria-describedby="rx-hint" tabindex="0" autofocus>
            <li class="ap-reactions__opt" role="option" id="rx-o-thumbs-up" aria-selected="false" data-glyph="👍" data-en="Thumbs up"><span aria-hidden="true">👍</span><span class="ap-reactions__vh">Thumbs up</span></li>
            <li class="ap-reactions__opt" role="option" id="rx-o-thumbs-down" aria-selected="false" data-glyph="👎" data-en="Thumbs down"><span aria-hidden="true">👎</span><span class="ap-reactions__vh">Thumbs down</span></li>
            <li class="ap-reactions__opt" role="option" id="rx-o-red-heart" aria-selected="false" data-glyph="❤️" data-en="Red heart"><span aria-hidden="true">❤️</span><span class="ap-reactions__vh">Red heart</span></li>
            <li class="ap-reactions__opt" role="option" id="rx-o-laughing" aria-selected="false" data-glyph="😂" data-en="Laughing"><span aria-hidden="true">😂</span><span class="ap-reactions__vh">Laughing</span></li>
            <li class="ap-reactions__opt" role="option" id="rx-o-party-popper" aria-selected="false" data-glyph="🎉" data-en="Party popper"><span aria-hidden="true">🎉</span><span class="ap-reactions__vh">Party popper</span></li>
            <li class="ap-reactions__opt" role="option" id="rx-o-clapping-hands" aria-selected="false" data-glyph="👏" data-en="Clapping hands"><span aria-hidden="true">👏</span><span class="ap-reactions__vh">Clapping hands</span></li>
            <li class="ap-reactions__opt" role="option" id="rx-o-fire" aria-selected="false" data-glyph="🔥" data-en="Fire"><span aria-hidden="true">🔥</span><span class="ap-reactions__vh">Fire</span></li>
            <li class="ap-reactions__opt" role="option" id="rx-o-eyes" aria-selected="false" data-glyph="👀" data-en="Eyes"><span aria-hidden="true">👀</span><span class="ap-reactions__vh">Eyes</span></li>
            <li class="ap-reactions__opt" role="option" id="rx-o-rocket" aria-selected="false" data-glyph="🚀" data-en="Rocket"><span aria-hidden="true">🚀</span><span class="ap-reactions__vh">Rocket</span></li>
            <li class="ap-reactions__opt" role="option" id="rx-o-thank-you" aria-selected="false" data-glyph="🙏" data-en="Thank you"><span aria-hidden="true">🙏</span><span class="ap-reactions__vh">Thank you</span></li>
            <li class="ap-reactions__opt" role="option" id="rx-o-tea" aria-selected="false" data-glyph="☕" data-en="Tea"><span aria-hidden="true">☕</span><span class="ap-reactions__vh">Tea</span></li>
            <li class="ap-reactions__opt" role="option" id="rx-o-done" aria-selected="false" data-glyph="✅" data-en="Done"><span aria-hidden="true">✅</span><span class="ap-reactions__vh">Done</span></li>
        </ul>
        <p class="ap-reactions__preview" aria-hidden="true"><span class="ap-reactions__big" data-ap-rx-big>👍</span><span data-ap-rx-name>Thumbs up</span></p>
      </dialog>
    </div>
  </div>
  <p class="ap-reactions__vh" aria-live="polite" data-ap-rx-say></p>
</article>

WCAG 2.2

What it meets

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

  • 1.1.1 Non-text Content Level A

    Each emoji is aria-hidden and its meaning is in words, so screen readers say Thumbs up, not a Unicode name, and never twice.

  • 1.4.1 Use of Color Level A

    Your reactions have a thicker border and bold count as well as a tint, and the name says you reacted.

  • 1.4.13 Content on Hover or Focus Level AA

    Who reacted shows on hover and on keyboard focus, stays while the pointer moves onto it, and Escape hides it.

  • 2.1.1 Keyboard Level A

    Toggling, opening the picker, moving through it, finding by name and choosing all work from the keyboard.

  • 2.5.3 Label in Name Level A

    Each reaction's name contains the count it shows, so voice control users can say the number they see.

  • 4.1.2 Name, Role, Value Level A

    Reactions are toggle buttons with aria-pressed; the picker is a dialog holding a listbox whose active emoji is set with aria-activedescendant.

  • 4.1.3 Status Messages Level AA

    A new count is written to a polite live region once the pressing stops, so it is read briefly and once.

Usage

When to use it

Use it

  • Quick, low-effort responses to a post, a comment or a message, where a count matters more than words.
  • Teams that acknowledge updates without adding noise to the thread.

Use something else

  • A rating that needs one answer from a scale: use a star rating or a radio group.
  • A vote that decides something: reactions are easy to add by accident and say little about why.
  • Feedback that needs words: give people a comment box.

Common failures

How it usually goes wrong

  • Emoji read as their Unicode names, then again as text

    A button holding 👍 12 with no name is read "thumbs up sign 12" or worse. Here the emoji is aria-hidden and the name says Thumbs up, 12 reactions.

  • Pressed state shown by a tint alone

    A slightly blue chip is invisible to many people. Your reactions here also have a thicker border, a bold count and aria-pressed.

  • Who reacted only on mouse hover

    A title attribute or hover card leaves keyboard and touch users out. The list here shows on focus too and is the button's description.

  • A picker with only a mouse in mind

    A grid of emoji spans that cannot be focused is unreachable. This one is a listbox with arrow keys, Home, End and typing to find.

  • Every count change announced at once

    Toggling quickly can queue a string of numbers. The live message waits until the pressing stops and says the final count once.

  • Focus lost after choosing

    Closing the picker without placing focus drops keyboard users at the top of the page. Focus returns to Add reaction.

Notes

Building it

  • The visible count sits inside the button, and the button's aria-label repeats it with the emoji's name, so the name always contains the number on screen.
  • Who reacted is a role=tooltip referenced by aria-describedby, so screen readers read it with the button; on screen it shows on hover and on focus.
  • The picker's listbox keeps focus while aria-activedescendant moves; the preview under the grid repeats the active name in large type for people who zoom.
  • Type-to-find compares the visible name and an English name in data-en, so it keeps working when the page is translated.
  • The live message repeats the new name because not every screen reader rereads a focused button whose name has just changed.

Sources: WAI-ARIA Authoring Practices: Button (toggle) · WAI-ARIA Authoring Practices: Listbox · WAI-ARIA Authoring Practices: Tooltip

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