Skip to content

accessibility: improve screen reader announcements for emoji reaction picker buttons - #7922

Open
UlisesMilani wants to merge 2 commits into
signalapp:mainfrom
UlisesMilani:accessibility-reactions
Open

accessibility: improve screen reader announcements for emoji reaction picker buttons#7922
UlisesMilani wants to merge 2 commits into
signalapp:mainfrom
UlisesMilani:accessibility-reactions

Conversation

@UlisesMilani

Copy link
Copy Markdown
Contributor

First time contributor checklist:

Contributor checklist:

  • My contribution is not related to translations.
  • My commits are in nice logical chunks with good commit messages
  • My changes are rebased on the latest main branch
  • A pnpm run ready run passes successfully (more about tests here)
  • My changes are ready to be shipped to users

Description

This PR improves accessibility and screen reader support for the emoji reaction buttons in the reaction picker.

Issues Addressed & Steps to Replicate

  • The Problem:
    1. The emoji buttons inside the reaction picker did not have proper descriptive labels, so screen readers announced them simply as generic "button" elements without context.
    2. The selected/active state of a reaction was not announced to screen reader users, so they had no way of knowing which reaction was currently selected.
  • Steps to Replicate:
    1. Turn on a screen reader (e.g. NVDA).
    2. Open the reaction picker on any message.
    3. Navigate to the emoji reactions. Notice they lack descriptive names or selection state feedback.
  • The Fix:
    • Configured aria-pressed={isSelected} on the interactive button to correctly convey the active selection state of the reaction.
    • Set the button's aria-label to the raw emoji character, which delegates emoji localization to the client's screen reader CLDR engine.
    • Set role="presentation" on the inner visual <FunStaticEmoji> to avoid duplicate reading in the accessibility tree.

Architectural & Accessibility Design Choices

1. Delegating Emoji Descriptions to the Client-Side Screen Reader (CLDR)

Instead of relying on custom, app-defined translations for emoji shortnames, this PR sets the aria-label directly to the raw Unicode emoji character (e.g. ❤️ or 👍).

  • Rationale: Native screen readers (NVDA, JAWS, VoiceOver, Narrator) have comprehensive, built-in dictionaries derived from Unicode's Common Locale Data Repository (CLDR) which localize emoji descriptions automatically to the user's operating system/synthesizer language. This provides a highly accurate, standard, and localized experience without burdening Signal's own translation files.
  • Outdated Client Fallback: If a client's OS or screen reader is outdated and does not recognize a newly added emoji, it will fallback gracefully to its default behavior (reading it as "unknown character" or pronouncing the Unicode sequence), which is the standard platform behavior across modern web and desktop apps.

2. Architecture Preservation & Fallback (title ?? emoji)

We preserve the app's i18n translations where they are explicitly defined:

  • The fallback logic const label = title ?? emoji ensures that when a title prop is explicitly provided (such as for the "Remove reaction" button which is translated via i18n('icu:Reactions--remove')), the translation system takes precedence. Raw emojis are only used as labels for the default reaction picker emoji selection where no custom localized label is supplied.

3. Preventing Double Announcements (DOM/Accessibility Tree Hygiene)

To prevent the screen reader from reading the emoji twice (once from the button's aria-label and once from the child element content), the inner <FunStaticEmoji> element is marked with role="presentation".

  • Validation: Manual testing with NVDA (v2024.1) on Windows 11 confirms that focusing on a reaction button reads the localized emoji name exactly once, along with its selection state (e.g., "red heart, pressed, button").

Test approach

  • Manual testing: Focused and activated emojis in the reaction picker with NVDA on Windows 11. Verified that each emoji is read correctly in the system's language and that the screen reader correctly announces whether the button is selected ("pressed") or not, with no duplicate readings.

isSelected && 'module-ReactionPickerPicker__button--is-selected'
)}
onPress={onClick}
aria-label={label}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually have an API that does a better job localizing the emoji name:

aria-label={Emoji.getDisplayLabel(emoji)}

I think we can get rid of the title attribute. The aria guide for toggle buttons specify that you shouldn't change the label based on the state:

Toggle button: A two-state button that can be either off (not pressed) or on (pressed). To tell assistive technologies that a button is a toggle button, specify a value for the attribute aria-pressed. For example, a button labelled mute in an audio player could indicate that sound is muted by setting the pressed state true. Important: it is critical the label on a toggle does not change when its state changes. In this example, when the pressed state is true, the label remains "Mute" so a screen reader would say something like "Mute toggle button pressed". Alternatively, if the design were to call for the button label to change from "Mute" to "Unmute," the aria-pressed attribute would not be needed.

https://www.w3.org/WAI/ARIA/apg/patterns/button/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review, @jamiebuilds-signal!

  1. Regarding the emoji name localization:
    I tested using Emoji.getDisplayLabel(emoji) locally with screen readers (NVDA/JAWS), but the returned text labels are very simplified compared to delegating the raw Unicode character. For example, for the crying face emoji it announces just "llorar" (cry) instead of the full descriptive name "cara llorando", and for the red heart emoji it just says "corazón" (heart) instead of "corazón rojo" (red heart).

There is no need to worry about adding translation strings in our codebase for this: by passing the raw Unicode emoji character as the aria-label, the browser/OS delegates the translation directly to the screen reader's native CLDR engine. The screen reader automatically translates and pronounces the emoji based on the active system language of the user. I verified this behavior across multiple languages (including English, Spanish, French, Portuguese, and Italian) and it works flawlessly, sounding much more natural in every single one of them.

Therefore, I've updated the full emoji picker cells (in the "More" panel) to use the raw emoji Unicode character as well, so it matches the suggested reactions' behavior and gets the correct full screen reader name.

  1. Regarding the toggle button state:
    You are completely right. I've respected the W3C ARIA toggle button guidelines and simplified the button labels to be static (always the emoji itself), letting aria-pressed={isSelected} convey the selection/active state instead of changing the label dynamically.

I also added a check so that the long-press skin tone description ("press and hold to select skin tone") is only announced for emojis that actually support skin tone variants.

I've pushed these updates to this branch. Let me know if you have any further feedback!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there are definitely many cases where the official unicode title is a better accessible description of the emoji, although there are also cases where the official title doesn't really match how emojis get used by people or are wordy to the point of being disruptive in the middle of text.

There is also a gap between the languages supported by screen readers (and operating systems or the Unicode CLDR) and the languages Signal supports. Which you aren't going to see as much only looking at very broadly supported locales like English/Spanish/French/etc). Language support can also vary on different operating systems, and where possible we try to rely on our own translations because the app locale can differ from the system locale.

I am going to bring it up with our localization team to see if we could review and get accessible descriptions for every emoji. That is gonna take some time though.

In the mean time, I wonder if it would be an improvement to use the short name from Emoji.getDisplayLabel() as the aria-label and provide the emoji in aria-description to fallback to whatever Unicode name is available.

<span role="img" aria-label="heart" aria-description="❤️">❤️</span>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, apparently our localization team was already working on getting translations together for the CLDR annotations dataset.

@UlisesMilani
UlisesMilani force-pushed the accessibility-reactions branch from ce1a708 to 3efd3e0 Compare June 13, 2026 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants