at main 4.1 kB view raw
1import plugin from 'tailwindcss/plugin'; 2 3/** 4 * Utility syntax (arbitrary values): 5 * 6 * - Font-size: fluid-[minPx_maxPx@vwMinPx_vwMaxPx] e.g. fluid-[28_88@360_1200] 7 * - Line-height (optional): fluid-lh-[minMax@vwMin_vwMax] values are plain numbers (unitless), e.g. 8 * fluid-lh-[1.15_1.25@360_1200] 9 * - Letter-spacing (optional, em): fluid-ls-[minEm_maxEm@vwMin_vwMax] e.g. fluid-ls-[-0.01_-0.02@360_1200] 10 * 11 * Notes: 12 * 13 * - Assumes 16px = 1rem. 14 * - Uses linear interpolation between vwMin and vwMax. 15 * - Guards with clamp() to cap at the ends. 16 */ 17export default function fluidType() { 18 return plugin(function ({ matchUtilities }) { 19 const toNum = (s: string) => Number(s.trim()); 20 21 const parse = (raw: string) => { 22 // "28_88@360_1200" 23 const [mm, rr] = raw.split('@'); 24 if (!mm || !rr) throw new Error('Expected pattern min_max@vwMin_vwMax'); 25 const [min, max] = mm.split('_').map(toNum); 26 const [vwMin, vwMax] = rr.split('_').map(toNum); 27 return { min, max, vwMin, vwMax }; 28 }; 29 30 // font-size in px -> rem 31 matchUtilities( 32 { 33 fluid: (value: string) => { 34 const { min, max, vwMin, vwMax } = parse(value); 35 const minRem = `${min / 16}rem`; 36 const maxRem = `${max / 16}rem`; 37 const slope = max - min; // px 38 const range = vwMax - vwMin; // px 39 // clamp(min, preferred, max) 40 // preferred = min + (slope) * ((100vw - vwMin) / range) 41 return { 42 fontSize: `clamp(${minRem}, calc(${minRem} + ${slope.toFixed(6)} * ((100vw - ${vwMin}px) / ${range})), ${maxRem})`, 43 }; 44 }, 45 }, 46 { values: {}, type: 'any' }, 47 ); 48 49 // line-height (unitless) 50 matchUtilities( 51 { 52 'fluid-lh': (value: string) => { 53 const { min, max, vwMin, vwMax } = parse(value); 54 const slope = max - min; 55 const range = vwMax - vwMin; 56 return { 57 lineHeight: `clamp(${min}, calc(${min} + ${slope.toFixed(6)} * ((100vw - ${vwMin}px) / ${range})), ${max})`, 58 }; 59 }, 60 }, 61 { values: {}, type: 'any' }, 62 ); 63 64 // letter-spacing in em 65 matchUtilities( 66 { 67 'fluid-ls': (value: string) => { 68 const { min, max, vwMin, vwMax } = parse(value); 69 const slope = max - min; 70 const range = vwMax - vwMin; 71 return { 72 letterSpacing: `clamp(${min}em, calc(${min}em + ${slope.toFixed(6)}em * ((100vw - ${vwMin}px) / ${range})), ${max}em)`, 73 }; 74 }, 75 }, 76 { values: {}, type: 'any' }, 77 ); 78 79 // Typography scale presets 80 // Format: [minPx, maxPx, viewportMinPx, viewportMaxPx] 81 // Scales from mobile (360px) to desktop (1200px) viewports 82 const fsPresets: Record<string, [number, number, number, number]> = { 83 // sm: Small text (captions, footnotes, labels) 84 sm: [ 85 14, 86 16, 87 360, 88 1200, 89 ], 90 // base: default text (body copy, paragraphs) 91 base: [ 92 16, 93 32, 94 360, 95 1200, 96 ], 97 // md: Medium text (body copy, paragraphs) 98 md: [ 99 24, 100 40, 101 360, 102 1200, 103 ], 104 // lg: Large text (subheadings, lead paragraphs) 105 lg: [ 106 32, 107 48, 108 360, 109 1200, 110 ], 111 // xl: Extra large text (headings) 112 xl: [ 113 40, 114 56, 115 360, 116 1200, 117 ], 118 // 2xl: Display text (hero headings, page titles) 119 '2xl': [ 120 48, 121 64, 122 360, 123 1200, 124 ], 125 126 }; 127 128 matchUtilities( 129 { 130 'fluid-preset': (key: string) => { 131 const p = fsPresets[key]; 132 if (!p) return null; 133 const [ 134 min, 135 max, 136 vwMin, 137 vwMax, 138 ] = p; 139 return { 140 fontSize: `clamp(${min / 16}rem, calc(${min / 16}rem + ${(max - min).toFixed(6)} * ((100vw - ${vwMin}px) / ${vwMax - vwMin})), ${max / 16}rem)`, 141 }; 142 }, 143 }, 144 { values: Object.fromEntries(Object.keys(fsPresets).map((k) => [k, k])) }, 145 ); 146 }); 147}