Skip to main content


There are some techniques that are do not follow ARIA/WCAG or are workarounds to improve accessibility.

These are not recommended practices but they "get the job done".

Overriding Visible Text

An aria-label only works for interactive elements. So, HTML like this will unfortunately be announced as "3" by the screen reader:

<ion-label aria-label="3 bedrooms">3</ion-label>

You can have the screen reader announce something different from the visible text using the following HTML:

<div role="text" aria-label="3 bedrooms">
<ion-label aria-hidden="true">3</ion-label>

role="text" is not a recognised aria attribute, however it is implemented by Voice Over in iOS and TalkBack in Android.

A screen reader should read the visible text, so in the above example you should make the visible text 3 bedrooms instead of 3. You would only use this technique if your UI design takes precedence over accessibility design.

Reading Formatted Text

Voice Over will break apart text that contains formatting. For example:

<ion-text>I like <strong>big</strong> brown <em>bears</em>.</ion-text>

The above text will be read as: "I like. Big. Brown. Bears.".

To ensure that Voice Over reads this sentence as "I like big brown bears." we can set role="text":

<ion-text role="text">I like <strong>big</strong> brown <em>bears</em>.</ion-text>

role="text" is not a recognised aria attribute, however it is implemented by Voice Over in iOS and TalkBack in Android.

Page Titles

TalkBack on Android will attempt to announce the page's title. You can use this to describe the page the user is on.

In an Angular application you can set the page title in your routing. For example in app-routes.ts or app-routing-module.ts:

path: 'intro',
title: 'Introduction',

You can also set the title programatically in Angular:

import {Title} from "@angular/platform-browser";


constructor(private title:Title) {
this.title.setTitle("Introduction Part 1 of 3");

A vanilla javascript alternative may be:

document.title = "Introduction"

TalkBack will read the document title followed by "WebView". There is no known option to change this text.

First Read Element on Routing

The screen reader will select the first element it wants to read when you route between pages.

On Android this will be the page itself and after swiping right TalkBack will select a focusable element in the top left of the screen.

On iOS the first element may appear to be random but it chooses a element close to where you have pressed before.


This behavior is an undesirable quirk of VoiceOver. You can workaround this by calling focus on an element when the route changes.

In Angular we can use the stackDidChange event to know when the page change completed animating in:

<ion-router-outlet (stackDidChange)="stackChanged()"></ion-router-outlet>

The following code snippet can be used in the stackChanged function:

import { Capacitor } from '@capacitor/core';
import { ScreenReader } from '@capacitor/screen-reader';
// Only want to do this with iOS voice over
if (Capacitor.getPlatform() !== 'ios') return;

// Only want to do this when the screen reader is enabled
if (!(await ScreenReader.isEnabled()).value) return;

// This can return more than one page so select the last one
const pages = document.querySelectorAll('.ion-page:not(.ion-page-hidden, .ion-page:has(ion-tabs))');
let page: Element | undefined;
pages.forEach((e) => {
page = e;

if (!page) return;

// Find the element on the page with the class of page-focus
const e: Element | null = page?.querySelector('.page-focus');

if (!e) return;

// We need to set tabindex to -1 and focus the element for the screen reader to read what we want
(e as HTMLElement).setAttribute('tabindex', '-1');

(e as HTMLElement).focus();

stackDidChange is an internal event of Ionic Angular and may change in the future.

Animation can interfer with the way VoiceOver selects an element to read as it can chose an element during animation.

To workaround this issue we can turn off animations when the screen reader is enabled by running this code when our application starts:

import { ScreenReader } from '@capacitor/screen-reader';
import { Capacitor } from '@capacitor/core';
const { value } = await ScreenReader.isEnabled();
this.animated = !value;
// This turns off animations on iOS when the screen reader is on
if (Capacitor.getPlatform() !== 'ios') {
ScreenReader.addListener('stateChange', ({ value }) => {
this.animated = !value;

In our template we use the animated property:

<ion-router-outlet [animated]="animated">