Designing Mobile Apps for Accessibility

A hands-on guide for mobile developers

SBB Inclusive (iOS/Android app with accessibility as a main focus).
  • Support large content sizes​
  • Choose colors wisely (& support dark mode)​
  • Optimize VoiceOver​

Principle 1: Support large content sizes

Many users with restricted eyesight will make use of the OS’s ability to increase text size (also called dynamic content size). This makes it vital for you as a developer to support those different content sizes.

SBB inclusive with regular (left) and accessibility XXXL (right) content size.

Make sure your texts aren’t clipped

While testing your app with larger content sizes, one of the first things you’ll notice is that some of your texts are getting clipped. The good news is, that this is easy to fix in most cases.

// SwiftUI
Text("...")
.fixedSize(horizontal: false, vertical: true)
// Flutter
Text('...'), // it usually just works ¯\_(ツ)_/¯
// but use Expanded for Text in a Row
Row(
children: <Widget>[
Expanded(
child: Text('...'),
),
],
)
// SwiftUI
Text("...")
.minimumScaleFactor(0.1)
// Flutter
AutoSizeText( // Use this awesome plugin
'...',
maxLines: 1,
minFontSize: 1,
)
Clipped text (left), text with multiline (center), text with restricted maximum font size (right).
Using ScrollViews to make all content accessible for larger content sizes.

Provide alternative layouts for larger content sizes

Unfortunately, even the best (and simplest) designs won’t work for all content sizes. In some cases you’ll need to provide an alternative layout for larger content sizes, where the views arrangement differs according to the content size. Sometimes it even makes sense to hide icons to create more space for more relevant content.

Regular content size (left), accessibility XXXL with same layout (center) and accessibility XXXL with different layout (right).
// SwiftUI
@Environment(\.sizeCategory) var sizeCategory
if SizeCategories.accessibility.contains(sizeCategory) {
TrainConnectionRowAccessibility(…)
} else {
TrainConnectionRowNormal(…)
}
// Flutter
final mq = MediaQuery.of(context);
// Decide for yourself when to switch
if (mq.size.width <= 330 || mq.textScaleFactor >= 1.4) {
TrainConnectionRowAccessibility();
} else {
TrainConnectionRowNormal();
}
  • Make sure your texts aren’t clipped by allowing either multiline (in most cases) or by restricting the maximum font size (in some edge cases). In many cases you will need to use ScrollViews to make all content accessible.
  • Make sure you use the space in an optimal way by implementing alternative view layouts for different content sizes and hiding less relevant content (e.g. icons) in larger content sizes

Principle 2: Choose colors wisely (& support dark mode)

As you might already know, many users will not be able to distinguish the entire color palette. Also with restricted eyesight, strong contrasts become even more important. We learnt that using a small color palette with only few colors works best. Ideally this palette features some foreground and background colors which all have strong contrast ratios to each other.

Color palette used for SBB Inclusive.
// SwiftUI
Text(“Bern”)
.foregroundColor(SBBColor.textBlack)
// Flutter
Text(
'Bern',
style: Theme.of(context).textTheme.bodyText1
.copyWith(color: SBBColors.textBlack),
);
Semantic Colors used for SBB Inclusive.
Light and dark mode using our (very restricted) semantic color palette.

Principle 3: Optimize VoiceOver

Obviously, when designing an app with accessibility in mind, you need to think about VoiceOver. However, before getting into tips on how to optimize your users VoiceOver experience, here’s our key learning:

“You (as a developer with good eyesight) need to start using VoiceOver on a daily basis. By doing so, you’ll get a feeling for it and understand that the app flow is really essential for a good VoiceOver experience.”

So start by setting up shortcut commands for VoiceOver on your device and use it regularly. The following tips are our learnings from using VoiceOver ourselves and from our blind testers’ feedback .

Hide unrelevant visual information

VoiceOver will try to read out every single view that is displayed. However this is not always helpful. Often an app’s UI makes use of icons or other elements to enhance the meaning of displayed text. VoiceOver users are not interested in this duplicate information, so hide unnecessary information for them.

Hiding unnecessary information in VoiceOver.
// SwiftUI
Image(”train-small”)
.accessibility(hidden: true)
// Flutter
Image.asset(
Images.trainSmall,
excludeFromSemantics: true,
)

Combine information belonging together

Often the real information content only becomes clear, if you assemble single pieces of information. Each single piece of information itself doesn’t yield any relevant information. This means that you need to combine those pieces of information for VoiceOver.

Combining information that belongs together.
// SwiftUI
Group {
Text("12:25")
Text("12:26")

}
.accessibilityElement(children: .combine)// Flutter
MergeSemantics(
child: Column(
children: const <Widget>[
Text('12:25'),
Text('12:26'),

],
),
),

Provide better VoiceOver texts

Clever UI design provides context to presented views without explicitly stating it. Take a look at the following screens. As someone with good eyesight, you’ll automatically assume, that the train arrives at 12:25 and departs at 12:26. However this information is not explicitly written down using texts. VoiceOver users are lacking those visual associations, so you need to add it to the texts being read to provide meaningful information.

Providing better VoiceOver texts.
// SwiftUI
Group {

}
.accessibility(label: Text(TextFormatter.stopStation(for: stopStation).voiceover))
// Flutter
Semantics(
value: createSemantics(stopStation),
child: …,
)

Use headers

To reduce drag gestures (used to “swipe” from element to element in VoiceOver), many VoiceOver users will navigate from header to header for quick access to their content of interest. To help them to so, you need to specify which of your views are headers.

Using headers for quicker navigation in VoiceOver.
// SwiftUI
Text(”Einstellungen")
.accessibility(addTraits: .isHeader)
// Flutter
Semantics(
header: true,
child: Text('Einstellungen'),
)

Attention to detail

While the VoiceOver feature is really powerful on both iOS and Android, it’s far from perfect. So you’ll have to help it out a little in some specific cases. Here’s an example:

Attention to detail in VoiceOver (e.g. using TimeFormatters)
// SwiftUI
Text(trainConnection.departureTime)
.accessibility(value: DateFormatter.hourMinuteVoiceoverTime.string(from: trainConnection.departureTime))
// Flutter
...
  • Start using VoiceOver on a daily basis to get a feeling for it
  • Hide unrelevant visual information (e.g. icons)
  • Combine information belonging together
  • Provide better VoiceOver texts
  • Use headers
  • Attention to detail
  • Test VoiceOver (again and again)

Conclusion

You’ll notice that once you get the hang of it, designing for accessibility isn’t really hard. Once you are familiar with the key principles, it won’t take a lot of your precious time as a developer. On the other hand your app will be suitable to a bigger audience and more importantly accessible to everyone. Our biggest learning however is that when designing for accessibility, the general user experience of your app will also get a boost because your app’s flow will get simpler.

Further reading:

--

--

In-house agency for Mobile Apps & Web at Swiss Federal Railways #sbbcffffs

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
AppBakery

In-house agency for Mobile Apps & Web at Swiss Federal Railways #sbbcffffs