Typography: Separating Style from Semantics
Tags: css, a11y, react, tutorial
Published: 2020-12-13 12:26:20

Getting started with accessibility can be intimidating, especially when your project has complicated user flows and interactions. A great entry point into accessibility is fixing up your heading hierarchy. Fixing this simple issue can make navigating the site much easier for your users.

This article focuses on improving webpage accessibility in a small but meaningful way. For more information on headings and accessibility, consult the [W3 Headings Accessibility Tutorial](https://www.w3.org/WAI/tutorials/page-structure/headings/).

> The intent of this Success Criterion is to ensure that information and relationships that are implied by visual or auditory formatting are preserved when the presentation format changes.
>
> \- [WCAG Success Criterion 1.3.1: Info and Relationships](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships.html)

## Heading Hierarchy

Correct heading hierarchy helps assistive technology like screen readers interpret your pages. Consistently applying styles with a 1:1 mapping between heading elements and size is the ideal scenario. On complex websites, though, design constraints often prevent us from consistently styling headings, so we're left with three options.

### Styles > Hierarchy

If we prioritize styles, we conform to design constraints but lose the heading hierarchy. Even worse, this results in misleading headings that explicitly miscommunicate the structure to screen readers and other assistive technology.

{% codesandbox typography-broken-headings-dj42o module=index.html view=split %}

This use of headings produces a document structure that implies the first `h3` (Life) isn't a parent heading of the subsequent `h2` despite Kingdom being a grouping within Life - this isn't the intended outcome.

This might not be a big deal for seeing users, but we want everyone to be able to fully navigate and experience the products we build, so we should avoid this practice.

### ~~Fight~~ Work with Design to Make Styles = Hierarchy

This option isn't always available, but you should apply it wherever possible. While not universal, text size indicates a visual hierarchy within the document, and making an `h3` larger than an `h2` can be misleading to the user.

If your designer isn't familiar with accessibility standards, work with them to better understand heading structure and WCAG guidelines. Engineers can't own accessibility alone, and neither can designers - it's a shared responsibility for everyone to invest time and effort into. If everyone does their part, the product will be better for **all** users.

If you reach full alignment between engineers and designers, you end up with a heading structure that matches both visually and semantically.

{% codesandbox typography-consistent-headings-r3crz module=index.html view=split %}

### Styling Elements Independent from Semantics

For the times we can't reach 100% alignment between engineering and design, we should still work to reach a helpful information hierarchy in our HTML. By separating appearance from the element tag, we can achieve the desired style while maintaining the correct heading hierarchy.

{% codesandbox typography-independent-styles-s1fjs module=index.html view=split %}

So how do we separate appearance from the element tag? The easiest solution is to use global classes that match the intended element:

```css
/* classes match the intended element */
h1,
h2,
h3,
h4,
h5,
p,
.h1,
.h2,
.h3,
.h4,
.h5,
.body {
  margin: 0 0 0.5em 0;
  font-family: "Poppins", sans-serif;
}

h1,
.h1 {
  font-size: 3rem;
  font-weight: bold;
}

h2,
.h2 {
  font-size: 2.5rem;
  font-weight: bold;
}

h3,
.h3 {
  font-size: 1.75rem;
  font-weight: bold;
}

h4,
.h4 {
  font-size: 1.25rem;
  font-weight: 600;
}

h5,
.h5 {
  font-size: 1rem;
  font-weight: 600;
}

p,
.body {
  font-size: 1rem;
  font-weight: normal;
}
```

_Note: the styles here aren't specific recommendations, rather an example of what **can** be done._

## Existing Implementations

While I've walked through a manual implementation with raw HTML and CSS, you may want a more robust solution. You could build one from scratch without much difficulty, but there are also existing solutions in component libraries:

- [Material UI - Typography](https://material-ui.com/components/typography/)
- [Semantic UI - Header](https://react.semantic-ui.com/elements/header/)

## Conclusion

Hopefully this tutorial helped you learn a bit more about accessibility in web development and how to incorporate it into your regular development practices. If you've come up with your own solution for this topic, please share it below.

## Appendix: Automated Heading Tags

Still here? Okay, let's go a little further. What if I use a component in two separate parts of the app - one where the parent heading is an `h1` and another where the parent heading is an `h2` - what should be the child heading tag in this component?

I might argue that in the first scenario it should be an `h2` and in the second it should be an `h3`. Should we do a custom heading prop? 🤢 Let's try solving it with React context instead.

```jsx
// Heading.jsx
import React, { createContext, useContext } from "react";

const headingLevelContext = createContext(0);

function useHeadingLevelContext() {
  return useContext(headingLevelContext);
}

export function Heading({ size, as, children }) {
  const defaultComponent = `h${useHeadingLevelContext()}`;
  const HeadingComponent = as || defaultComponent;

  return (
    <HeadingComponent className={size || HeadingComponent}>
      {children}
    </HeadingComponent>
  );
}

export function HeadingBlock({ level, children }) {
  const defaultLevel = useHeadingLevelContext();

  return (
    <headingLevelContext.Provider value={(level || defaultLevel) + 1}>
      {children}
    </headingLevelContext.Provider>
  );
}
```

`HeadingBlock` keeps track of the heading level by incrementing the heading level context when nested inside another `HeadingBlock` and increasing the heading tag used by `Heading`. Let's look at it in action:

{% codesandbox automatic-heading-level-olroi module=src/App.js view=split %}

By capturing the heading level in the context, we can provide the size for styling, while the context determines the heading element used.

Abstracting heading semantics away from developers may also have downsides, and I haven't tested it in a production application or accessibility audit, but it's an interesting idea to explore further.