A semantic heading element that allows control of typographic attributes.
You might be surprised (or overjoyed 😄) to see a <Heading />
component. Allow me to explain.
Why do this:
<Heading size="6xl" weight="bold" align="center" tracking="tight">Title</Heading>
Instead of this:
<h1 className="text-6xl font-bold text-center tracking-tight">Title</h1>
Because you and your designer need a common language without adding a cognitive burnen on you.
Alignment and weight are pretty much standard at this point, but the quantity and values for the sizes and trackings aren't - that's decided by the designer.
Imagine that you use utility classes throughout your codebase and your designer decides that a certain typography element, the big titles say, should go from 60px to 48px. You have to go through the entire codebase and change all titles from text-6xl to text-5xl.
"Ok, André, but I could just create custom utility classes that use CSS variables and if the designer would need to change all titles to another size, he would just change the CSS variable value".
Yes you could, but that has two problems: you're now forced to learn about and remember to use those custom utility classes. If you forget to use them, and the designer changes the CSS variable it uses, the design will break where you've forgotten. The other problem is that there's no distinction between utility classes that are typographic attributes and one that aren't. It's all mixed together in the className string.
By using the <Heading /> component, you have a clear interface between you and the designer (or you and the design 🤭) available as props, leaving non-typographic attributes to the className=""
. This way you're not forced to memorize custom tailwind classes, while giving the designer complete control over the typography.
<Heading size="6xl">Heading 6xl</Heading><Heading size="5xl">Heading 5xl</Heading><Heading size="4xl">Heading 4xl</Heading><Heading size="3xl">Heading 3xl</Heading><Heading size="2xl">Heading 2xl</Heading><Heading size="xl">Heading xl</Heading><Heading size="lg">Heading lg</Heading><Heading size="md">Heading md</Heading><Heading size="sm">Heading sm</Heading><Heading size="xs">Heading xs</Heading>
Here's the heading component in action.
Prop | Type | Default |
---|---|---|
as | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'h1' |
size | '4xl' | '3xl' | '2xl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs' | '4xl' |
weight | 'thin' | 'extralight' | 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | 'extrabold' | 'black' | 'bold' |
align | 'left' | 'center' | 'right' | 'left' |
tracking | 'tighter', 'tight', 'normal', 'wide', 'wider', 'widest' | undefined |
The tracking's default value depends on the size prop and is controlled through CSS variables.
/* Default tracking. Can be overridden by the 'tracking' prop on the <Heading /> and <Text /> components) */--text-size-xs-tracking: 0em;--text-size-sm-tracking: 0em;--text-size-md-tracking: 0em;--text-size-lg-tracking: 0em;--text-size-xl-tracking: 0em;--text-size-2xl-tracking: -0.025em;--text-size-3xl-tracking: -0.025em;--text-size-4xl-tracking: -0.025em;--text-size-5xl-tracking: -0.05em;--text-size-6xl-tracking: -0.05em;
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'import { cva, type VariantProps } from 'class-variance-authority'import { cn } from '~/utils/tailwind-merge.ts'
const headingVariants = cva('', { variants: { size: { '6xl': 'text-size-6xl', '5xl': 'text-size-5xl', '4xl': 'text-size-4xl', '3xl': 'text-size-3xl', '2xl': 'text-size-2xl', xl: 'text-size-xl', lg: 'text-size-lg', md: 'text-size-md', sm: 'text-size-sm', xs: 'text-size-xs', }, weight: { black: 'font-black', extrabold: 'font-extrabold', bold: 'font-bold', semibold: 'font-semibold', medium: 'font-medium', normal: 'font-normal', light: 'font-light', extralight: 'font-extralight', thin: 'font-thin', }, align: { left: 'text-left', center: 'text-center', right: 'text-right', }, tracking: { tighter: 'tracking-tighter', tight: 'tracking-tight', normal: 'tracking-normal', wide: 'tracking-wide', wider: 'tracking-wider', widest: 'tracking-widest', }, },})
type HeadingType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
type HeadingProps = { as?: HeadingType } & VariantProps<typeof headingVariants> & { children: ReactNode } & HTMLAttributes<HTMLHeadingElement>
/** * A Heading component. * * @param {HeadingType} [as='h1'] - The HTML heading element type (h1, h2, h3, h4, h5, or h6). * @param {string} [weight='bold'] - The font weight of the heading (thin, extralight, light, normal, medium, semibold, bold, extrabold, or black). * @param {string} [size='4xl'] - The size variant of the heading (4xl, 3xl, 2xl, xl, lg, md, sm, xs). * @param {string} [align='left'] - The text alignment of the heading (left, center, or right). * @param {string} [tracking] - The letter spacing of the heading (tighter, tight, normal, wide, wider, or widest). */const Heading = forwardRef<HTMLHeadingElement, HeadingProps>(({ as = 'h1', size = '4xl', weight = 'bold', align = 'left', tracking, children, className, ...props }, ref) => { const Comp = as return ( <Comp ref={ref} className={cn(headingVariants({ size, weight, align, tracking }), className)} {...props}> {children} </Comp> )})Heading.displayName = 'Heading'
export { Heading }
The typographic styling is actually pretty simple once you wrap your head around it. For an explanation check out the Fluid Typography Sizing and Scales article.
--container-min-width: 20; /* 320px */--container-max-width: 96; /* 1536px */
--text-size-deltaX: calc(var(--container-max-width) - var(--container-min-width));/* 6xl */--text-size-6xl-min-font-size: 4.5; /* 👈 you can edit this value (in rem) */--text-size-6xl-max-font-size: 8; /* 👈 you can edit this value (in rem) */--text-size-6xl-deltaY: calc(var(--text-size-6xl-max-font-size) - var(--text-size-6xl-min-font-size));--text-size-6xl-gradient: calc(var(--text-size-6xl-deltaY) / var(--text-size-deltaX));--text-size-6xl-intercept: calc(var(--text-size-6xl-min-font-size) - (var(--text-size-6xl-gradient) * var(--container-min-width)));--text-size-6xl-font-size: calc(var(--text-size-6xl-gradient) * 100vw + var(--text-size-6xl-intercept) * 1rem);--text-size-6xl: clamp(calc(var(--text-size-6xl-min-font-size) * 1rem), var(--text-size-6xl-font-size), calc(var(--text-size-6xl-max-font-size) * 1rem));
/* 5xl */--text-size-5xl-min-font-size: 3; /* 👈 you can edit this value (in rem) */--text-size-5xl-max-font-size: 6; /* 👈 you can edit this value (in rem) */--text-size-5xl-deltaY: calc(var(--text-size-5xl-max-font-size) - var(--text-size-5xl-min-font-size));--text-size-5xl-gradient: calc(var(--text-size-5xl-deltaY) / var(--text-size-deltaX));--text-size-5xl-intercept: calc(var(--text-size-5xl-min-font-size) - (var(--text-size-5xl-gradient) * var(--container-min-width)));--text-size-5xl-font-size: calc(var(--text-size-5xl-gradient) * 100vw + var(--text-size-5xl-intercept) * 1rem);--text-size-5xl: clamp(calc(var(--text-size-5xl-min-font-size) * 1rem), var(--text-size-5xl-font-size), calc(var(--text-size-5xl-max-font-size) * 1rem));
/* 4xl */--text-size-4xl-min-font-size: 2.25; /* 👈 you can edit this value (in rem) */--text-size-4xl-max-font-size: 3.75; /* 👈 you can edit this value (in rem) */--text-size-4xl-deltaY: calc(var(--text-size-4xl-max-font-size) - var(--text-size-4xl-min-font-size));--text-size-4xl-gradient: calc(var(--text-size-4xl-deltaY) / var(--text-size-deltaX));--text-size-4xl-intercept: calc(var(--text-size-4xl-min-font-size) - (var(--text-size-4xl-gradient) * var(--container-min-width)));--text-size-4xl-font-size: calc(var(--text-size-4xl-gradient) * 100vw + var(--text-size-4xl-intercept) * 1rem);--text-size-4xl: clamp(calc(var(--text-size-4xl-min-font-size) * 1rem), var(--text-size-4xl-font-size), calc(var(--text-size-4xl-max-font-size) * 1rem));
/* 3xl */--text-size-3xl-min-font-size: 1.875; /* 👈 you can edit this value (in rem) */--text-size-3xl-max-font-size: 2.25; /* 👈 you can edit this value (in rem) */--text-size-3xl-deltaY: calc(var(--text-size-3xl-max-font-size) - var(--text-size-3xl-min-font-size));--text-size-3xl-gradient: calc(var(--text-size-3xl-deltaY) / var(--text-size-deltaX));--text-size-3xl-intercept: calc(var(--text-size-3xl-min-font-size) - (var(--text-size-3xl-gradient) * var(--container-min-width)));--text-size-3xl-font-size: calc(var(--text-size-3xl-gradient) * 100vw + var(--text-size-3xl-intercept) * 1rem);--text-size-3xl: clamp(calc(var(--text-size-3xl-min-font-size) * 1rem), var(--text-size-3xl-font-size), calc(var(--text-size-3xl-max-font-size) * 1rem));
/* 2xl */--text-size-2xl-min-font-size: 1.5; /* 👈 you can edit this value (in rem) */--text-size-2xl-max-font-size: 1.875; /* 👈 you can edit this value (in rem) */--text-size-2xl-deltaY: calc(var(--text-size-2xl-max-font-size) - var(--text-size-2xl-min-font-size));--text-size-2xl-gradient: calc(var(--text-size-2xl-deltaY) / var(--text-size-deltaX));--text-size-2xl-intercept: calc(var(--text-size-2xl-min-font-size) - (var(--text-size-2xl-gradient) * var(--container-min-width)));--text-size-2xl-font-size: calc(var(--text-size-2xl-gradient) * 100vw + var(--text-size-2xl-intercept) * 1rem);--text-size-2xl: clamp(calc(var(--text-size-2xl-min-font-size) * 1rem), var(--text-size-2xl-font-size), calc(var(--text-size-2xl-max-font-size) * 1rem));
/* xl */--text-size-xl-min-font-size: 1.25; /* 👈 you can edit this value (in rem) */--text-size-xl-max-font-size: 1.5; /* 👈 you can edit this value (in rem) */--text-size-xl-deltaY: calc(var(--text-size-xl-max-font-size) - var(--text-size-xl-min-font-size));--text-size-xl-gradient: calc(var(--text-size-xl-deltaY) / var(--text-size-deltaX));--text-size-xl-intercept: calc(var(--text-size-xl-min-font-size) - (var(--text-size-xl-gradient) * var(--container-min-width)));--text-size-xl-font-size: calc(var(--text-size-xl-gradient) * 100vw + var(--text-size-xl-intercept) * 1rem);--text-size-xl: clamp(calc(var(--text-size-xl-min-font-size) * 1rem), var(--text-size-xl-font-size), calc(var(--text-size-xl-max-font-size) * 1rem));
/* lg */--text-size-lg-min-font-size: 1.125; /* 👈 you can edit this value (in rem) */--text-size-lg-max-font-size: 1.25; /* 👈 you can edit this value (in rem) */--text-size-lg-deltaY: calc(var(--text-size-lg-max-font-size) - var(--text-size-lg-min-font-size));--text-size-lg-gradient: calc(var(--text-size-lg-deltaY) / var(--text-size-deltaX));--text-size-lg-intercept: calc(var(--text-size-lg-min-font-size) - (var(--text-size-lg-gradient) * var(--container-min-width)));--text-size-lg-font-size: calc(var(--text-size-lg-gradient) * 100vw + var(--text-size-lg-intercept) * 1rem);--text-size-lg: clamp(calc(var(--text-size-lg-min-font-size) * 1rem), var(--text-size-lg-font-size), calc(var(--text-size-lg-max-font-size) * 1rem));
/* md */--text-size-md-min-font-size: 1; /* 👈 you can edit this value (in rem) */--text-size-md-max-font-size: 1.125; /* 👈 you can edit this value (in rem) */--text-size-md-deltaY: calc(var(--text-size-md-max-font-size) - var(--text-size-md-min-font-size));--text-size-md-gradient: calc(var(--text-size-md-deltaY) / var(--text-size-deltaX));--text-size-md-intercept: calc(var(--text-size-md-min-font-size) - (var(--text-size-md-gradient) * var(--container-min-width)));--text-size-md-font-size: calc(var(--text-size-md-gradient) * 100vw + var(--text-size-md-intercept) * 1rem);--text-size-md: clamp(calc(var(--text-size-md-min-font-size) * 1rem), var(--text-size-md-font-size), calc(var(--text-size-md-max-font-size) * 1rem));
/* sm */--text-size-sm-min-font-size: 0.875; /* 👈 you can edit this value (in rem) */--text-size-sm-max-font-size: 1; /* 👈 you can edit this value (in rem) */--text-size-sm-deltaY: calc(var(--text-size-sm-max-font-size) - var(--text-size-sm-min-font-size));--text-size-sm-gradient: calc(var(--text-size-sm-deltaY) / var(--text-size-deltaX));--text-size-sm-intercept: calc(var(--text-size-sm-min-font-size) - (var(--text-size-sm-gradient) * var(--container-min-width)));--text-size-sm-font-size: calc(var(--text-size-sm-gradient) * 100vw + var(--text-size-sm-intercept) * 1rem);--text-size-sm: clamp(calc(var(--text-size-sm-min-font-size) * 1rem), var(--text-size-sm-font-size), calc(var(--text-size-sm-max-font-size) * 1rem));
/* xs */--text-size-xs-min-font-size: 0.75; /* 👈 you can edit this value (in rem) */--text-size-xs-max-font-size: 0.875; /* 👈 you can edit this value (in rem) */--text-size-xs-deltaY: calc(var(--text-size-xs-max-font-size) - var(--text-size-xs-min-font-size));--text-size-xs-gradient: calc(var(--text-size-xs-deltaY) / var(--text-size-deltaX));--text-size-xs-intercept: calc(var(--text-size-xs-min-font-size) - (var(--text-size-xs-gradient) * var(--container-min-width)));--text-size-xs-font-size: calc(var(--text-size-xs-gradient) * 100vw + var(--text-size-xs-intercept) * 1rem);--text-size-xs: clamp(calc(var(--text-size-xs-min-font-size) * 1rem), var(--text-size-xs-font-size), calc(var(--text-size-xs-max-font-size) * 1rem));
/* Line height (this depends on paragraph length) */--text-size-xs-line-height: clamp(1.125rem, 6.2vw, 1.75rem); /* 12px <-> 14px => 18px <-> 28px */--text-size-sm-line-height: clamp(1.25rem, 6.2vw, 2rem); /* 20px <-> 32px */--text-size-md-line-height: clamp(1.5rem, 6vw, 2.25rem); /* 24px <-> 36px */--text-size-lg-line-height: clamp(1.75rem, 5.8vw, 2.25rem); /* 28px <-> 36px */--text-size-xl-line-height: clamp(2rem, 6.5vw, 2.5rem); /* 32px <-> 40px */--text-size-2xl-line-height: 1.5;--text-size-3xl-line-height: 1.5;--text-size-4xl-line-height: 1.2;--text-size-5xl-line-height: 1.2;--text-size-6xl-line-height: 1.2;
/* Default tracking. Can be overridden by the 'tracking' prop on the <Heading /> and <Text /> components) */--text-size-xs-tracking: 0em;--text-size-sm-tracking: 0em;--text-size-md-tracking: 0em;--text-size-lg-tracking: 0em;--text-size-xl-tracking: 0em;--text-size-2xl-tracking: -0.025em;--text-size-3xl-tracking: -0.025em;--text-size-4xl-tracking: -0.025em;--text-size-5xl-tracking: -0.05em;--text-size-6xl-tracking: -0.05em;