ARIA Plugins
Gameface ships three ready-made ARIA plugins that cover the most common accessibility use cases in game UI: CohtmlARIAHoverReadPlugin for mouse users, CohtmlARIAFocusChangePlugin for gamepad and keyboard users, and CohtmlARIALiveRegionsPlugin for dynamic content announcements. Each plugin is self-contained and can be enabled independently. Together, they cover the full interaction surface of a game UI without any custom TTS logic.
Plugin Overview
Section titled “Plugin Overview”| Plugin | Trigger | Priority | Typical use case |
|---|---|---|---|
CohtmlARIAHoverReadPlugin | mouseenter | Low | Describing buttons and interactive elements on hover |
CohtmlARIAFocusChangePlugin | Focus change | High | Reading focused elements during gamepad or keyboard navigation |
CohtmlARIALiveRegionsPlugin | DOM mutation inside aria-live | Variable (polite/assertive) | Announcing notifications, status changes, objective updates |
The priority column reflects how the plugin schedules its speech requests in the SpeechAPI queue. High-priority requests interrupt lower-priority ones. A focus change read will cut off an in-progress hover read; a focus change read will not be interrupted by a background notification set to polite.
CohtmlARIAHoverReadPlugin
Section titled “CohtmlARIAHoverReadPlugin”This plugin speaks the content of any element the pointer enters. It reads the element’s aria-label attribute first. If no aria-label is present, it reads the element’s visible text content. If neither exists (icon-only button with no label), nothing is spoken.
Required HTML
Section titled “Required HTML”<!-- aria-label is the most reliable source for the plugin to read. The visible text "+" alone lacks context for a TTS user. --><button class="slot__add-btn" aria-label="Add item to slot 3">+</button>
<!-- An icon button with no visible text - aria-label is mandatory --><button class="action-btn action-btn--equip" aria-label="Equip weapon"> <img src="./icons/sword.svg" alt="" /></button>
<!-- Text button - aria-label optional; the plugin falls back to inner text --><button class="menu-btn">Resume Game</button>What Not to Do
Section titled “What Not to Do”Avoid placing aria-label values that repeat the visible text verbatim with no additional context. The hover plugin adds value when the label provides information the visual design cannot.
<!-- This adds nothing over what a sighted user already reads --><button aria-label="Start">Start</button>
<!-- This is better - it adds context for a TTS user --><button aria-label="Start new game from the beginning">Start</button>CohtmlARIAFocusChangePlugin
Section titled “CohtmlARIAFocusChangePlugin”This plugin is critical for gamepad and keyboard navigation. Every time DOM focus moves to a new element, the plugin reads that element aloud. Without it, a player navigating a settings menu with a gamepad has no audio feedback about which option is currently selected.
The plugin reads aria-label first, then falls back to visible text content. On focusable elements where neither is meaningful (a canvas, an abstract container), add an explicit aria-label.
Required HTML
Section titled “Required HTML”The plugin fires on any focus event, but the elements need to be focusable. Interactive elements (<button>, <input>, <a>, <select>) are focusable by default. Custom components built from <div> or <span> must use tabindex to enter the focus ring.
<!-- Native focusable elements - the plugin reads these automatically --><button class="settings-item" aria-label="Graphics quality: High"> Graphics Quality <span class="settings-item__value" data-bind-value="settings.graphicsQuality"></span></button>
<!-- Custom focusable component via tabindex --><div class="keybind-row" tabindex="0" aria-label="Jump: Space"> <span class="keybind-row__action">Jump</span> <span class="keybind-row__key">Space</span></div>Gamepad Navigation Pattern
Section titled “Gamepad Navigation Pattern”In a gamepad-driven menu, focus moves programmatically rather than via mouse click. The pattern typically involves calling element.focus() in response to D-pad input. The plugin fires on the resulting focus event regardless of how the focus change was triggered.
// Called when the player presses D-pad down on the gamepad.// The focus change itself triggers CohtmlARIAFocusChangePlugin to speak the new element.function moveFocusDown(currentElement) { const next = getNextFocusableElement(currentElement); if (next) { next.focus(); // plugin reads this element automatically }}CohtmlARIALiveRegionsPlugin
Section titled “CohtmlARIALiveRegionsPlugin”This plugin monitors elements marked with aria-live and reads their text content when it changes. It is the standard mechanism for announcing dynamic UI updates without requiring the player to focus or hover on an element.
aria-live Values
Section titled “aria-live Values”| Value | Behavior |
|---|---|
polite | Waits for the current speech to finish before reading the update |
assertive | Interrupts current speech immediately |
<!-- Mission objective updates - polite; they can wait --><div class="objective-tracker" aria-live="polite" data-bind-value="mission.currentObjective"></div>
<!-- Critical alert (low health, incoming attack) - assertive; speaks immediately --><div class="critical-alert" aria-live="assertive" data-bind-value="alerts.criticalMessage"></div>
<!-- Chat messages - polite; new messages queue behind current reads --><ul class="chat-log" aria-live="polite"> <!-- Items are added dynamically via data binding --></ul>When Content Changes
Section titled “When Content Changes”The plugin reacts when the DOM content inside an aria-live element is mutated. For data-bound elements, this means the plugin fires automatically when the bound model value changes. No additional JavaScript is needed.
For manually written content updates, update the element’s textContent or innerHTML directly:
const alertBanner = document.querySelector('.critical-alert');
// Setting textContent triggers the live region mutation.// CohtmlARIALiveRegionsPlugin reads this value automatically.function showCriticalAlert(message) { alertBanner.textContent = message;}Combining All Three Plugins
Section titled “Combining All Three Plugins”Most game UIs need all three plugins running simultaneously. Pass them all to the manager at initialization. The manager routes events to the correct plugin internally and the SpeechAPI queue handles priority arbitration.
<!-- Load order: utilities → SpeechAPI → plugins → manager entry point --><script src="./js/aria-js/cohtml-aria-utils.js"></script><script src="./js/aria-js/cohtml-aria-common.js"></script><script src="./js/aria-js/cohtml-aria-plugin.js"></script><script src="./js/speechAPI/cohtml-speech-api.js"></script><script src="./js/aria-js/plugins/cohtml-aria-live-region.plugin.js"></script><script src="./js/aria-js/plugins/cohtml-aria-hover-read.plugin.js"></script><script src="./js/aria-js/plugins/cohtml-aria-focus-change.plugin.js"></script><script src="./js/aria-js/cohtml-aria-observer.js"></script><script src="./js/aria-js/cohtml-aria-manager.js"></script>const ariaManager = new CohtmlARIAManager([ new CohtmlARIALiveRegionsPlugin(), new CohtmlARIAFocusChangePlugin(), new CohtmlARIAHoverReadPlugin(),]);
ariaManager.observe(document.body);The order of plugins in the array does not affect priority. Priority is determined by the SpeechAPI channel each plugin uses internally.
Writing a Custom Plugin
Section titled “Writing a Custom Plugin”If the built-in plugins do not cover a specific interaction in your UI, you can extend CohtmlARIAPlugin to build your own. A custom plugin implements two things: an observedDomEvents getter that declares which DOM events it listens for, and an onDOMEvent handler that reacts to them.
The example below speaks a custom confirmation message when a specific UI component fires a custom "item-purchased" event:
class CohtmlARIAItemPurchasedPlugin extends CohtmlARIAPlugin { onDOMEvent(event) { if (event.type === 'item-purchased') { // event.detail.itemName is set by the dispatching component const message = `Purchased: ${event.detail.itemName}`; // Schedule via the SpeechAPI queue on a polite channel this.speakPolite(message); } }
get observedDomEvents() { // Declare every DOM event type this plugin should receive return ['item-purchased']; }}To fire the event from the purchasing component:
function purchaseItem(item) { // ... purchase logic ...
// Dispatch the custom event - CohtmlARIAItemPurchasedPlugin picks it up const event = new CustomEvent('item-purchased', { detail: { itemName: item.name }, bubbles: true, }); document.body.dispatchEvent(event);}Register the custom plugin alongside the built-in ones:
const ariaManager = new CohtmlARIAManager([ new CohtmlARIALiveRegionsPlugin(), new CohtmlARIAFocusChangePlugin(), new CohtmlARIAHoverReadPlugin(), new CohtmlARIAItemPurchasedPlugin(), // custom plugin]);
ariaManager.observe(document.body);© 2026 Coherent Labs. All rights reserved.