Aa
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
- Oklab and OkLCh — comes from the design tokens
- :where()
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
HeadingPropsHeadingAs = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'HeadingSize = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | number
Basic Usage
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:
h1throughh6 - Size: visual scale
1through11 - 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.
asshould follow document outline, not visual taste alone.
Common Mistakes
- Picking heading level by size instead of document structure.
- Using
Headingfor 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.
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.
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.
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.
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.
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.
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.
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.
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.
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
Heading- 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.
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.