Headings are used to organize content in a web page by separating it into meaningful sections. Well-written headings help users to scan quickly through the page and get a sense of whether the page contains the information they are looking for. Headings are critical for accessibility, the adaptive technology users rely on formatted headings to understand and navigate through the page. Without a good heading, the screen reader software read the entire content as a single section.
Generally, it's considered bad practice to skip heading levels, for example having a <h4>
without a <h3>
. You can not only confuse screen readers but all readers when you don't follow a consistent pattern for your content.
From practice, I've noticed that the developers are using the wrong element just because of style. You should NOT use different heading tags just for styling purposes.
Also, in React it's very easy to end up with a wrong structure, especially when you move components with headings around. You have to check the levels if still make sense and adjust them if needed, so most of the time developers ends up with using only a <h1>
element or with a wrong structure.
A very interesting solution for this problem I found in baseweb, a component library created by Uber.
Instead of worrying about what element you have to use, you can have a React Context that handles the document outline algorithm for you. Here is the HeadingLevel
component that's used to track the heading levels.
export const HeadingLevel = ({children}: Props) => {
const level = React.useContext(LevelContext)
return (
<LevelContext.Provider value={level + 1}>{children}</LevelContext.Provider>
)
}
Now the Heading
component can consume the level and render the correct element. It contains validations to make sure you cannot have more than 6 levels deep. Also, it solves the styling problem. If you need to have a <h2>
element but styled as a <h4>
, you can use the styleLevel
prop to specify it.
import { LevelContext } from "./heading-level";
interface Props {
styleLevel?: number;
children: React.ReactNode;
}
const STYLES = ["", "h1", "h2", "h3", "h4", "h5", "h6"];
const Heading = ({ styleLevel, children }: Props) => {
const level = React.useContext(LevelContext);
if (level === 0) {
throw new Error(
"Heading component must be a descendant of HeadingLevel component."
);
}
if (level > 6) {
throw new Error(
`HeadingLevel cannot be nested ${level} times. The maximum is 6 levels.`
);
}
if (typeof styleLevel !== "undefined" && (styleLevel < 1 || styleLevel > 6)) {
throw new Error(`styleLevel = ${styleLevel} is out of 1-6 range.`);
}
const Element = `h${level}` as React.ElementType;
const classes = styleLevel ? STYLES[styleLevel] : STYLES[level];
return <Element className={classes}>{children}</Element>;
};
It might look a bit verbose, but now you don't have to worry about what element you should use, you just care about the levels. If you want to play around with this solution, you can use the sandbox below.
Top comments (1)
That's the beauty of programming, you cannot have a single solution for a problem :) I think the React Context was used to check if you have the right levels deep. You shouldn't have more than 6 levels. That's not a new idea, we even had a tag in HTML for that developer.mozilla.org/en-US/docs/W... . With your code I don't see how you'll enforce to have the right nesting. Anyway, this approach shouldn't be used on every project I think, it can be used on pages where you have a lot of content and need to be correctly structured, like documentation or online books.