docs / patterns / section-heading
SectionHeading
the opener that carries a section: a mono uppercase eyebrow over a large, fluid title.
intensity · structuresrsc · server
provenance
shaped from personal-websitelayer
core / patternsintensity
structures · organizes a region, stays out of the contentcomposition
opens a page section, one per sectionthe eyebrow names the beat: the problem, outcome, approachnot a card title. inside Card use CardHeadernever picked by size. as follows the document outlinea11y
a real heading element,h2 by default · the eyebrow is a plain paragraph, so the accessible name is the title alonejest-axelive demo · try it out
The problem
Students could not see their whole degree.
Outcome
One planner, four systems reconciled.
No eyebrow, just the heading.
props
| prop | type | default | notes |
|---|---|---|---|
| eyebrow | ReactNode | — | mono uppercase kicker above the title. omitted entirely when absent. |
| title | ReactNode | — | the display title. |
| as | "h1" | "h2" | "h3" | "h2" | the heading element. the type scale never changes with it. |
| tone | "accent" | "accent-alt" | "accent-alt" | colors the eyebrow. |
get it
npx shadcn add https://usva.build/r/section-heading.jsonsource · components/ui/section-heading.tsxexactly what this command copies
import * as React from "react";
import { cn } from "@/lib/utils";
export type SectionHeadingTone = "accent" | "accent-alt";
export type SectionHeadingLevel = "h1" | "h2" | "h3";
const eyebrowTone: Record<SectionHeadingTone, string> = {
accent: "text-accent",
"accent-alt": "text-accent-alt",
};
export interface SectionHeadingProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
eyebrow?: React.ReactNode;
title: React.ReactNode;
as?: SectionHeadingLevel;
tone?: SectionHeadingTone;
}
/**
* A display heading: mono uppercase eyebrow over a large fluid title.
*
* Not SectionLabel. That one is built around a numbered mono index with an
* optional lede and stays at body scale. This is the section opener that
* carries the page, and the two do not collapse into one component without
* making both worse.
*/
export const SectionHeading = React.forwardRef<
HTMLDivElement,
SectionHeadingProps
>(
(
{
className,
eyebrow,
title,
as: Tag = "h2",
tone = "accent-alt",
...props
},
ref,
) => (
<div ref={ref} className={cn("mb-10", className)} {...props}>
{eyebrow != null && (
<p
className={cn(
"mb-3 font-mono text-xs uppercase tracking-[0.18em]",
eyebrowTone[tone],
)}
>
{eyebrow}
</p>
)}
<Tag className="max-w-3xl text-balance text-[clamp(1.75rem,4vw,3rem)] font-bold leading-[1.15] tracking-[-0.02em] text-ink">
{title}
</Tag>
</div>
),
);
SectionHeading.displayName = "SectionHeading";