Skip to content
SiteEmail

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.

PluginTriggerPriorityTypical use case
CohtmlARIAHoverReadPluginmouseenterLowDescribing buttons and interactive elements on hover
CohtmlARIAFocusChangePluginFocus changeHighReading focused elements during gamepad or keyboard navigation
CohtmlARIALiveRegionsPluginDOM mutation inside aria-liveVariable (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.


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.

inventory.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>

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.

bad-example.html
<!-- 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>

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.

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.

settings.html
<!-- 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>

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.

menu-navigation.js
// 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
}
}

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.

ValueBehavior
politeWaits for the current speech to finish before reading the update
assertiveInterrupts current speech immediately
hud.html
<!-- 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>

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:

notifications.js
const alertBanner = document.querySelector('.critical-alert');
// Setting textContent triggers the live region mutation.
// CohtmlARIALiveRegionsPlugin reads this value automatically.
function showCriticalAlert(message) {
alertBanner.textContent = message;
}

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.

index.html
<!-- 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>
accessibility.js
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.


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:

item-purchased-plugin.js
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:

store.js
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:

accessibility.js
const ariaManager = new CohtmlARIAManager([
new CohtmlARIALiveRegionsPlugin(),
new CohtmlARIAFocusChangePlugin(),
new CohtmlARIAHoverReadPlugin(),
new CohtmlARIAItemPurchasedPlugin(), // custom plugin
]);
ariaManager.observe(document.body);