Designing Mobile Apps for Accessibility
It’s not often that you get to write an app specifically targeted at an audience with accessibility in mind. Yet, that’s exactly what we were lucky enough to do this year. In this article you’ll get to know our best-practices for designing accessible mobile apps and how to cleverly implement them, optimizing your precious time as a developer. You’ll also find code snippets for SwiftUI and Flutter.
The idea was to create SBB Inclusive - an app supporting blind people as well as people with poor eyesight in their daily use of the public transport system in Switzerland by providing them all relevant information for their specific location. So if User A is currently sitting in a train to Geneva, the app will provide him information about the train he is currently on. Or if User B is in Zurich right now, we’ll tell him about his next departures from Zurich trainstation. The app is called SBB Inclusive and optimized for large content size as well as VoiceOver usage. It will be available to the public by the end of 2020.
Developing an app with accessibility as our main focus was an interesting learning process. We’ve learnt a lot from our blind test users and from using accessibility features ourselves on a daily basis. We’d like to share our insights accompanied with examples taken from our app.
Three key principles will guide us through this article:
- Support large content sizes
- Choose colors wisely (& support dark mode)
- Optimize VoiceOver
So without further notice, let’s dive right in!
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.
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.
Depending on the case there are two different solution approaches. First, you can make sure that you allow multiline on your Texts.
// 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('...'),
),
],
)
However in some usecases (like buttons for example) this might not be desirable. So as a second fix you can also restrict the maximum font size for certain texts, so that the maximum font size is automatically computed by the OS. Bear in mind that the second approach is not optimal for people with restricted eyesight, because it might display some texts in a smaller font than their preferred content size settings. So use it only in some edge-cases and rely on allowing multiline wherever possible.
// SwiftUI
Text("...")
.minimumScaleFactor(0.1)// Flutter
AutoSizeText( // Use this awesome plugin
'...',
maxLines: 1,
minFontSize: 1,
)
It is important to note that in many cases a larger content size means that you can’t display everything you want on the size of your screen. This is fine for the users as they’re used to it. However for you as a developer, this means that you’ll need to use ScrollViews in places you wouldn’t when designing for regular content size.
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.
// SwiftUI
@Environment(\.sizeCategory) var sizeCategoryif 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();
}
So to wrap up our principle 1 (support large content sizes):
- 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.
Many users with restricted eyesight use dark mode due to it’s better contrast ratio. To make our lifes easier we created a set of semantic colors. What do we mean by ‘semantic color’? A semantic color is a set of two colors with a color value for light and dark mode each. So instead of having to worry about light and dark mode colors, you define those sets once and use them everywhere in your app.
// SwiftUI
Text(“Bern”)
.foregroundColor(SBBColor.textBlack)// Flutter
Text(
'Bern',
style: Theme.of(context).textTheme.bodyText1
.copyWith(color: SBBColors.textBlack),
);
With your semantic colors defined it really becomes a pleasure to support both light and dark mode because it doesn’t require any additional effort on the developer side.
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.
// 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.
// 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.
Unfortunately this can’t simply be done by adding a specific attribute to your code. We propose to implement a custom TextFormatter responsible to provide meaningful 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.
// 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:
// SwiftUI
Text(trainConnection.departureTime)
.accessibility(value: DateFormatter.hourMinuteVoiceoverTime.string(from: trainConnection.departureTime))// Flutter
...
To detect those cases where VoiceOver is not perfect you’ll need to test your app over and over again. Again, we can’t emphasize enough how important it is to use VoiceOver on a regular basis.
Wrapping up our third principle (optimize VoiceOver):
- 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:
- SwiftUI Accessibility Attributes (excellent summary of all SwiftUI accessibility attributes)
- A deep dive into Flutter’s accessibility widgets