Skip to content
v2.5.0
English
Русский
Documentation

Heading

Heading is the semantic heading primitive.

Aa

heading.tsx · 3 lines
import { Heading } from '@vega-ui/react';

<Heading as='h2' size={6} fontWeight={700}>Aa</Heading>
Browser support
Widely available
  • Chrome 111
  • Edge 111
  • Firefox 113
  • Safari 15.4
  • Safari on iOS 15.4
Bundle size
Tested in
11 tests

Summary

Heading is the semantic heading primitive. It separates heading level from visual size so document structure and typography scale can be managed independently.

Imports

import { Heading, type HeadingAs, type HeadingProps, type HeadingSize } from '@vega-ui/react';

Exported Types

  • HeadingProps
  • HeadingAs = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
  • HeadingSize = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | number

Basic Usage

basic-usage.tsx · 10 lines
import { Heading, Text } from '@vega-ui/react';

export default function BasicUsage() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-4)', width: '100%' }}>
      <Heading as='h2'>Workspace settings</Heading>
      <Text size={3} style={{ color: 'var(--label-secondary)' }}>Manage the name, members and billing of the Acme workspace.</Text>
    </div>
  );
}

Variants

  • Level: h1 through h6
  • Size: visual scale 1 through 11
  • Weight: 400, 500, 600, 700, 900

Edge Cases

  • Visual size and semantic level are independent.
  • Large heading sizes still inherit heading line-height tokens rather than body line-height tokens.
  • as should follow document outline, not visual taste alone.

Common Mistakes

  • Picking heading level by size instead of document structure.
  • Using Heading for form labels.
  • Mixing arbitrary custom font sizes with the heading scale.

Examples

Basic

Basic: document outline

Choose as from the outline first. Without size, each level gets its default visual size (h1 → 11, h2 → 10, h3 → 8), so a plain outline needs no extra props.

document-outline.tsx · 20 lines
import { Heading, Text } from '@vega-ui/react';

export default function DocumentOutline() {
  return (
    <article style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: '60ch' }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Heading as='h1'>Getting started</Heading>
        <Text size={3} style={{ color: 'var(--label-secondary)' }}>Set up your workspace in three steps.</Text>
      </div>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Heading as='h2'>Invite your team</Heading>
        <Text size={3}>Members get access to every public project.</Text>

Controlled/Stateful

Controlled/Stateful: count in a section heading

The heading shows the current member count and labels the section through aria-labelledby, so the region name updates with the list. Each remove button gets a name that includes the member, and the empty state replaces the list.

heading-with-dynamic-count.tsx · 47 lines
import { Button, Heading, Text } from '@vega-ui/react';
import { useState } from 'react';

const candidates = ['Alicia Gómez', 'Ben Carter', 'Chen Wei', 'Dana Novak', 'Emeka Obi'];

export default function HeadingWithDynamicCount() {
  const [members, setMembers] = useState(candidates.slice(0, 2));
  const nextCandidate = candidates.find((name) => !members.includes(name));

  const addMember = () => {
    if (nextCandidate === undefined) return;
    setMembers([...members, nextCandidate]);

Form/Integration

Form/Integration: heading that names a form

The form takes its accessible name from the Heading through aria-labelledby, which makes it a named form landmark. Groups inside use FieldsetLegend, not more headings, and the submit reads the values with FormData.

fieldset-section-title.tsx · 53 lines
import {
  Button,
  Fieldset,
  FieldsetLegend,
  Heading,
  Label,
  Text,
  TextField,
  TextFieldInput,
} from '@vega-ui/react';
import { useState, type FormEvent } from 'react';

Layout/Overlay

Layout/Overlay: hierarchy inside a dialog

The dialog title is an h2 linked with aria-labelledby, and its sections use h3, so the dialog has its own outline under the page. The close button sits in the corner with logical insets.

dialog-title-hierarchy.tsx · 56 lines
import {
  Button,
  Dialog,
  DialogClose,
  DialogContent,
  DialogTrigger,
  Heading,
  Icon,
  IconButton,
  Text,
} from '@vega-ui/react';
import { XIcon } from '@vega-ui/icons';

Error

Error: focused error summary

On a failed submit an error summary appears above the form and its Heading receives focus through tabIndex={-1}, so screen readers announce the problem count right away. Each message links to its field, and the fields are marked with aria-invalid.

error-summary-heading.tsx · 87 lines
import { Button, Heading, Label, Text, TextField, TextFieldInput } from '@vega-ui/react';
import { useEffect, useRef, useState, type CSSProperties, type FormEvent } from 'react';

interface FieldError {
  id: string
  message: string
}

export default function ErrorSummaryHeading() {
  const summaryHeading = useRef<HTMLHeadingElement>(null);
  const [errors, setErrors] = useState<FieldError[]>([]);
  const [attempt, setAttempt] = useState(0);

Disabled

Disabled: heading of a read-only section

The section stays an h3 with its level intact; only the color is muted. The controls are disabled through a native fieldset, which is labelled by the heading and described by the note that explains why.

muted-configuration-heading.tsx · 36 lines
import { Button, Heading, Label, Text, TextField, TextFieldInput } from '@vega-ui/react';
import type { CSSProperties } from 'react';

export default function MutedConfigurationHeading() {
  return (
    <section aria-labelledby='legacy-webhooks-title' style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%' }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-2)' }}>
          <Heading
          as='h3'
          size={4}
          id='legacy-webhooks-title'
          style={{ color: 'var(--disable-text-color)' }}

Edge

Edge: small visual size, correct level

Sidebar section titles are h2 in the outline but use size={2}: as and size are independent, so a dense layout does not force a wrong level.

visual-size-smaller-than-semantic-level.tsx · 21 lines
import { Heading, Text } from '@vega-ui/react';

const sections = [
  { title: 'Recent activity', items: ['Invoice #4821 paid', 'Dana joined the workspace'] },
  { title: 'Pinned projects', items: ['Website redesign', 'Mobile onboarding'] },
];

export default function VisualSizeSmallerThanSemanticLevel() {
  return (
    <aside aria-label='Sidebar' style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 240 }}>
      {sections.map((section) => (
        <section key={section.title} style={{ display: 'grid', gap: 'var(--spacing-3)' }}>

Edge: long titles in a narrow column

Heading wraps like normal text. textWrap: 'balance' evens out the lines of a long title, and overflowWrap: 'anywhere' lets a long compound word break instead of overflowing the column.

long-heading-wrap.tsx · 14 lines
import { Heading } from '@vega-ui/react';

export default function LongHeadingWrap() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%' }}>
      <Heading as='h3' size={6} style={{ textWrap: 'balance' }}>
        Quarterly revenue report for the Europe, Middle East and Africa region
      </Heading>
      <Heading as='h3' size={6} style={{ overflowWrap: 'anywhere' }}>
        Datenschutzgrundverordnungskonformität
      </Heading>
    </div>

Edge: level from nesting depth

A recursive component computes as from its depth and clamps it at h6, because HTML has no deeper level. The visual size shrinks with depth separately from the level.

nested-heading-levels.tsx · 54 lines
import { Heading, Text, type HeadingAs } from '@vega-ui/react';

interface Topic {
  title: string
  children?: Topic[]
}

const guide: Topic = {
  title: 'Admin guide',
  children: [
    {
      title: 'Security',

Anatomy

Overview

Heading is a single-part primitive. The root owns semantic heading level, visual size, and weight.

Required Parts

Heading

Required. Renders the heading element and applies VegaUI heading typography.

Optional Parts

There are no exported child parts.

Composition Order

  1. Heading
  2. heading content

Valid Composition Patterns

The heading carries an id, and the section it titles references it with aria-labelledby, so the region gets the same name as the visible title.

valid-composition-patterns.tsx · 10 lines
import { Heading, Text } from '@vega-ui/react';

export default function ValidCompositionPatterns() {
  return (
    <section aria-labelledby='billing-contacts-title' style={{ display: 'grid', gap: 'var(--spacing-2)', width: '100%' }}>
      <Heading as='h3' size={5} id='billing-contacts-title'>Billing contacts</Heading>
      <Text size={2} style={{ color: 'var(--label-secondary)' }}>Invoices are sent to finance@acme.io and the workspace owner.</Text>
    </section>
  );
}

Invalid Composition Patterns

Heading level chosen only for visual size

This breaks document structure.

Body text styled to mimic section structure

This hides navigational semantics from assistive technology.