Display
Heading
A heading whose element and whose size are two decisions.
Usage
When to reach for it
import { Heading } from '@misoto22/design'Notes
A heading, with its element and its size decided separately.
Every heading component that takes one number gets this wrong in one of two directions: either the outline is bent to reach a size (an <h1> in the middle of a page because the design wanted big type), or the size is bent to keep the outline (a section title set at 20px because it is the fourth level down). Both are one prop away from being right, so there are two.
level is the document. size is the page. The default binds them through the system's ladder, so writing only level is correct — and the moment they come apart, the call site says so.
Set in the editorial serif at weight 400, like every heading in the system. It also carries scroll-margin-top, so a heading given an id and linked from a table of contents comes to rest below the masthead rather than under it.
Anatomy
| Element | Description |
|---|---|
| Elementrequired | level, 1 to 6, rendered as the matching <h1>–<h6>. This is the document outline — what a screen reader navigates by — so it follows the section the heading opens, never the size it wants to be. |
| Steprequired | size, one of title, lead, heading, sub, item and label. Defaults from level through the system ladder, so writing only level is correct. |
| Anchor offsetrequired | scroll-margin-top: var(--scroll-offset), carried by every heading, so one given an id and linked from a table of contents comes to rest below the masthead instead of under it. |
Best practices
Do
- Write level alone unless the outline and the design genuinely disagree. The default ladder is 1→title, 2→heading, 3→sub, 4→item, 5 and 6→label — the same map article.css applies to rendered Markdown, which is what makes a post and a component page read as one publication.
- Notice that the default ladder SKIPS lead between levels 1 and 2, and skip a step yourself when you set size by hand: --fs-lead over --fs-heading is a ratio of 1.14 and reads as a rendering accident, where --fs-title over --fs-heading is 1.86 and reads as a hierarchy.
- Give a heading an id when anything links to it. The component already carries the scroll offset the anchor needs; nothing else on the page does.
Don’t
- Do not raise level to get a bigger heading. Two <h1>s on a page make its outline unnavigable, and size is one prop away.
- Do not reach past size="title". The ladder ends there because a page has exactly one thing larger than its own records; a bigger heading is className territory and it is the moment the page stopped being in the system.
- size="label" is the mono kicker at 11px and --ink-3-aa, not a small serif heading — it is what levels 5 and 6 should look like, and setting it on an <h2> makes the section title read as metadata.
Examples
level and size
The two decisions, apart. level is the document outline — what a screen reader navigates by — and size is the page. The default binds them through the system ladder, which SKIPS a step between the first two: --fs-lead over --fs-heading is a ratio of 1.14 and reads as an accident, where --fs-title over --fs-heading is 1.86 and reads as a hierarchy. The mono eyebrow is the ladder's fifth rung, worn here by an h4 so this run does not skip a level in the outline; the last pair is a semantically-correct h3 that had to look like a page title.
The White Reset
A record title
A sub-head inside it
Metadata
Third level, page-title sized
The last one is an h3. Nothing about the outline moved.
anchored headings
Two headings a table of contents can link into. Every Heading carries scroll-margin-top: var(--scroll-offset), so one given an id comes to rest below the masthead rather than under it — nothing else on a page does that, which is why an anchor into a hand-rolled h2 lands with its own title hidden behind the bar. The id is the caller's, and it has to be stable: it is what every link to this section already says.
Installation
One package, and one stylesheet next to it.
Upgrading
Minor versions add exports; they never move one.
a fragment that starts at h2
A panel dropped into a page that already has its h1. The outline decides level — this opens a section, so it is an h2 and its sub-heads are h3s — and size decides how big it looks, which is the whole reason they are two props. Raising level to reach the larger step would give the page two first-level headings and an outline nobody can navigate, for a difference one prop already covers.
Publish 0.4.1
What ships
Five display components, and the examples that explain them.
What it breaks
Nothing — every export in 0.4.0 is still exported here.
Types
export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6
export type HeadingSize = 'title' | 'lead' | 'heading' | 'sub' | 'item' | 'label'Accessibility
- level renders the real heading element, so the outline is navigable rather than merely visible.
- The two decisions are separate props, which is what lets a semantically-correct h3 look like a page title without bending the outline.
- Carries scroll-margin-top so an anchored heading is not hidden under a fixed masthead (WCAG 2.4.7 in practice).