Skip to content

Instantly share code, notes, and snippets.

@nitin42
Last active March 27, 2023 18:40
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nitin42/bec1b698b3435032b98cdc8de28a5f75 to your computer and use it in GitHub Desktop.
Save nitin42/bec1b698b3435032b98cdc8de28a5f75 to your computer and use it in GitHub Desktop.
Creating an accessible Accordion component in React Native

Creating an accessible accordion component in React Native

Table of contents

What is an Accordion?

An Accordion component is a header which can be clicked or pressed to show and hide a section of related content.

An example of Accordion component. When the header is clicked, it reveals the additional related content.

Read more Accordion's use cases and when not to use it

Implementing an Accordion component

Structure

Before we go into the implementation details, let's look at each individual element which combine to form an Accordion component.

An Accordion consists of three elements:

  • Header - which can be pressed to reveal the content.
  • Icon - which changes based on the state (show and hide) - An Accordion uses the icon to provide a visual affordance to user.
  • Main content - which is displayed when the header is pressed.

Each element of the Accordion component. Header is highlighted with Green, icon with blue and the main content with red.

Each element of the Accordion component. Header is highlighted with Green, icon with blue and the main content with red.

API

To be able to have the control over how to render each of Accordion's element i.e header and the content, we will be structuring the API in this manner -

<Accordion
  label="Payment settings"
  open={true}
  content={<Text>Your payment settings</Text>}
/>

In the above example, we can see that the Accordion includes three props:

  • label to set the label text for header
  • content which accepts a component to render the main content when the header is pressed.
  • open to set the Accordion to be open by default.

Creating the touchable header

To create the touchable header, we will be using the TouchableHighlight component. The touchable header component has two things to do -

  • showing the header text,
  • and updating the Accordion's state i.e whether it is open or closed when it is pressed.

Variants

The touchable header will have two variants:

  • Standard - This will be the default variant

standard-variant

  • Expanded - This will be rendered when the Accordion is in open state.

expanded-variant

Difference between standard and expanded variant The key difference between both the variants is that, the latter is used to provide a visual affordance to a user. When the Accordion is in the default state i.e closed, it shows the plain header but when it is opened, it highlights the header section.

Let's draw the basic markup of the touchable header component.

import * as React from 'react';
import {
  View,
  Text,
  TouchableHighlight,
} from 'react-native';
import Icon from 'react-native-vector-icons/AntDesign';

const TouchableHeader = (props) => {
  /**
   * Switch between different icons depending upon the Accordion's state.
   */
  const iconName = props.isOpen ? 'minus' : 'plus';

  return (
    <TouchableHighlight onPress={props.onPress}>
      <View>
        <Text>{props.label}</Text>
	<Icon name={iconName} size={16} color="#003366" />
      </View>
    </TouchableHighlight>
  );
};

To summarise, we have created a touchable header component that receives three props -

  • isOpen which tells us whether the Accordion is open or closed.
  • label for showing the header text
  • onPress callback, which will set the Accordion's state on press.

Let's include some basic styling as well to ensure that each element is at the appropriate place.

import * as React from 'react';
import {
  View,
  Text,
  TouchableHighlight,
  StyleSheet
} from 'react-native';
import Icon from 'react-native-vector-icons/AntDesign';

const TouchableHeader = (props) => {
  /**
   * Set a bottom border when the accordion is opened. It ensures
   * that there is clear separation between header and the content
   * area.
   */
  const touchableViewBorderWidth = props.isOpen ? 1 : 0;
  /**
   * Set a background color for the accordion when it is opened.
   * This helps provide visual affordance to a user.
   */
  const headerBackgroundColor = props.isOpen ? '#F2F2F2' : 'none';

  /**
   * Switch between different icons depending upon the Accordion's state.
   */
  const iconName = props.isOpen ? 'minus' : 'plus';

  return (
    <TouchableHighlight
      onPress={props.onPress}
      style={[
        styles.touchableView,
        { borderBottomWidth: touchableViewBorderWidth },
      ]}>
      <View
        style={[
          styles.headerWrapperStyles,
          { backgroundColor: headerBackgroundColor },
        ]}>
        <Text style={styles.labelStyle}>{props.label}</Text>
	<Icon name={iconName} size={16} color="#003366" />
      </View>
    </TouchableHighlight>
  );
};

const styles = StyleSheet.create({
  // Styles for the header section
  headerWrapperStyles: {
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    flexDirection: 'row',
    padding: 12,
  },
  // Base styles for the touchable highlight component
  touchableView: {
    borderColor: '#F2F2F2',
    borderRadius: 2,
  },
  // Styles for the header text
  labelStyle: {
    fontWeight: 'bold',
    fontSize: 15,
    color: '#003366',
  },
});

Great! We have now created the touchable header component which -

  • shows the header text and the icon
  • renders different variants based on the state i.e open or closed

With the above styling and markup, our touchable header component should look like this -

Touchable header showing different variants based on the state.

Revealing the content

Let's create a component that will show the main content when the Accordion is in open state.

import * as React from 'react';
import {
  View,
  StyleSheet
} from 'react-native';

const Content = (props) => {
  if (props.isOpen) {
    return <View style={styles.accordionContent}>{props.children}</View>;
  }

  return null;
};

const styles = StyleSheet.create({
  accordionContent: {
    padding: 12,
  },
});

Easy, right?

Combining the parts

Now let's merge both, the header and content component to create the Accordion. First, we will need to add a local state variable to keep track of Accordion's state.

/**
 * State to track whether the expander is open or closed.
 * The default state can also be controlled using the "open" prop.
 */
const [isOpen, setIsOpen] = React.useState(props.open || false);

Second, we will need a callback which will be invoked when the header is pressed, and will be used to update the state.

const onPress = () => {
 setIsOpen(!isOpen);
};

Final step is to render the markup.

const Accordion = (props) => {
  /**
   * State to track whether the expander is open or closed.
   */
  const [isOpen, setIsOpen] = React.useState(props.open || false);

  /**
   * Invoked when the header element is pressed.
   */
  const onPress = () => {
    setIsOpen(!isOpen);
  };

  return (
    <View style={styles.accordionWrapper}>
      <TouchableHeader
        onPress={onPress}
        isOpen={isOpen}
        label={props.label}
      />
      <Content isOpen={isOpen}>{props.content}</Content>
    </View>
  );
};

const styles = StyleSheet.create({
  accordionWrapper: {
    borderColor: BORDER_COLOR,
    borderRadius: 2,
    borderWidth: 1,
  },
  app: {
    display: 'flex',
    justifyContent: 'center',
    margin: 10
  }
});

Nice! Now let's render the Accordion component and see how it would like.

const App = () => (
  <View style={styles.app}>
    <Accordion
      label="Payment settings"
      content={<Text>Your payment settings</Text>}
    />
  </View>
);

const styles = StyleSheet.create({
  app: {
    display: 'flex',
    justifyContent: 'center',
    margin: 10
  }
});

Accordion component

Playground

https://snack.expo.dev/embedded/@nitintulswani/react-native-accordion?preview=true

Accessibility

Defining a role

As a sighted user, the design of the Accordion indicates to me that it is an interactive/clickable control. But as a screen reader user, I won't be able to know if the Accordion component is interactive or clickable which creates confusion.

To solve this, we can use the prop accessibilityRole which defines a role to provide context on the currently focused element. Since our Accordion is interactive (shows and hides content view on press) and clickable, the most appropriate role attribute that we can use is button

const TouchableHeader = (props) => {
...
...

 return (
  <TouchableHighlight accessibilityRole="button" {...props}>
   ...
  </TouchableHighlight>
 )
}

Without the prop accessibilityRole , when the Accordion is focused, the screen reader would just announce the header text -

Screen reader: Payment settings

As we can see, just announcing the header text when the element is focused is not very helpful because I don't know if the element is interactive or not. But when the accessibilityRole prop is defined, the screen reader would announce -

Screen reader: Payment settings, button

So now I know that the element is a clickable and it also gave me a hint that what might happen upon the interaction.

Defining a label

Again, as a sighted user, I had the visual affordance of the icon (minus and plus) in the header indicating the purpose i.e when the Accordion is in open state, it shows minus icon and when it is in close state, it shows plus icon. But the screen reader won't convey this information, so let's fix that.

Let's add the prop accessibilityLabel to define the label.

const TouchableHeader = (props) => {
...
...

 return (
  <TouchableHighlight accessibilityLabel={props.label} accessibilityRole="button" {...props}>
   ...
  </TouchableHighlight>
 )
}

Now, we need to update the label text in such a way that when the Accordion is in close state, the screen reader announces Open payment settings, button and when it is in open state, it announces Close payment settings, button.

To do that, let's define prefixes for our label -

const a11yLabelPrefixes = {
  open: 'Open',
  close: 'Close',
};

Next, we need to update the label based on the state -

const a11yLabelPrefixes = {
  open: 'Open',
  close: 'Close',
};

const TouchableHeader = (props) => {
...
...

const a11yLabelPrefix = a11yLabelPrefixes[props.isOpen ? 'close' : 'open'];
const accessibilityLabel = `${a11yLabelPrefix} ${props.a11yLabel}`
 
return (
  <TouchableHighlight accessibilityLabel={accessibilityLabel} {...props}>
   ...
  </TouchableHighlight>
 )
}

So when the Accordion is in close state, screen reader will announce the label as -

Open payment settings, button

and when it is in open state, screen reader will announce the label as -

Close your payment settings, button

Defining the label and role via props accessibilityLabel and accessibilityRole, makes it easy for a user to understand the purpose of the control (Accordion) and gives a hint that what might happen when I interact with the element.

Additional resources

Thanks for reading. Let me know if you found the blog post helpful!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment