Accessible Bottom Tab Navigation on Android

Bottom tabs have become such a popular mode of navigation in mobile apps that they’ve gotten their own official design guideline in the Material Design docs, but making them accessible for users with visual or motor disability can be a challenge. The default BottomTabNavigationView class has some slick animations that looks great, but can present challenges for users expecting navigation items to reliably be in the same place on the screen. And when screen readers read the tab views for users with visual impairment, the results can be confusing. But, in software, there’s always a way around. Here’s how to make your bottom tabs accessible to all your users.

The first problem with the BottomTabNavigationView is that, by default, screen readers will simply read the titles of the tabs. This can be confusing, since there’s nothing to distinguish the tab from any other clickable view, and users who can’t see the tab or the screen might not have enough context from the rest of the elements on the screen to know what the tab actually is or does. To fix that, we’ll override some functionality in the View and improve the default contentDescription property of the tab views.

Since we’re overriding some default functionality, the first step is to create a new class that extends BottomTabNavigationView.

public class AccessibleBottomNavigationView extends BottomNavigationView

Next, replace the old default view class in your xml with this new class you’ve just created.

<com.example.myapp.AccessibleBottomNavigationView
android:id="@+id/bottom_bar"
android:layout_width="match_parent"
android:layout_height="64dp"
android:background="@drawable/bottom_bar_bg"/>

Now, before I can modify the tab views, I need references to them. I’ll declare a property in my class to hold the ViewGroup that contains the tabs:

private ViewGroup items;

Next, after inspecting the hierarchy of the view, I know that the first child in the root group contains the tabs, so I’ll capture it so that I can modify them later… I know that the root ViewGroup is available when the onFinishInflate() function fires, so I’ll override it:

@Override
protected void onFinishInflate() {
super.onFinishInflate();
items = (ViewGroup) getChildAt(0);
}

Now that I have a way to reference my tab views, I need to find a place to do it. I’m going to override the draw() function in my class, because I know that by the time the draw() function is called, the tab views will all have been inflated, and also because I know the view will redraw when a tab is selected, and I want to make sure to update my contentDescriptions to reflect that.

@Override
public void draw(Canvas canvas) {
super.draw(canvas);
for (int i = 0; i < getMenu().size(); i++) {
boolean isChecked = getMenu().getItem(i).isChecked();
View menuItemView = getMenuItemView(i);
if (menuItemView != null && tabNameResIds.size() == getMenu().size()) {
String tabName = getResources().getString(tabNameResIds.get(i));
String contentDescription;
if (isChecked) {
contentDescription = getResources().getString(R.string.accessibility_tab_format_selected, tabName);
} else {
contentDescription = getResources().getString(R.string.accessibility_tab_format, tabName);
}
menuItemView.setContentDescription(contentDescription);
}
}
}

And to get each tab view from my saved ViewGroup reference I use:

private View getMenuItemView(int position) {
View bottomItem = items.getChildAt(position);
if (bottomItem instanceof MenuView.ItemView) {
return bottomItem;
}
return null;
}

As you can see from the snippets above, I’ve given my AccessibleBottomTabNavigationView a list of resource ids to hold my tab names. You can set those programmatically or by using custom attributes in your xml, or if you only have one Bottom Tab view in your app, you can hardcode the tab names in the view class itself. Every time the draw() method is called, this overridden version will execute, loop through the tabs and reset the contentDescriptions to something more explicit. In this case, I just use the original tab title plus the word “tab,” with an even more explicit string for the selected tab, “<tab name> tab. selected.” These subtle changes indicate to disabled users that the element is a tab, and that it is or isn’t the currently selected tab. That can make all the difference when a user is trying to navigate through the app without seeing it.

So with just a few simple lines of code we can turn the vague descriptions in the original view into something much more accessible.

The other problem with the bottom tab navigation view is that, if you add more than three tabs to it, it will animate the selected tab, changing it’s location on the screen:

This behavior can be confusing when using explore-by-touch, since the tab locations are changing. Unfortunately, since the properties that govern this behavior aren’t accessible to us, we can’t change this be overriding behavior. Instead, we have to use some information we can dig out of the source code to make this class do what we want. I prefer this solution, which uses reflection to get the fields we need to modify. It’s not pretty, but it works. Just keep in mind that, with each new release of the design support library, this solution could be broken. So make sure to test it every time you upgrade your support libraries.

So that’s it. With a little know-how we’ve managed to take the official version of the Bottom Tab Navigation implementation and make it a whole lot better. Now we just need to wait for Google to catch up.

Director, Mobile Product Engineering at Realogy