logo

John Henry Blog -- Semantics Versus Accessibility

Semantics Versus Accessibility

back to blog Astro

4 November 2021

Semantics Versus Accessibility

Long ago, while trying to shoehorn new accessibility standards into an existing website, I ran into an interesting problem.

Kevin Powell’s video on a common HTML mistake reminded me of it.

Semantics for Accessibility

According to current standards HTML heading elements must appear in a hierarchy within a web page.

<body>
  <h1>The first Thanksgiving</h1>
  <section>
    <h2>Leaving England</h2>
    <p>
      Due to certain "differences", the pilgrims decided to leave england...
    </p>
    <section>
      <h2>Getting on the boat</h2>
      <p>The Mayflower was a mighty ship...</p>
    </section>
  </section>
  <section>
    <h2>Crossing the sea</h2>
    <p>...</p>
  </section>
</body>

This allows screen readers to recognize content and describe it to the user of a website.

The Problem

Modern component-based design would have us create components that are unaware of their containers.

Semantically, a top level heading within a component shouldn’t depend upon the headings of its ancestors.

You would be tempted do this:

<body>
  <h1>The first Thanksgiving</h1>
  <section>
    <h1>Leaving England</h1>
    <p>
      Due to certain "differences", the pilgrims decided to leave england...
    </p>
    <section>
      <h1>Getting on the boat</h1>
      <p>The Mayflower was a mighty ship...</p>
    </section>
  </section>
  <section>
    <h1>Crossing the sea</h1>
    <p>...</p>
  </section>
</body>

isolating the semantics of the sections heading.

In fact, there was a proposal to allow this — the “Document Outline Algorithm”. Sadly, it was never implemented.

But, in the case of HTML headings; if a section has a heading of “h2”, it must be the child of a section with an “h1”. A section with an “h3” must have a parent with and “h2” and so on.

The problem arises in that using sensical semantics to create components is at odds with how screenreaders interpret accessibility cues in HTML… or…

Semantics Versus Accessibility.

(That’s the name of the movie post!)

The Solution

We can mitigate this by passing a bit of context to components to indicate hierarchical positioning.

We will do this using Astro, though the concept should be translatable to other environments.

---
// file:///./boring-section.astro
const parentLevel = Astro.props.paerentLevel || 0;
const currentLevel = Astro.props.currentLevel = parentLevel + 1;
---
<style>
section {
  color: black;
  font-style: normal;
}
</style>
<section>
  <h{currentLevel}>Boring Section</h{currentLevel}>
  <slot />
</section>
---
// file:///./fancy-section.astro
const parentLevel = Astro.props.paerentLevel || 0;
const currentLevel = Astro.props.currentLevel = parentLevel + 1;
---
<style>
  section {
    color: pink;
    font-style: italic;
  }
</style>
<section>
  <h{currentLevel}>Fancy Section</h{currentLevel}>
  <slot />
</section>
---
import BoringSection from "./boring-section.astro";
import FancySection from "./fancy-section.astro";
---
---
<body>
  <BoringSection>
    <FancySection parentLevel={1}>
      Hello, Earl!
    </FancySection>
  </BoringSection>
</body>

This will produce the following HTML:

<body>
  <section>
    <h1>Fancy Section</h1>
    <section>
      <h2>Boring Section</h2>
      <p>Hello, Earl!</p>
    </section>
  </section>
</body>

You can use a utility function to make the code a bit cleaner a bit cleaner.

// file:///./get-level.mjs
export default (Astro) => (Astro.props.parentLevel || 0) + 1;
---
// file:///./boring-section.astro
import getLevel from "./get-level.mjs";
const currentLevel = getLevel(Astro);
---
<style>
section {
  color: black;
  font-style: normal;
}
</style>
<section>
  <h{currentLevel}>Boring Section</h{currentLevel}>
  <slot />
</section>

Still, this is not ideal as the developer is required to manually pass the parentLevel property through to each child.

Using nested, indexed for loops may help, but I can already imagine being convoluted.

The React Context API and the Vue Provide/Inject API may also provide solutions. Hopefully we can explore these in the future.

back to blog