React Native: A Checkbox Story

featured-19

In a recent blog, we described how we built the app for Pride in London with React Native. We mentioned how important accessibility was and that it provided some new challenges. I want to describe one example in more detail: checkboxes.

Semantics

I should start by saying that, similar to the web, widgets in mobile apps need to be declared with the correct semantics to be picked up by assistive technologies like screen readers. If something looks like a button, sighted users will press it. But if it is not declared as a button, screen reader users will not know that they can press it. In the same way, we need to declare headings, text boxes and also checkboxes.

React Native supports these semantics by using the properties accessibilityTraits on iOS and accessibilityComponentType on Android. Looking at the documentation you will notice that there are very few options for Android. This is an area that we should improve. Checkboxes however, are fine on both platforms:

  • iOS: Add the trait "selected" when the checkbox is selected; otherwise remove it. Some apps also use the "button" trait to indicate that the element is clickable.
  • Android: Toggle between "radiobutton_checked" and "radiobutton_unchecked".

A simple checkbox

When we combine the semantics for Android and iOS, add some text and a checkbox image, a simple component can look like this: 

class MyCheckbox extends React.PureComponent {
  render() {
    const { label, checked, onChange } = this.props;
    
    return (
      <TouchableOpacity
        accessibilityComponentType={
          checked ? "radiobutton_checked" : "radiobutton_unchecked"
        }
        accessibilityTraits={
          checked ? ["button", "selected"] : ["button"]
        }
        onPress={onChange}
      >
        <Image source={checked ? checkboxChecked : checkboxUnchecked} />
        <Text>{label}</Text>
      </TouchableOpacity>
    );
  }
}

In "onChange" we expect the consumer of this component to toggle the checked prop. Using VoiceOver with this checkbox we can hear it saying:

  • Focus unchecked checkbox: LABEL, "button"
  • Then double tap to toggle: "selected", LABEL

  • Focus checked checkbox: "selected", LABEL, "button"
  • Then double tap to toggle: LABEL

The challenge

In the Pride in London app, we used a component like this for quite some time. At some point, however, we noticed that VoiceOver was a bit off. When toggling the checkbox, it first read the old state and then the new one as if it was a new component:

  • Focus unchecked checkbox: LABEL, "button"
  • Then double tap to toggle: LABEL, "selected", LABEL, "button"

  • Focus checked checkbox: "selected", LABEL, "button"
  • Then double tap to toggle: "selected", LABEL, LABEL, "button"

We came up with these points contributing to this problem:

  • On double tap VoiceOver always reads the content of the currently focused element.
  • Rerendering our components in JavaScript took longer as the app got more complex.

Together this seems to confuse VoiceOver, leading to a decreased experience for our users.

TalkBack on Android does not suffer from this problem. The reason might be, that TalkBack does not read out elements on double tab. Instead it requires custom logic to notify TalkBack about the changed state of our checkbox. You can find a good example for this in the React Native documentation.

The solution

Since we introduced the issue by increasing our render time, the obvious solution would be to reduce the time we spend rendering. This proved to be difficult, though. We could reproduce the issue sometimes, even if our rendering finished within ~50ms, an acceptable value after a user interaction.

Instead, we turned to the native side and toggled the "selected" accessibility trait immediately after a tap.

- (void) handleTap:(UITapGestureRecognizer *)recognizer
{
  UIView *view = recognizer.view;
  UIAccessibilityTraits newTraits = view.accessibilityTraits^UIAccessibilityTraitSelected;
  view.accessibilityTraits = newTraits;
}

 

This gives our checkbox the same restrictions as the "Switch" component: it is not possible to cancel the toggle. But it ensures, that VoiceOver always correctly identifies the changed checkbox state, which leads to a good experience for screen reader users.

Final words

It is important to make sure your app works for everyone. We found, that you to constantly need test your app to make sure you don't accidentally regress. React Native provides some unique challenges because you need to think about different platforms when building accessible components.

We published our solution as react-native-accessible-selectable on GitHub. If you experience similar issues, we hope this can help.

Do you want to give feedback or discuss anything in this article? Tweet me directly here: @frigus02