Text

A semantic text element that allows control of typographic attributes.

Features

  • Allows control of size, weight, alignment, and tracking.
  • Clear separation of concerns between semantics and design.

Why

You might be surprised (or overjoyed 😄) to see a <Text /> component. Allow me to explain.

Why do this:

SomeComponent.tsx
  1. 1
<Text size="6xl" weight="bold" align="center" tracking="tight">Title</Text>

Instead of this:

SomeComponent.tsx
  1. 1
<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.

Usage

TextExample.tsx
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
<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

Props

PropTypeDefault
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.

tailwind.css
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
/* 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;

Source

text.tsx
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
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 }

Styling

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.

tailwind.css
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
  74. 74
  75. 75
  76. 76
  77. 77
  78. 78
  79. 79
  80. 80
  81. 81
  82. 82
  83. 83
  84. 84
  85. 85
  86. 86
  87. 87
  88. 88
  89. 89
  90. 90
  91. 91
  92. 92
  93. 93
  94. 94
  95. 95
  96. 96
  97. 97
  98. 98
  99. 99
  100. 100
  101. 101
  102. 102
  103. 103
  104. 104
  105. 105
  106. 106
  107. 107
  108. 108
  109. 109
  110. 110
  111. 111
  112. 112
  113. 113
  114. 114
  115. 115
  116. 116
  117. 117
--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;