Problem
Many components hardcode a specific heading tag (<h2>, <h3>, <h4>) for their title slot. When these components are composed into a page, the surrounding heading hierarchy is unknown to the component, so the rendered DOM can skip levels (e.g. h1 → h3).
This breaks WCAG 1.3.1 / 2.4.6 (Info & Relationships, Headings & Labels) and Lighthouse heading-order audit:
Heading elements are not in a sequentially-descending order.
The component author can't fix this from the consumer side without forking, monkey-patching, or wrapping with a sr-only bridge heading — all leaky workarounds.
Proposal
Add an optional as prop (or headingLevel) to every component that renders a user-supplied title heading. Default stays at the current level for backward compatibility.
type HeadingTag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
<ProfileSection as="h1" dict={...} />
<FAQ as="h3" items={...} />
<ContentIntro titleAs="h1" tocLabelAs="h2" title={...} />
Implementation sketch (per component):
function ProfileSection({ as: Heading = "h2", dict, ... }: Props) {
return (
<div>
<Heading className="font-semibold text-xl">{dict.profile.name}</Heading>
...
</div>
);
}
For components with multiple headings (e.g. ContentIntro has a title + a TOC label), expose one prop per slot (titleAs, tocLabelAs) to keep semantics explicit.
Scope — affected components (audit)
Single-title components (one as prop each):
ProfileSection — currently h2
FAQ — currently h4
Slideshow (sections label) — currently h3
WorldClockBar — currently h2
TableOfContentsPanel — currently h2
TableOfContents — currently h3
KeyboardShortcutsHelp — currently h2
Watchlist — currently h2
OrderBook — currently h2 + h3
HorizontalScrollRow — currently h3
MarketTreemap — currently h2 + h3
ActivityHeatmap — currently h2
KeyConcept — currently h4
StatusBoard — currently h2
CodePlayground — currently h4
Comparison — currently h4
Quiz — currently h4
Exercise — currently h4
ShareSection — currently h3
CompletionDialog — currently h2
Checklist — currently h4
LearningObjectives — currently h4
CandlestickChart — currently h3
StepByStep — currently h3 + h4
Multi-title components (multiple slot props):
ContentIntro — h2 title + h3 TOC label
TutorialComplete — h2 heading + h3 section labels
Out of scope (intentional structural mapping, do not change):
MdxContent, TutorialMdx, TutorialIntroContent — markdown-to-HTML heading map
FlowErrorBoundary — internal error UI, not a user title slot
Acceptance criteria
- Each component listed above accepts an
as (or per-slot *As) prop typed as "h1" | "h2" | "h3" | "h4" | "h5" | "h6".
- Default value matches the current hardcoded tag — no breaking change.
- Prop is documented in the component's TSDoc / Storybook story.
- A11y story or visual test confirms the rendered tag updates.
- Lighthouse
heading-order audit passes when consumers pass the correct level.
Notes
- Storybook stories should add an
as knob so designers can preview the level shift.
- Consider also exposing the prop on
aria-level for components that render non-heading wrappers (out of scope here).
- This is a non-breaking enhancement — safe for a minor release.
Happy to split this into per-component sub-issues if preferred.
Problem
Many components hardcode a specific heading tag (
<h2>,<h3>,<h4>) for their title slot. When these components are composed into a page, the surrounding heading hierarchy is unknown to the component, so the rendered DOM can skip levels (e.g.h1→h3).This breaks WCAG 1.3.1 / 2.4.6 (Info & Relationships, Headings & Labels) and Lighthouse
heading-orderaudit:The component author can't fix this from the consumer side without forking, monkey-patching, or wrapping with a
sr-onlybridge heading — all leaky workarounds.Proposal
Add an optional
asprop (orheadingLevel) to every component that renders a user-supplied title heading. Default stays at the current level for backward compatibility.Implementation sketch (per component):
For components with multiple headings (e.g.
ContentIntrohas a title + a TOC label), expose one prop per slot (titleAs,tocLabelAs) to keep semantics explicit.Scope — affected components (audit)
Single-title components (one
asprop each):ProfileSection— currentlyh2FAQ— currentlyh4Slideshow(sections label) — currentlyh3WorldClockBar— currentlyh2TableOfContentsPanel— currentlyh2TableOfContents— currentlyh3KeyboardShortcutsHelp— currentlyh2Watchlist— currentlyh2OrderBook— currentlyh2+h3HorizontalScrollRow— currentlyh3MarketTreemap— currentlyh2+h3ActivityHeatmap— currentlyh2KeyConcept— currentlyh4StatusBoard— currentlyh2CodePlayground— currentlyh4Comparison— currentlyh4Quiz— currentlyh4Exercise— currentlyh4ShareSection— currentlyh3CompletionDialog— currentlyh2Checklist— currentlyh4LearningObjectives— currentlyh4CandlestickChart— currentlyh3StepByStep— currentlyh3+h4Multi-title components (multiple slot props):
ContentIntro—h2title +h3TOC labelTutorialComplete—h2heading +h3section labelsOut of scope (intentional structural mapping, do not change):
MdxContent,TutorialMdx,TutorialIntroContent— markdown-to-HTML heading mapFlowErrorBoundary— internal error UI, not a user title slotAcceptance criteria
as(or per-slot*As) prop typed as"h1" | "h2" | "h3" | "h4" | "h5" | "h6".heading-orderaudit passes when consumers pass the correct level.Notes
asknob so designers can preview the level shift.aria-levelfor components that render non-heading wrappers (out of scope here).Happy to split this into per-component sub-issues if preferred.