# Heading Semantic heading elements for structuring page and section content Section: components Docs: https://nebula.kaluza.com/components/heading --- ## 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](/components/text) (`P`) instead. - **For decorative large text that carries no structural meaning** - use [Text](/components/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 1. **Heading text** ## Basic usage Use the numbered heading component (`Heading1` through `Heading4`) that matches the correct semantic level in the document outline. ```tsx <> Heading 1 Heading 2 Heading 3 Heading 4 ``` ## 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. ```tsx <> Your account Billing Payment method Direct Debit details ``` ### 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: ```tsx Section title ``` 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. ## Rules and guidance ### Usage - **Heading** MUST be chosen based on its semantic position in the document outline, not its visual size. Rationale: Screen readers and assistive technology use heading levels to allow users to navigate page structure. Choosing a heading for its appearance breaks this navigation. [id: heading.semantic-level; testability: manual] - **Heading** MUST be used instead of raw h1–h4 HTML elements in application code. Rationale: Nebula heading components apply responsive font sizes and spacing from the design token system. Raw elements bypass these. [id: heading.token-system; testability: auto] - **Heading levels** MUST NOT skip levels in the document outline (e.g. jumping from Heading1 to Heading3). Rationale: Skipping levels creates a broken document outline that is confusing for screen reader users. [id: heading.no-skip-levels; testability: auto] - **Heading1** SHOULD appear no more than once per page. Rationale: Multiple h1 elements make the page purpose ambiguous for assistive technology and search engines. [id: heading.one-h1-per-page; testability: auto] - **Heading** SHOULD use the as prop when the correct semantic level and the required visual size differ. Rationale: Using the wrong heading component to achieve a visual size breaks document structure. The as prop lets both be correct independently. [id: heading.use-as-prop-for-visual-mismatch; testability: manual] - **Heading** SHOULD NOT have its color overridden outside the theme. Rationale: Heading color is set to colors.heading, which contrasts correctly against background tokens. Overriding it may break contrast ratios. [id: heading.color-not-overridden; testability: manual] ### Placement - **Heading** WILL collapse its top margin automatically when it is the first child or immediately follows a heading of the level above. Rationale: Heading components collapse their top margin in these positions to avoid double spacing. This happens automatically via CSS. [id: heading.margin-collapse; testability: auto] ### Content - **Heading text** SHOULD succinctly describe the content that follows it. Rationale: Headings are used by screen reader users to decide whether to read or skip a section. Vague headings reduce this usefulness. [id: heading.concise-label; testability: manual] - **Heading text** SHOULD NOT end with a full stop. [id: heading.no-terminal-punctuation; testability: auto] ## Accessibility ### 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](https://www.w3.org/WAI/WCAG22/Understanding/headings-and-labels.html) #### 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](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html) ### 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 `` for the look of a level 2 heading at level 3. WCAG: [1.3.1 Info and Relationships](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html) #### 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](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html) #### 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](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html) ### 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 each test covers: - 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. What these statuses mean: - 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. 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. ## Properties ### Heading1 Also accepts all props from `HTMLAttributes`. - `as` (type `ElementType`, optional). Renders the component as a different element or component, e.g. `as="section"`. - `forwardedAs` (type `ElementType`, optional). 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`. - `as` (type `ElementType`, optional). Renders the component as a different element or component, e.g. `as="section"`. - `forwardedAs` (type `ElementType`, optional). 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`. - `as` (type `ElementType`, optional). Renders the component as a different element or component, e.g. `as="section"`. - `forwardedAs` (type `ElementType`, optional). 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`. - `as` (type `ElementType`, optional). Renders the component as a different element or component, e.g. `as="section"`. - `forwardedAs` (type `ElementType`, optional). 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 Reference only; you do not need this to use the component. This is the CSS its styled elements use, with theme tokens shown by name. `${...}` is an interpolation (a theme token, a mixin or another value from the code). A `&[prop]` or `@when ` block holds CSS that applies only under that condition; these are notation, not valid CSS. ### heading1 ```css 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 ```css 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 ```css 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 ```css 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; } ```