Heading

A section title that establishes document structure for readers and assistive technology.

Basic usage

Install the package:

pnpm add @design-schema/react @design-schema/tokens

Import the stylesheet once for the app, then the component:

import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { Heading } from '@design-schema/react';

Examples

Default

  • Account settings

    Default

    33px / 2.0625rem · 39px / 2.4375rem · font.size.3xl

  • Not rendered here: a grid shows every tile at once, and this example’s heading would skip a level after the one above it. Its source stands in — the level it names is the point, and the page outline cannot demonstrate it and stay unbroken at the same time.

    Subsection Sized Up

    23px / 1.4375rem · 25px / 1.5625rem · font.size.xl

level

  • Not rendered here: this example renders a level-1 heading, and a page has one of those — the title at the top of this one. Its source stands in, and it says the same thing either way: level 1 really does produce an <h1>, which is the whole reason this page cannot embed it.

    level="1"

    40px / 2.5rem · 49px / 3.0625rem · font.size.4xl

  • Account settings

    level="2"

    33px / 2.0625rem · 39px / 2.4375rem · font.size.3xl

  • Account settings

    level="3"

    28px / 1.75rem · 31px / 1.9375rem · font.size.2xl

  • Account settings

    level="4"

    23px / 1.4375rem · 25px / 1.5625rem · font.size.xl

  • Account settings
    level="5"

    19px / 1.1875rem · 20px / 1.25rem · font.size.lg

  • Account settings
    level="6"

    16px / 1rem · font.size.md

  • Not rendered here: this example renders a level-1 heading, and a page has one of those — the title at the top of this one. Its source stands in, and it says the same thing either way: level 1 really does produce an <h1>, which is the whole reason this page cannot embed it.

    level="1"

    40px / 2.5rem · 49px / 3.0625rem · font.size.4xl

  • Billing

    level="2"

    33px / 2.0625rem · 39px / 2.4375rem · font.size.3xl

size

  • Account settings

    size="4xl"

    40px / 2.5rem · 49px / 3.0625rem · font.size.4xl

  • Account settings

    size="3xl"

    33px / 2.0625rem · 39px / 2.4375rem · font.size.3xl

  • Account settings

    size="2xl"

    28px / 1.75rem · 31px / 1.9375rem · font.size.2xl

  • Account settings

    size="xl"

    23px / 1.4375rem · 25px / 1.5625rem · font.size.xl

  • Account settings

    size="lg"

    19px / 1.1875rem · 20px / 1.25rem · font.size.lg

  • Account settings

    size="md"

    16px / 1rem · font.size.md

align

  • Account settings

    align="start"
  • Account settings

    align="center"
  • Account settings

    align="end"

Props

4 rows
PropTypeDefaultDescriptionPlatforms
level required1 | 2 | 3 | 4 | 5 | 6—

Position in the document outline. Controls the semantic element, not the visual size. Canonical values are strings; generated components also accept the number — the exported `HeadingLevel` type is the string union, and the prop or property type adds `1 | 2 | 3 | 4 | 5 | 6`. A missing, out-of-range or non-numeric level (untyped JavaScript, `level="7"`) is treated as `2` on every platform: an <h2> on web and Lit, the 3xl default size everywhere, and one development warning per element for its lifetime. The warning is developer-facing, not copy, so it has no copy key; it names the received value and the level-2 fallback (`Heading: level 7 is not one of 1–6; rendering as level 2.`), printing the value as `String(level)`, so a missing level reads `level undefined`. The warning is issued once per element whatever later values arrive (a later, different invalid value does not warn again), and on web from an effect behind a ref guard rather than during render, so StrictMode cannot send it twice. Behavior scenarios take only canonical values, so each platform's own test file covers a numeric level and the fallback with its single warning; neither gets a story. On Lit the property holds `undefined` until set, and the fallback renders. The Default story renders level `2` with "Account settings" and no other args.

Accessibility: Screen-reader users navigate by heading level; levels must not skip (h1 → h3).

web, lit, rn, swiftui
size4xl | 3xl | 2xl | xl | lg | md—

Visual size, independent of level. There is no single default; the default is read from `level` by this exact map — 1 → 4xl, 2 → 3xl, 3 → 2xl, 4 → xl, 5 → lg, 6 → md — and an explicit `size` always wins over it. On Lit the resolved default is never written back to the `size` attribute, so `[size]` selectors match only explicit sizes; web exposes no size attribute at all, only the `ds-heading--size-*` modifier class, which carries the resolved size. Heading takes the large end of the shared size vocabulary; the exported type is its own, since Text takes the small end. Enum stories keep the literal `<Prop><Value>` casing: `Size4xl`, `Size2xl`, `Level1`.

web, lit, rn, swiftui
children requiredcontent—

The heading text. Keep it short and descriptive; it is what appears in the page outline.

web, lit, rn, swiftui
alignstart | center | endstart

Horizontal text alignment. It has no style binding on purpose — alignment is a layout choice, not a themed value — so it maps straight to the platform's text-align. `start` and `end` are logical on web and Lit; React Native has no logical values and resolves them through I18nManager.isRTL at render, so a writing-direction change mid-session does not re-align an already-rendered heading, the same limit Text has. The value set is Text's, so every platform reuses Text's exported `TextAlign` type and exports no separate HeadingAlign, with no deprecated alias; Text exports no mapping helper on web or Lit, so Heading keeps its own `ds-heading--align-*` classes there and its own `:host([align])` selectors on Lit. On web every value, `start` included, emits its `ds-heading--align-<value>` class.

web, lit, rn, swiftui

Accessibility

Decisions this component leaves to you

Choose the heading level so it follows the heading above it on your page. The component will not guess the outline it lands in.

Required props that carry accessibility meaning: level. Each is described in the props table above.

Implicit ARIA role: heading.

Anatomy

text

What generation guarantees

Heading hierarchy — Headings this component renders take their level from the caller, so the page outline never skips a level.

Contrast — Text contrast meets WCAG AAA; the pairs are listed below.

Contrast pairs

1 row
ForegroundBackgroundLevel
color.foreground.strongcolor.backgroundAAA

This component does not animate, so it has no reduced-motion behaviour to describe.