DEV Community

Alexander
Alexander

Posted on

Generating fluid CSS clamps from raw Figma typography tokens

Your designer hands you a new typography scale. It looks beautiful on their massive studio monitor. You check the mobile designs and the font sizes are completely different. The design file just gives you fixed pixel values for desktop and fixed pixel values for mobile. You now have to write media queries for every single heading level. This is tedious and honestly prone to mistakes.

We can fix this by converting those static pixel values into fluid CSS clamps automatically. You feed in the raw JSON from Figma. You run a script. You get perfect fluid typography that scales smoothly between mobile and desktop viewports.

This means no more media queries for font sizes. Your type just adapts automatically.

Prerequisites

You need Node installed on your machine. You also need a basic project folder with a package.json file. We will use Style Dictionary to process our tokens. It is a fantastic tool for transforming raw data into usable code.

Create a new folder and initialize your project.

npm init -y
npm install style-dictionary
Enter fullscreen mode Exit fullscreen mode

The raw token data

Figma usually exports tokens as JSON. A good typography token structure for fluid type needs a minimum size and a maximum size. Let us look at a simple example for a heading scale.

Create a file called tokens.json in your project root.

{
  "typography": {
    "heading": {
      "large": {
        "min": { "value": "32", "type": "typography" },
        "max": { "value": "64", "type": "typography" }
      },
      "medium": {
        "min": { "value": "24", "type": "typography" },
        "max": { "value": "48", "type": "typography" }
      },
      "small": {
        "min": { "value": "18", "type": "typography" },
        "max": { "value": "32", "type": "typography" }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice that we store raw numbers here. These represent pixels. We will convert them to rem units in our transformation script to ensure they respect the browser default font size settings.

The fluid typography math

Before we write the script we need to understand how clamp() works. The CSS clamp() function takes three values. It needs a minimum value, a preferred fluid value, and a maximum value.

The preferred value is the tricky part. We calculate it using the viewport width. We want the font size to start scaling up at our minimum viewport width and stop scaling at our maximum viewport width.

Let us assume our mobile viewport starts at 320px. Our desktop viewport stops at 1200px. We convert everything to rem units assuming a base of 16px.

Minimum viewport is 20rem. Maximum viewport is 75rem.

The formula for the preferred value involves finding the slope of the scale. We subtract the minimum font size from the maximum font size. We divide that by the difference between the maximum and minimum viewport widths. We then multiply that slope by the viewport width unit.

It sounds complicated but we can write a JavaScript function to handle the math for us.

Writing the custom transform

We need to tell Style Dictionary how to read our JSON and spit out CSS clamps. We do this by creating a custom build script.

Create a file named build.js in your project folder. We will define a custom transform that looks for our typography tokens and applies the fluid math.

const StyleDictionary = require('style-dictionary')

const minViewport = 20
const maxViewport = 75

function calculateClamp(minPx, maxPx) {
  const minRem = minPx / 16
  const maxRem = maxPx / 16

  const slope = (maxRem - minRem) / (maxViewport - minViewport)
  const intersection = -1 * minViewport * slope + minRem

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

  return `clamp(${minRem}rem, ${preferredValue}, ${maxRem}rem)`
}

StyleDictionary.registerTransform({
  name: 'typography/fluid',
  type: 'value',
  matcher: function(token) {
    return token.type === 'typography' && token.path.includes('max') === false
  },
  transformer: function(token, dictionary) {
    const path = token.path.slice(0, -1)
    const tokenGroup = dictionary.tokens[path[0]][path[1]][path[2]]

    const minPx = parseFloat(tokenGroup.min.value)
    const maxPx = parseFloat(tokenGroup.max.value)

    return calculateClamp(minPx, maxPx)
  }
})
Enter fullscreen mode Exit fullscreen mode

This script registers a new transform. The matcher function makes sure we only process the min token. We ignore the max token so we do not generate duplicate CSS variables. The transformer function grabs both the minimum and maximum pixel values from the token group. It then passes them into our math function and returns the final clamp() string.

Configuring the build process

Now we need to configure Style Dictionary to use our new transform. We add this configuration directly to our build.js file.

const config = {
  source: ['tokens.json'],
  platforms: {
    css: {
      transformGroup: 'css',
      transforms: ['attribute/cti', 'name/cti/kebab', 'typography/fluid'],
      buildPath: 'build/css/',
      files: [{
        destination: 'variables.css',
        format: 'css/variables',
        filter: function(token) {
          return token.path.includes('max') === false
        }
      }]
    }
  }
}

const sd = StyleDictionary.extend(config)
sd.buildAllPlatforms()
Enter fullscreen mode Exit fullscreen mode

We tell Style Dictionary to read from tokens.json. We set up a CSS platform. We include our custom typography/fluid transform in the transforms array. We also add a filter to the file output. This filter ensures that the max tokens do not get printed as separate CSS variables. We only want one CSS variable per heading level.

Run your build script from the terminal.

node build.js
Enter fullscreen mode Exit fullscreen mode

The final output

Check your project folder. You will see a new build/css directory containing a variables.css file. Open it up and look at the generated code.

:root {
  --typography-heading-large-min: clamp(2rem, 0.836rem + 5.818vw, 4rem);
  --typography-heading-medium-min: clamp(1.5rem, 0.627rem + 4.364vw, 3rem);
  --typography-heading-small-min: clamp(1.125rem, 0.416rem + 3.545vw, 2rem);
}
Enter fullscreen mode Exit fullscreen mode

You now have perfect fluid typography. The large heading will be exactly 32px on small screens. It will scale up smoothly as the screen gets wider. It will stop scaling when it reaches exactly 64px on large monitors. Everything is converted to rem units so it remains accessible.

You can drop these variables straight into your stylesheets.

h1 {
  font-size: var(--typography-heading-large-min);
}

h2 {
  font-size: var(--typography-heading-medium-min);
}
Enter fullscreen mode Exit fullscreen mode

This approach saves hundreds of lines of CSS. You never have to write another min-width media query just to bump up a font size. Your components become much cleaner and easier to maintain. You also guarantee that the code exactly matches the design intent without manual calculations.

I honestly got so tired of manually copying token values and running these build scripts for every single project. Setting up the pipelines always took away hours of development time. So basically I built a plugin called Design System Sync to handle this automatically. It exports your Figma tokens directly to GitHub or Bitbucket via pull requests. It handles all the W3C formats and even does the CSS variable generation for you. You can check out the Design System Sync website or grab it directly from the Figma Community if you want to skip the manual setup entirely.

Top comments (0)