Never write
<h1>–<h6>again.
Hardcoded heading levels break accessibility and SEO when components are reused. React Headings fixes this by computing each level from your component tree.
- Supports server-side rendering
- Only 369 bytes minified & brotlied
- Typed with TypeScript
- Works with any CSS solution (Tailwind, CSS-in-JS, etc.)
- Plays nicely with component libraries
npm install react-headingsUse Heading for every heading. Wrap the content that belongs under a heading in HeadingLevel: headings inside render one level deeper.
import { Heading, HeadingLevel } from "react-headings";
function Page() {
return (
<>
<Heading>Star Wars</Heading> {/* <h1> */}
<HeadingLevel>
<Heading>Characters</Heading> {/* <h2> */}
<HeadingLevel>
<Section title="Luke" /> {/* <h3> */}
<Section title="Leia" /> {/* <h3> */}
<Section title="Han Solo" /> {/* <h3> */}
</HeadingLevel>
</HeadingLevel>
</>
);
}
function Section({ title, children }) {
return (
<section>
<Heading>{title}</Heading>
<HeadingLevel>{children}</HeadingLevel>
</section>
);
}Section doesn't know its level: it renders <h3> here.
Portaled elements, like dialogs, aren't part of the page outline. Use level to start their headings at <h1>:
<Dialog>
<HeadingLevel level={1}>
<Heading>Delete file?</Heading> {/* <h1> */}
</HeadingLevel>
</Dialog>React Headings only picks the tag. Style headings however you like:
<Heading className="text-lg">Title</Heading>Or use render for component libraries:
<Heading render={(props, { tag }) => <Text as={tag} size="lg" {...props} />}>
Title
</Heading>Renders <h1>, <h2>, <h3>, <h4>, <h5>, or <h6> based on the current level. <h1> by default.
Accepts every <h1> prop and a ref, plus:
| Prop | Type | Description |
|---|---|---|
render |
(props, { level, tag }) => ReactElement |
Renders your own element. Spread props on it (includes children and ref). |
Moves the headings inside one level deeper.
| Prop | Type | Default | Description |
|---|---|---|---|
offset |
number |
1 |
Levels added to headings inside. Rarely used. |
level |
number |
Level of headings inside, ignoring parents. |
Pass offset or level, not both. Nested offsets add up. offset={0} adds nothing, which is useful for conditional nesting.
Invalid values never throw: they're ignored with a warning, as if not passed. null counts as not passed, without a warning.
Returns the level (1–6) and tag ("h1"–"h6") a Heading would render here.
In development, React Headings warns in the console when:
- a level is skipped,
- a level goes past 6,
- an
offsetorlevelis invalid.
Warnings are removed from production builds. Vite and webpack show them automatically in dev mode. In plain Node, run with --conditions=development.
MIT
