A semantic text element that allows control of typographic attributes.
You might be surprised (or overjoyed 😄) to see a <Text />
component. Allow me to explain.
Why do this:
<Text size="6xl" weight="bold" align="center" tracking="tight">Title</Text>
Instead of this:
<p className="text-6xl font-bold text-center tracking-tight">Title</p>
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 paragraphs 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 paragraphs 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 <Text /> 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.
<Text size="6xl">Text 6xl</Text><Text size="5xl">Text 5xl</Text><Text size="4xl">Text 4xl</Text><Text size="3xl">Text 3xl</Text><Text size="2xl">Text 2xl</Text><Text size="xl">Text xl</Text><Text size="lg">Text lg</Text><Text size="md">Text md</Text><Text size="sm">Text sm</Text><Text size="xs">Text xs</Text>
Here's the <Text /> component in action.
Text 6xl
Text 5xl
Text 4xl
Text 3xl
Text 2xl
Text xl
Text lg
Text md
Text sm
Text xs
Prop | Type | Default |
---|---|---|
as | 'p' | 'span' | 'div' | 'p' |
size | '4xl' | '3xl' | '2xl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs' | 'md' |
weight | 'thin' | 'extralight' | 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | 'extrabold' | 'black' | 'normal' |
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 textVariants = 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 TextType = 'p' | 'span' | 'div'
type TextProps = { as?: TextType } & VariantProps<typeof textVariants> & { children: ReactNode } & HTMLAttributes<HTMLParagraphElement>
/** * A text component. * * @param {HeadingType} [as='p'] - The HTML text element type (p, span, or div). * @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 Text = forwardRef<HTMLParagraphElement, TextProps>(({ as = 'p', size = 'md', weight = 'normal', align = 'left', tracking, children, className, ...props }, ref) => { const Comp = as return ( <Comp ref={ref} className={cn(textVariants({ size, weight, align, tracking }), className)} {...props}> {children} </Comp> )})Text.displayName = 'Text'
export { Text, textVariants }
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;