On this page
Semantic heading elements for structuring page and section content
- Overview
- Rules and guidance
- Properties
- Styles
- Accessibility
When to use
- To title a page, section, or content block so users can scan and navigate the content hierarchy
- To create a logical document outline that assistive technology can traverse
- When a designer has specified a heading size for a piece of content and it needs to render in the design token system
When not to use
- For body text or labels - use Text (P) instead.
- For decorative large text that carries no structural meaning - use Text with appropriate styling rather than a heading element that would mislead screen reader users.
- To make text bigger without implying document structure - use the as prop to separate visual size from semantic level rather than choosing the heading component based on appearance alone.
Anatomy
- Heading text
Basic usage
Use the numbered heading component (Heading1 through Heading4) that matches the correct semantic level in the document outline.
Edit live code
<> <Heading1>Heading 1</Heading1> <Heading2>Heading 2</Heading2> <Heading3>Heading 3</Heading3> <Heading4>Heading 4</Heading4> </>
Variants and features
Heading levels
Nebula provides four heading components: Heading1, Heading2, Heading3, and Heading4. Each renders the corresponding HTML element (h1 through h4) and applies the matching visual size from the design token system.
Choose the component based on its semantic level in the content hierarchy, not its visual size. Do not skip levels. If the page has an h1 and a section needs a sub-heading, use h2 regardless of how large or small you want it to appear.
Edit live code
<> <Heading1>Your account</Heading1> <Heading2>Billing</Heading2> <Heading3>Payment method</Heading3> <Heading4>Direct Debit details</Heading4> </>
Separating visual size from semantic level
When the correct semantic level and the desired visual size do not match, use the as prop from styled-components to render a different HTML element while keeping the visual styling.
For example, a section heading that sits at h3 in the document outline but should appear at the Heading2 visual size:
Edit live code
<Heading2 as="h3">Section title</Heading2>
Use this sparingly. If your design consistently requires as overrides, the page structure or design may need revisiting.
Responsive sizing
All heading components resize automatically at the smallAndUp breakpoint. Margins between headings and adjacent content also adjust responsively. No extra configuration is needed.
usage
- Heading must be chosen based on its semantic position in the document outline, not its visual sizeScreen readers and assistive technology use heading levels to allow users to navigate page structure. Choosing a heading for its appearance breaks this navigation.Copy rule ID for heading.semantic-level
- Heading must be used instead of raw h1–h4 HTML elements in application codeNebula heading components apply responsive font sizes and spacing from the design token system. Raw elements bypass these.Copy rule ID for heading.token-system
- Heading levels must not skip levels in the document outline (e.g. jumping from Heading1 to Heading3)Skipping levels creates a broken document outline that is confusing for screen reader users.Copy rule ID for heading.no-skip-levels
- Heading1 should appear no more than once per pageMultiple h1 elements make the page purpose ambiguous for assistive technology and search engines.Copy rule ID for heading.one-h1-per-page
- Heading should use the as prop when the correct semantic level and the required visual size differUsing the wrong heading component to achieve a visual size breaks document structure. The as prop lets both be correct independently.Copy rule ID for heading.use-as-prop-for-visual-mismatch
- Heading should not have its color overridden outside the themeHeading color is set to colors.heading, which contrasts correctly against background tokens. Overriding it may break contrast ratios.Copy rule ID for heading.color-not-overridden
placement
- Heading will collapse its top margin automatically when it is the first child or immediately follows a heading of the level aboveHeading components collapse their top margin in these positions to avoid double spacing. This happens automatically via CSS.Copy rule ID for heading.margin-collapse
content
- Heading text should succinctly describe the content that follows itHeadings are used by screen reader users to decide whether to read or skip a section. Vague headings reduce this usefulness.Copy rule ID for heading.concise-label
- Heading text should not end with a full stopCopy rule ID for heading.no-terminal-punctuation
Heading1
Also accepts all props from HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
Heading2
Also accepts all props from HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
Heading3
Also accepts all props from HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
Heading4
Also accepts all props from HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
Styles
heading1
font-family: ${fonts.display};
line-height: ${lineHeights.level1};
font-weight: ${fontWeights.bold};
color: ${colors.heading};
font-size: ${responsiveFontSizes.level1[0]};
margin-top: ${space[6]};
margin-bottom: ${space[1]};
${mediaQueries.smallAndUp} {
font-size: ${responsiveFontSizes.level1[1]};
margin-top: ${space[9]};
margin-bottom: ${space[2]};
}
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
heading2
font-family: ${fonts.display};
line-height: ${lineHeights.level2};
font-weight: ${fontWeights.bold};
color: ${colors.heading};
font-size: ${responsiveFontSizes.level2[0]};
margin-top: ${space[4]};
margin-bottom: ${space[1]};
${mediaQueries.smallAndUp} {
font-size: ${responsiveFontSizes.level2[1]};
margin-top: ${space[6]};
margin-bottom: ${space[2]};
}
${Heading1} + &,
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
heading3
font-family: ${fonts.display};
line-height: ${lineHeights.level3};
font-weight: ${fontWeights.bold};
color: ${colors.heading};
font-size: ${responsiveFontSizes.level3[0]};
margin-top: ${space[3]};
margin-bottom: ${space[1]};
${mediaQueries.smallAndUp} {
font-size: ${responsiveFontSizes.level3[1]};
margin-top: ${space[4]};
margin-bottom: ${space[2]};
}
${Heading2} + &,
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
heading4
font-family: ${fonts.display};
line-height: ${lineHeights.level4};
font-weight: ${fontWeights.bold};
color: ${colors.heading};
font-size: ${responsiveFontSizes.level4[0]};
margin-top: ${space[2]};
margin-bottom: ${space[1]};
${mediaQueries.smallAndUp} {
font-size: ${responsiveFontSizes.level4[1]};
margin-top: ${space[3]};
margin-bottom: ${space[2]};
}
${Heading3} + &,
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
In brief
Heading is a native h1 to h4. A screen reader announces it as a heading with its level and lets the user jump between headings, so the outline is the consumer's to get right.
What Nebula provides
- Heading1 to Heading4 render h1 to h4. There is no Heading5 or Heading6.
- The as prop changes the element and keeps the style, so a level and a size can differ.
- A heading is never a tab stop and has no role beyond its element.
Accessible design
Write headings that sum up the content that follows
A screen reader user often reads only the headings to decide where to go. A heading such as "Details" says nothing; "Direct Debit details" does.
WCAG: 2.4.6 Headings and Labels
Mark the level of every heading in the design
A size does not say which level a heading is. Annotate the outline (h1, h2, h3) next to the design, so the developer does not choose the level from the size.
WCAG: 1.3.1 Info and Relationships
Accessible development
Choose the component for the level, and use as to match the size
Pick the element that fits the outline of the page. When the design needs a different size, keep that level and change the style, for example <Heading2 as="h3"> for the look of a level 2 heading at level 3.
WCAG: 1.3.1 Info and Relationships
Avoid skipping a level
A jump from h2 to h4 suggests a missing section to a user who navigates by heading.
WCAG: 1.3.1 Info and Relationships
Don't use a heading to make text bigger or bolder
A heading that is not a heading adds a stop to the outline that leads nowhere. Use P and Strong for emphasis.
WCAG: 1.3.1 Info and Relationships
Accessibility testing status
Reviewed on 9 October 2026 against v17.0.1.
| Test | Status | Tested with |
|---|---|---|
| Default state | Tested | axe-core via the Storybook test runner (CI) |
| Advanced states | Partially tested | axe-core via the Storybook test runner (CI) |
| Keyboard navigation | Manually tested | keyboard (reported by the owner) |
| Screen reader | Manually tested | VoiceOver with Chrome (reported by the owner) |
What these tests and statuses mean
The table records which checks were run on this component on its own. It is not a statement of WCAG conformance, and it does not cover how the component is used.
The tests
- Default state: The component as it first appears, before anything is clicked or changed.
- Advanced states: Every other state and variant: error, open, selected, each size and style, and hovering.
- Keyboard navigation: Whether every function can be reached and used with the keyboard alone, with a visible focus ring.
- Screen reader: Whether a screen reader announces what is on screen correctly: the name, the role and any state.
The statuses
- Tested: Every check in this row passed in an automated run that repeats on every pull request.
- Manually tested: Every check in this row was run once, by a person or with a tool they started, and all of them passed. It is not re-run on every change.
- Partially tested: Some checks in this row were run once and passed; the rest are still to do.
- Issue found: A check in this row failed. The ticket is shown beside the status.
- Not tested: No check in this row has been run yet.
- Not applicable: This row does not apply to the component, for the reason given beside it.