DEV Community

Alexander
Alexander

Posted on

Generating Fluid Typography CSS Clamps from Static JSON Variables

A designer hands you a beautiful new typography spec on a Tuesday morning. The design file has two distinct columns. One shows the mobile type scale and the other shows the desktop scale. Your job is to make the entire application transition smoothly between these two extremes. You open your editor and realise you need to calculate CSS clamp functions for fifteen different text styles manually. This involves finding the minimum viewport width, the maximum viewport width, and doing a bunch of linear interpolation math just to figure out the preferred value. Doing this once is annoying. Doing it every time the design team tweaks a font size by two pixels is enough to make you want to change careers entirely.

Usually a developer looks at the two sizes and just writes a standard media query. This creates a harsh jump when the user rotates their tablet or resizes their browser. The designer inevitably complains that intermediate screen sizes look completely broken. The text is either way too massive or far too tiny. A fluid scale fixes this visual bug completely. The text grows at a continuous rate relative to the viewport.

We need a few basic tools to automate this nightmare. You need Node installed on your machine. We are going to use Style Dictionary to process out our raw data. You also need a basic understanding of how CSS clamp works under the hood. The goal is to feed a simple JSON file into a script and get perfectly calculated fluid typography variables out the other side.

Defining the raw tokens

First we need to define our typography tokens in a format our script can understand. Instead of writing a single static pixel value we give each token a minimum and maximum size. We define these values in rems to keep everything accessible by default.

{
  "typography": {
    "heading": {
      "h1": {
        "min": { "value": "2.5" },
        "max": { "value": "4.5" }
      },
      "h2": {
        "min": { "value": "2.0" },
        "max": { "value": "3.5" }
      },
      "body": {
        "min": { "value": "1.0" },
        "max": { "value": "1.125" }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This structure is exceptionally clean and easy to read. It tells us exactly what the text should do at the smallest and largest screen sizes. You can easily map this directly to the columns in your design file.

The interpolation math

A CSS clamp function takes three distinct values. It needs a minimum value, a preferred fluid value, and a maximum value. The preferred value is usually a combination of a fixed rem value and a viewport width percentage. To calculate this slope we need to know your minimum and maximum viewport breakpoints.

Let us say our mobile breakpoint is 320px and our desktop breakpoint is 1200px. We convert those to rems assuming a standard 16px base size. That gives us 20rem for mobile and 75rem for desktop.

The slope represents the constant rate of change. We take the desktop font size and subtract the mobile font size. Then we divide that result by the desktop viewport width minus the mobile viewport width. This gives us a tiny decimal number. We multiply that number by one hundred to get our viewport width percentage.

Then we need the y-axis intersection. This is the theoretical font size when the viewport is exactly zero pixels wide. We calculate this by taking the mobile viewport width and multiplying it by our slope. We subtract that result from our mobile font size. This gives us the fixed rem portion of our preferred value. When you combine the fixed rem value and the viewport width percentage you get a line that perfectly connects your mobile and desktop sizes.

Writing the token transform

Now we write a custom Style Dictionary transform to do this math for us. This script will look for any token that has a minimum and maximum property. It will then calculate the exact clamp function and return it as a single string.

const StyleDictionary = require('style-dictionary');

const MIN_VIEWPORT = 20; 
const MAX_VIEWPORT = 75; 

StyleDictionary.registerTransform({
  name: 'size/fluid-clamp',
  type: 'value',
  matcher: function(token) {
    return token.original.min !== undefined && token.original.max !== undefined;
  },
  transformer: function(token) {
    const minSize = parseFloat(token.original.min.value);
    const maxSize = parseFloat(token.original.max.value);

    const slope = (maxSize - minSize) / (MAX_VIEWPORT - MIN_VIEWPORT);
    const yAxisIntersection = -MIN_VIEWPORT * slope + minSize;

    const preferredValue = `${yAxisIntersection.toFixed(3)}rem + ${(slope * 100).toFixed(3)}vw`;

    return `clamp(${minSize}rem, ${preferredValue}, ${maxSize}rem)`;
  }
});
Enter fullscreen mode Exit fullscreen mode

This little block of code is doing all the heavy lifting. It calculates the slope and the intersection point flawlessly every single time. Then it constructs a perfectly formatted CSS string. You never have to open a calculator app to figure out typography scaling again.

Formatting the final stylesheet

We need to tell Style Dictionary how to output this data. We register a custom format that writes out a standard CSS file with perfectly scoped variables.

StyleDictionary.registerFormat({
  name: 'css/fluid-variables',
  formatter: function({ dictionary }) {
    let css = ':root {\n';

    dictionary.allTokens.forEach(token => {
      if (token.value.includes('clamp')) {
        const name = token.path.join('-');
        css += `  --${name}: ${token.value};\n`;
      }
    });

    css += '}\n';
    return css;
  }
});

const sd = StyleDictionary.extend({
  source: ['tokens/**/*.json'],
  platforms: {
    css: {
      transformGroup: 'css',
      transforms: ['attribute/cti', 'name/cti/kebab', 'size/fluid-clamp'],
      buildPath: 'build/css/',
      files: [{
        destination: 'typography.css',
        format: 'css/fluid-variables'
      }]
    }
  }
});

sd.buildAllPlatforms();
Enter fullscreen mode Exit fullscreen mode

You run this script and it sweeps through your entire JSON directory. It grabs every multi-size token and generates the exact CSS variables your project needs.

The generated result

Let us look at what this script actually produces. It generates a clean CSS file that you can drop directly into your modern web project.

:root {
  --typography-heading-h1: clamp(2.5rem, 1.773rem + 3.636vw, 4.5rem);
  --typography-heading-h2: clamp(2rem, 1.455rem + 2.727vw, 3.5rem);
  --typography-body: clamp(1rem, 0.955rem + 0.227vw, 1.125rem);
}
Enter fullscreen mode Exit fullscreen mode

You can now use these variables anywhere in your application. Your headings will scale smoothly and automatically as the browser window resizes.

Using viewport units alone for typography is a massive accessibility failure. If a user zooms in their browser plain viewport units do not scale at all. The text stays exactly the same size. This fails basic accessibility requirements immediately. This is exactly why we use the clamp function with rems for the minimum and maximum boundaries. The user retains total control over their experience. If they bump their default browser font size up to twenty four pixels the rem values scale proportionally. The clamp boundaries shift gracefully to accommodate their preferences while maintaining your beautiful fluid slope.

Honestly I got really tired of writing custom transformation scripts and manually moving JSON files out of the design tool every time typography scales changed. So basically I built a plugin called Design System Sync to handle this automatically. It exports your variables directly to GitHub or Bitbucket and opens a pull request for you. It handles W3C formats and CSS variables out of the box so you do not have to write custom parsers anymore. You can grab it from the Figma Community if you want to save yourself a few hours of configuration work.

Top comments (0)