Loading…
Loading…
Typography component for section titles and content headings.
The Heading component renders typographic section titles (<h1> through <h6>) with consistent styling, semantic correctness, and fluid scaling. Headings are the single most important structural element in any document — they create the outline that sighted users scan visually and screen-reader users navigate programmatically.
Getting headings right is deceptively difficult. The tension between visual hierarchy (how big and bold a heading looks) and semantic hierarchy (its level in the document outline) is the root cause of most heading-related accessibility failures. A Heading component resolves this by decoupling visual appearance from semantic level — you can render an <h3> that looks like an <h1> when the design requires visual prominence but the document outline doesn't.
When to use a Heading:
<h1> — exactly one per page)<h2> through <h6>)When NOT to use a Heading:
Heading font sizes should scale fluidly across viewport widths using CSS clamp(). Design your fluid type scale with the Clamp Calculator, choose your typeface with the Font Explorer, and verify heading-to-background contrast with the Contrast Checker.
| Level | HTML Element | Typical Use |
|---|---|---|
| h1 | <h1> | Page title. Exactly one per page. |
| h2 | <h2> | Major sections. Most common heading level. |
| h3 | <h3> | Subsections within an h2 block. |
| h4 | <h4> | Sub-subsections. Card titles, sidebar sections. |
| h5 | <h5> | Deep nesting. Rarely needed in well-structured content. |
| h6 | <h6> | Deepest level. Often styled identically to body bold. |
| Size | Font Size (Desktop) | Fluid Clamp | Use Case |
|---|---|---|---|
| Display XL | 60–72px | clamp(2.5rem, 5vw + 1rem, 4.5rem) | Hero headlines, landing pages |
| Display | 48–56px | clamp(2rem, 4vw + 0.75rem, 3.5rem) | Marketing sections |
| H1 | 36–40px | clamp(1.75rem, 3vw + 0.5rem, 2.5rem) | Page titles |
| H2 | 28–32px | clamp(1.5rem, 2.5vw + 0.25rem, 2rem) | Section headings |
| H3 | 22–24px | clamp(1.25rem, 2vw + 0.25rem, 1.5rem) | Subsection headings |
| H4 | 18–20px | clamp(1.125rem, 1.5vw + 0.25rem, 1.25rem) | Card titles |
| H5 | 16px | 1rem | Small headings |
| H6 | 14px | 0.875rem | Overlines, eyebrows |
Generate precise clamp() values with the Clamp Calculator. The formula clamp(min, preferred, max) ensures headings scale smoothly from mobile (320px) to desktop (1440px) without media query breakpoints.
| Weight | Value | Use Case |
|---|---|---|
| Bold | 700 | Default for h1–h3. Strong visual hierarchy. |
| Semibold | 600 | h4–h6. Slightly softer than bold. |
| Medium | 500 | Subtle headings, overlines. |
| Regular | 400 | Display sizes where the size alone provides hierarchy. |
background-clip: text) for marketing/hero contextstext-overflow: ellipsis for constrained layouts| Property | Type | Default | Description |
|---|---|---|---|
level | 1 | 2 | 3 | 4 | 5 | 6 | 2 | Semantic heading level. Determines the HTML element (<h1>–<h6>). |
size | 'display-xl' | 'display' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | Matches level | Visual size variant. Allows decoupling from semantic level. |
weight | 'regular' | 'medium' | 'semibold' | 'bold' | 'bold' | Font weight. |
color | string | 'inherit' | Text color. Accepts design tokens or CSS values. |
align | 'left' | 'center' | 'right' | 'left' | Text alignment. |
truncate | boolean | false | Truncates to a single line with ellipsis. |
maxLines | number | — | Clamps text to N lines with -webkit-line-clamp. |
as | ElementType | Determined by level | Override the rendered element (e.g., render an <h3> semantically but as a <span> visually). |
id | string | — | Element ID. Critical for anchor links and aria-labelledby references. |
className | string | — | Additional CSS classes. |
The level vs size distinction is the key design decision. Setting level={3} renders an <h3> element; setting size="h1" makes it look like an h1. This decoupling is essential when a component (e.g., a Card title) always uses <h3> semantically but may appear in different visual contexts.
| Token | Role | Typical Value |
|---|---|---|
--heading-font-family | Heading typeface | var(--font-family-display) or var(--font-family-sans) |
--heading-font-weight | Default weight | 700 |
--heading-color | Default heading color | var(--color-text-primary) |
--heading-line-height | Line height | 1.2 (tight for large sizes) to 1.4 (for h5–h6) |
--heading-letter-spacing | Tracking | -0.02em (large sizes) to 0 (small sizes) |
--heading-margin-top | Space above heading | 0 (controlled by parent layout) |
--heading-margin-bottom | Space below heading | var(--space-3, 0.75rem) |
--heading-h1-size | H1 font size | clamp(1.75rem, 3vw + 0.5rem, 2.5rem) |
--heading-h2-size | H2 font size | clamp(1.5rem, 2.5vw + 0.25rem, 2rem) |
--heading-h3-size | H3 font size | clamp(1.25rem, 2vw + 0.25rem, 1.5rem) |
--heading-h4-size | H4 font size | clamp(1.125rem, 1.5vw + 0.25rem, 1.25rem) |
--heading-h5-size | H5 font size | 1rem |
--heading-h6-size | H6 font size | 0.875rem |
--heading-display-xl-size | Display XL | clamp(2.5rem, 5vw + 1rem, 4.5rem) |
--heading-display-size | Display | clamp(2rem, 4vw + 0.75rem, 3.5rem) |
Choose your heading typeface with the Font Explorer. For fluid sizes, generate clamp() values with the Clamp Calculator. A common pattern: use a display/serif font for h1 and a sans-serif for h2–h6, or maintain a single family and rely on size/weight for hierarchy.
Headings are primarily static content elements, so their "states" are contextual rather than interactive:
| State | Visual Treatment | Context |
|---|---|---|
| Default | Rendered at specified size, weight, and color. | Normal content display. |
| Anchor Link Target | Scroll target with optional highlight animation (flash background or underline). | When a user navigates to #section-id, the heading briefly highlights. Use scroll-margin-top to offset fixed headers. |
| Editable | Cursor changes to text; heading becomes an inline editable field. | CMS interfaces, title editing in productivity apps. |
| Skeleton / Loading | Replaced by a skeleton placeholder (rectangle at heading width/height). | Content loading state. See Skeleton. |
| Truncated | Text cut off with ellipsis. Full text accessible via title attribute or Tooltip. | Constrained layouts (card titles, sidebar headings). |
| Error / Invalid | Red color or error icon (rare). | Form section headings in validation error state. |
When using anchor links with fixed/sticky navigation bars, headings need scroll-margin-top to prevent them from hiding behind the fixed element:
[id] {
scroll-margin-top: calc(var(--navbar-height, 64px) + 1rem);
}
This ensures that clicking a table-of-contents link scrolls the heading into view below the fixed nav, not behind it.
Headings are the primary mechanism screen-reader users rely on for page navigation. A well-structured heading hierarchy is arguably the single highest-impact accessibility improvement you can make.
Semantic Hierarchy (WCAG 1.3.1 Info and Relationships):
<h1>, representing the page's primary topic.<h1> → <h2> → <h3>. Never skip levels (e.g., <h2> → <h4>).size prop to decouple appearance from semantics. A <Heading level={3} size="h1"> renders an <h3> that looks like an h1.Heading-Level Audit: Run a heading-level audit on every page. Tools: axe DevTools, WAVE, or the HeadingsMap browser extension. Common violations:
<h1> elements (often: site logo in <h1> plus page title in <h1>)<h1> then <h3> because h2 "looked too big")<div class="title"> instead of <h2>)Color Contrast (WCAG 1.4.3 Contrast Minimum):
background-clip: text) must maintain contrast across the entire gradient range, not just at one point.Landmark Labeling: Headings frequently serve as labels for landmarks. For example:
<section aria-labelledby="features-heading">
<h2 id="features-heading">Features</h2>
…
</section>
This connects the heading to its section landmark, allowing screen readers to announce "Features, region" when navigating by landmark (WCAG 1.3.1).
Reading Order: Ensure headings appear in the DOM in the correct reading order, not just visually via CSS positioning. Screen readers follow DOM order, not visual order (WCAG 1.3.2 Meaningful Sequence).
Do:
<h1> per page, matching the page's primary topiclevel={3} size="h1")clamp() for responsive scaling — configure via the Clamp Calculatorid attributes to headings that serve as anchor link targetsscroll-margin-top to offset fixed headers when using anchor linksline-height (1.1–1.2) for large headings and looser (1.3–1.4) for small headingsletter-spacing (-0.01em to -0.03em) for large display sizesDon't:
size prop insteadTypography Pairing: A common pattern is pairing a display/serif font for h1/h2 with a sans-serif for h3–h6. This creates visual distinction without relying solely on size. The Font Explorer helps you preview pairings. Ensure both fonts share similar x-heights for visual harmony.
<!-- Heading – Semantic HTML with Fluid Typography -->
<h1 class="heading heading-h1">Page Title</h1>
<h2 class="heading heading-h2">Section Title</h2>
<h3 class="heading heading-h3">Subsection Title</h3>
<!-- Heading with anchor link target -->
<h2 class="heading heading-h2" id="features">Features</h2>
<!-- Display variant on a semantic h1 -->
<h1 class="heading heading-display-xl">Launch Day</h1>
<!-- Heading with overline -->
<div class="heading-group">
<span class="heading-overline">Chapter 3</span>
<h2 class="heading heading-h2">The Journey Begins</h2>
</div>
<style>
.heading {
font-family: var(--heading-font-family, system-ui, sans-serif);
font-weight: var(--heading-font-weight, 700);
color: var(--heading-color, #111827);
margin-top: 0;
margin-bottom: var(--heading-margin-bottom, 0.75rem);
}
.heading-display-xl {
font-size: clamp(2.5rem, 5vw + 1rem, 4.5rem);
line-height: 1.1;
letter-spacing: -0.03em;
}
.heading-display {
font-size: clamp(2rem, 4vw + 0.75rem, 3.5rem);
line-height: 1.1;
letter-spacing: -0.02em;
}
.heading-h1 {
font-size: var(--heading-h1-size, clamp(1.75rem, 3vw + 0.5rem, 2.5rem));
line-height: 1.2;
letter-spacing: -0.02em;
}
.heading-h2 {
font-size: var(--heading-h2-size, clamp(1.5rem, 2.5vw + 0.25rem, 2rem));
line-height: 1.25;
letter-spacing: -0.01em;
}
.heading-h3 {
font-size: var(--heading-h3-size, clamp(1.25rem, 2vw + 0.25rem, 1.5rem));
line-height: 1.3;
}
.heading-h4 {
font-size: var(--heading-h4-size, clamp(1.125rem, 1.5vw + 0.25rem, 1.25rem));
line-height: 1.35;
}
.heading-h5 { font-size: var(--heading-h5-size, 1rem); line-height: 1.4; }
.heading-h6 { font-size: var(--heading-h6-size, 0.875rem); line-height: 1.4; text-transform: uppercase; letter-spacing: 0.05em; }
.heading-overline {
display: block;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--color-brand, #2563eb);
margin-bottom: 0.25rem;
}
/* Anchor link scroll offset */
[id] {
scroll-margin-top: calc(var(--navbar-height, 64px) + 1rem);
}
/* Truncation */
.heading-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>interface HeadingProps {
level?: 1 | 2 | 3 | 4 | 5 | 6;
size?: "display-xl" | "display" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
weight?: "regular" | "medium" | "semibold" | "bold";
align?: "left" | "center" | "right";
color?: string;
truncate?: boolean;
maxLines?: number;
id?: string;
className?: string;
children: React.ReactNode;
}
function Heading({
level = 2,
size,
weight = "bold",
align = "left",
color,
truncate = false,
maxLines,
id,
className,
children,
}: HeadingProps) {
const Tag = `h${level}` as keyof JSX.IntrinsicElements;
const visualSize = size || `h${level}`;
const weightMap = { regular: 400, medium: 500, semibold: 600, bold: 700 };
const style: React.CSSProperties = {
fontFamily: "var(--heading-font-family, system-ui, sans-serif)",
fontWeight: weightMap[weight],
color: color || "var(--heading-color, #111827)",
textAlign: align,
marginTop: 0,
marginBottom: "var(--heading-margin-bottom, 0.75rem)",
...(truncate && {
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap" as const,
}),
...(maxLines && {
display: "-webkit-box",
WebkitLineClamp: maxLines,
WebkitBoxOrient: "vertical" as const,
overflow: "hidden",
}),
};
return (
<Tag id={id} className={`heading heading-${visualSize} ${className || ""}`} style={style}>
{children}
</Tag>
);
}
// Usage: Semantic h3 styled as h1
<Heading level={3} size="h1">
Card Title That Looks Big
</Heading>
// Usage: Display heading for hero
<Heading level={1} size="display-xl" weight="bold" align="center">
Design Without Limits
</Heading>
// Usage: Truncated heading
<Heading level={4} truncate>
This very long heading will be cut off with an ellipsis when it overflows its container
</Heading>
// Usage: Heading with overline
<div>
<span className="heading-overline">Case Study</span>
<Heading level={2} size="h1">Redesigning the Dashboard</Heading>
</div>Material Design (MUI) uses a <Typography> component with variant prop for both headings and body text. Heading variants: h1 through h6, plus subtitle1, subtitle2. The component prop decouples the rendered element from the visual variant — <Typography variant="h1" component="h3"> renders an <h3> with h1 styling. MUI's default type scale uses Roboto with sizes from 6rem (h1) down to 1.25rem (h6), with decreasing letter-spacing at larger sizes.
Ant Design provides heading via <Typography.Title> with a level prop (1–5). Sizes are predefined and follow Ant's 14px base scale. The component supports copyable, editable, ellipsis (with configurable rows and expandable toggle), and type for semantic coloring (secondary, success, warning, danger). No built-in decoupling of visual size from semantic level.
Chakra UI offers a <Heading> component with as prop for semantic level and size prop (4xl, 3xl, 2xl, xl, lg, md, sm, xs) for visual size. This cleanly decouples semantics from appearance. Chakra's heading inherits from Box, supporting all style props. Sizes use responsive array syntax: size={["lg", "xl", "2xl"]} for viewport-based scaling.
Bootstrap styles headings via .h1–.h6 classes (applicable to any element) and .display-1–.display-6 for larger display headings. This CSS-class approach inherently decouples visual style from semantics. Bootstrap 5 uses $font-size-base * multiplier for heading sizes, with responsive scaling via $enable-rfs (Responsive Font Sizes) which applies calc() scaling similar to clamp().
Apple Human Interface Guidelines specifies Dynamic Type styles: Large Title, Title 1–3, Headline, Subheadline. These map to semantic roles rather than HTML levels. In SwiftUI, .font(.largeTitle) etc. iOS heading styles automatically respond to the user's preferred text size (accessibility setting), scaling from the default 34pt (Large Title) to significantly larger or smaller based on the Dynamic Type slider.
Tailwind CSS provides heading sizing through utility classes: text-4xl, text-3xl, etc. Combined with font-bold, tracking-tight, and leading-tight, developers compose heading styles directly. Tailwind doesn't provide a heading component — it's utility-first, leaving component abstraction to the developer or to component libraries like Shadcn/ui that use <h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">.