Skip to content

Heading

SM · 950B gzip budget

Headings that know their own level, so a reusable component never breaks the page outline.

Category
Accessibility & preferences
Budget
sm, single-purpose control
External runtime
React / React DOM only
axe fixtureSSR rendersource scanbundle budget

Preview

Live fixture
Preview locale

Page title

Section inside it

Try the component here. Locale-aware formatting and direction follow the selected language; example content and labels are not automatically translated. Overlay demos use interactive launchers.

Install

npx shadcn@latest add https://gear5-ui.vercel.app/r/heading-level.json

Copies the source into your project. Pulls in 1 shared registry item: cn.

New here? Set up Tailwind and import aliases first →

Source & props

Edit on GitHub ↗

Prop types and inline documentation are included below. This is repository source; the installer rewrites shared imports for your project.

"use client";

import { createContext, useContext, useMemo } from "react";
import { cn } from "../lib/cn";

const HeadingLevelContext = createContext(1);

export interface HeadingSectionProps {
  children: React.ReactNode;
}

/**
 * Increments the heading level for everything inside it.
 *
 * Wrap a region whose headings sit one level deeper than the surrounding page.
 */
export function HeadingSection({ children }: HeadingSectionProps) {
  const level = useContext(HeadingLevelContext);
  const next = useMemo(() => Math.min(level + 1, 6), [level]);

  return <HeadingLevelContext.Provider value={next}>{children}</HeadingLevelContext.Provider>;
}

export interface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {
  children: React.ReactNode;
  className?: string;
}

/**
 * A heading that knows its own level.
 *
 * A reusable card cannot know whether it is being rendered under an `h1` or an
 * `h3`, so it guesses — and the result is a page whose heading outline skips
 * levels or restarts. That outline is the primary way screen reader users
 * navigate a page: they pull up a list of headings and jump. A broken outline
 * is a broken table of contents for exactly the people who rely on it most.
 *
 * `Heading` renders whatever level its position in the tree implies, and
 * `HeadingSection` deepens that level for its subtree — so a component is
 * correct wherever it is placed, rather than correct where it was first
 * written.
 */
export function Heading({ children, className, ...props }: HeadingProps) {
  const level = useContext(HeadingLevelContext);
  const Tag = `h${level}` as "h1" | "h2" | "h3" | "h4" | "h5" | "h6";

  return (
    <Tag {...props} className={cn("text-start font-semibold", className)}>
      {children}
    </Tag>
  );
}

/** The heading level the current subtree would render at. */
export function useHeadingLevel(): number {
  return useContext(HeadingLevelContext);
}

What CI checks

Quality contract
  • Bundled, minified and gzipped against its tier budget
  • Audited by axe in the state previewed above
  • Rendered through react-dom/server with no browser globals
  • Scanned for network calls, dangerous sinks, and unguarded animation