Skip to content

Heading

SM, under 950B gzipped

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

Category
Accessibility & preferences
Budget
sm, single-purpose control
Runtime deps
None

Preview

Preview language

Page title

Section inside it

This is the exact fixture the conformance suite renders in CI. Switch the language to see the component mirror and reformat.

Install

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

Copies the source into your project. Pulls in 1 primitive: cn.

Source

"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

← All components