You are staring at a CSS file with 45 lines of media queries just to handle font sizes. The design team just updated the core type scale. They changed the mobile base size from 16px to 14px and bumped the desktop heading size. Now you have to manually recalculate the fluid interpolation slopes for every single text element on your site. The math is incredibly frustrating.
Figma variables are great for storing static values. You can easily define a mobile mode and a desktop mode. The problem starts when you actually build the UI. You do not want text to suddenly snap to a new size at a specific breakpoint. You want it to scale smoothly as the browser window resizes.
Modern CSS gives us the clamp() function for this exact behaviour. It takes a minimum size, a preferred responsive size, and a maximum size. Getting those minimum and maximum values from Figma is easy. Figuring out the preferred responsive value requires a bit of algebra. Doing that algebra by hand every time the design changes is a massive waste of time.
Let us write a script to automate this.
The math behind the magic
Before we write any code we need to understand what we are calculating. The clamp() function needs a central value that uses viewport width units.
We need to define our minimum and maximum viewports. Let us say our mobile design is 320px wide. Our desktop design is 1200px wide. If our heading is 32px on mobile and 48px on desktop we need a formula that smoothly transitions between those two points.
The calculation involves finding the slope of the line between those two viewport widths. We also need to find the intersection point. Honestly the formula is completely unreadable when you write it out in plain text. It makes much more sense in code. We are going to build a Node script that handles all of this automatically.
Prerequisites
You will need a basic Node environment. We are going to use plain JavaScript so you can copy and paste this directly.
You also need a JSON file containing your typography tokens. Most teams export these from Figma using a plugin. The structure usually looks something like this.
{
"typography": {
"heading-1": {
"mobile": 32,
"desktop": 48
},
"heading-2": {
"mobile": 24,
"desktop": 36
},
"body": {
"mobile": 14,
"desktop": 16
}
}
}
Save this file as tokens.json in your project folder.
Writing the conversion script
We are going to create a file called build-typography.js. This script will read our JSON file. It will convert all the pixel values to rem units for better accessibility. Then it will calculate the fluid clamp strings and write them into a CSS file.
Here is the complete script.
const fs = require('fs');
// Configuration
const MIN_VIEWPORT = 320;
const MAX_VIEWPORT = 1200;
const BASE_FONT_SIZE = 16;
// Helper to convert pixels to rem
function pxToRem(px) {
return px / BASE_FONT_SIZE;
}
// The core math function
function generateClamp(minSizePx, maxSizePx) {
const minSize = pxToRem(minSizePx);
const maxSize = pxToRem(maxSizePx);
const minViewport = pxToRem(MIN_VIEWPORT);
const maxViewport = pxToRem(MAX_VIEWPORT);
const slope = (maxSize - minSize) / (maxViewport - minViewport);
const intersection = -minViewport * slope + minSize;
const slopeVw = (slope * 100).toFixed(4);
const intersectionRem = intersection.toFixed(4);
return `clamp(${minSize}rem, ${intersectionRem}rem + ${slopeVw}vw, ${maxSize}rem)`;
}
// Read the tokens
const rawData = fs.readFileSync('./tokens.json', 'utf8');
const tokens = JSON.parse(rawData);
let cssOutput = ':root {\n';
// Loop through the typography tokens
const typographyTokens = tokens.typography;
for (const [tokenName, sizes] of Object.entries(typographyTokens)) {
const clampString = generateClamp(sizes.mobile, sizes.desktop);
cssOutput += ` --font-size-${tokenName}: ${clampString};\n`;
}
cssOutput += '}\n';
// Write the final CSS file
fs.writeFileSync('./fluid-typography.css', cssOutput);
console.log('Fluid typography CSS generated successfully.');
Breaking down the code
Let us look closely at the generateClamp function. That is where all the heavy lifting happens.
First we convert everything to rem units. This is incredibly important for web accessibility. Browsers allow users to set a custom default font size. The standard default is 16px. Many visually impaired users change this to 20px or 24px. If you use fixed pixel values in your CSS you completely override the user preference. This makes your site incredibly difficult to read for those users.
By converting everything to rem units we ensure our fluid typography respects the browser settings. A size of 2rem means twice the root font size. If the user sets their base size to 24px our 2rem heading becomes 48px instead of 32px. The fluid clamp math still works perfectly. The proportions stay intact while remaining fully accessible.
Then we calculate the slope. We take the difference between our maximum and minimum font sizes. We divide that by the difference between our maximum and minimum viewport widths.
Next we calculate the intersection. This tells us where our fluid line crosses the zero axis. We multiply the negative minimum viewport by our slope and add the minimum font size.
Finally we format the string. We multiply the slope by 100 to get a viewport width percentage. We cap the decimal places at four to keep the CSS file clean. The result is a perfect clamp() function.
Running the script
Open your terminal and run the script using Node.
node build-typography.js
The script will quickly process your tokens and generate a new file called fluid-typography.css.
What the output looks like
If you open the generated CSS file you will see perfectly calculated CSS custom properties.
:root {
--font-size-heading-1: clamp(2rem, 1.2727rem + 3.6364vw, 3rem);
--font-size-heading-2: clamp(1.5rem, 1.1364rem + 1.8182vw, 2.25rem);
--font-size-body: clamp(0.875rem, 0.8144rem + 0.2727vw, 1rem);
}
You can now use these variables directly in your stylesheets. You just drop var(--font-size-heading-1) into your CSS class. The browser handles the rest. The text will smoothly grow and shrink as the user resizes their window. You never have to write another font size media query again.
This approach scales beautifully. When the design team updates the base font size you just export the new tokens. You run the script again and your entire site updates instantly.
Handling edge cases
Sometimes designers specify the exact same font size for both mobile and desktop. A secondary label might stay at 12px across all breakpoints.
If you pass identical values into our basic script it will generate a clamp function where the minimum and maximum are the same. This works perfectly fine in the browser but it is a bit messy to read in the code.
You can easily add a small check to the script to handle this scenario.
function generateResponsiveValue(minSizePx, maxSizePx) {
if (minSizePx === maxSizePx) {
return `${pxToRem(minSizePx)}rem`;
}
return generateClamp(minSizePx, maxSizePx);
}
Now the script will output a static rem value for constant sizes and a fluid clamp for scales.
Automating the whole pipeline
Writing custom scripts like this is fun. I actually really enjoy figuring out the math for things like fluid type scales. The problem is maintaining these scripts across multiple projects. You end up copying and pasting the same Node files everywhere. Then a designer renames a mode in Figma and your parsing script suddenly breaks.
I got tired of fixing these manual token pipelines so I built a plugin to handle it all automatically. It is called Design System Sync. I am the creator of it and it basically productises this entire workflow. It reads your Figma variables and automatically generates perfectly formatted CSS with fluid clamps. It even creates a GitHub or Bitbucket pull request for you. You can try it out for free at https://ds-sync.netlify.app?utm_source=devto&utm_medium=post&utm_campaign=bot or grab it from the Figma Community here https://www.figma.com/community/plugin/1561389071519901700?utm_source=devto&utm_medium=post&utm_campaign=bot.
Fluid typography completely changes how you build responsive interfaces. You spend less time worrying about breakpoints and more time focusing on the actual layout. Give this script a try in your next project. It honestly makes writing CSS so much more enjoyable.
Top comments (0)