PSA: Stop Hard-Coding Heading Levels in Your React Components

Suzanne Aitchison - Sep 23 '19 - - Dev Community

(This post was originally posted on Up Your A11y - Heading Levels in Reusable Components)

Reusability is Key

One of the key reasons React is so popular is the ability to define a component, pass it some simple props, and then re-use it in a variety of places without having to write duplicate HTML throughout your app.

When creating a re-usable component, there are always a few things to consider, e.g. what should be customisable via props, and what should be an integral part of the component itself.

The problem with inflexible heading levels

Consider a simple card component which renders a title and description, which might look something like this:

class SimpleCard extends React.Component {

  render() {
    const { title, description } = this.props;
    return (
      <div className='card'>
        <h2>{title}</h2>
        <p>{description}</p>
      </div>
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

At first glance we have a nice reusable component that I can start placing throughout my app. However, we have one limiting problem - the 'h2' element.

Headings Have Semantic Value, Especially for Screen Readers

It's important to understand that heading levels in HTML are not simply about sizing and styling your header text; they provide semantic information about the organisation and importance of your content.

In particular, they are interpreted by screen readers so that users can jump directly to top level headings, next level headings and so on.

For this reason, heading levels should always increase in a logical order, and only by 1 step at a time. This allows users of assistive technology to skim and scan through your content as well as sighted users.

The Problem With Our SimpleCard Example

The SimpleCard component above defines an h2 element which will appear wherever I re-use this component. This means I can only use it on a page where there is already an 'h1' title, and where being an 'h2' makes logical sense for the flow of my page.

Given the power of React is flexible re-use of components, some refactoring would be beneficial.

Passing a Heading Level in Props

The problem can be easily solved with a simple trick that allows you to dynamically set the heading level according to the props passed in. See the upgraded version of the SimpleCard:

class SimpleCard extends React.Component {

  render() {
    const { title, description, headingLevel } = this.props;
    const Title = headingLevel;
    return (
      <div className='card'>
        <Title>{title}</Title>
        <p>{description}</p>
      </div>
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

As you can see, the component now receives the heading level as a string (e.g. 'h1') in props and dynamically creates the correct heading element to render in the card. Note in the example above that:

  • The 'Title' value could be named anything, I've just chosen 'Title' as it made sense in the context. The example would still work if the value was called 'Banana' and we rendered out <Banana>{title}</Banana>
  • 'Title' is title-cased - this is essential otherwise React will not recognise it as a DOM element.

Final Tweaks and Considerations

While dynamically creating DOM elements based on string props like this is very powerful, it could also yield some unwanted behaviour if the expected prop types aren't passed in.

I'd recommend when using this approach to also make sure you complete some props validation before attempting to create the Title element. There's a variety of ways to achieve this, but a very simple implementation could be:

class SimpleCard extends React.Component {

  render() {
    const { title, description, headingLevel } = this.props;
    const validHeadingLevels = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];

    const safeHeading = headingLevel ? headingLevel.toLowerCase() : '';
    const Title = validHeadingLevels.includes(safeHeading) ? safeHeading : 'p';

    return (
      <div className='card'>
        <Title>{title}</Title>
        <p>{description}</p>
      </div>
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Now The Component Can Be Used Anywhere!

If an appropriate heading level isn't passed in props, we default to creating a basic paragraph element instead.

Great! So now I can use my SimpleCard in a variety of locations in my app without breaking any semantic HTML conventions or degrading the experience for screen reader users.


Did you find this post useful? Please consider buying me a coffee so I can keep making content 🙂

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .