FabUnit: A Smart Way To Control And Synchronize Typo And Space

FabUnit: A Smart Way To Control And Synchronize Typo And Space

What if we were able to implement the sizes of designer’s contribution without hassle? What if we could set custom anchor points to generate a perfectly responsive value, giving us more options as the fluid size approach? What if we had a magic formula that controlled and synchronized the whole project?

It is often the case that I receive two templates from the designer: One for mobile and one for desktop. Lately, I have been asking myself how I could automate the process and optimize the result. How do I implement the specified sizes most effectively? How do I ensure a comfortable tablet view? How can I optimize the output for large screens? How can I react to extreme aspect ratios?

I would like to be able to read the two values of the different sizes (font, spaces, etc.) in pixel and enter them as arguments into a function that does all the work for me. I want to create my own responsive magic formula, my FabUnit.

When I started working on this topic in the spring and launched the FabUnit, I came across this interesting article by Adrian. In the meantime, Ruslan and Brecht have also researched in this direction and presented interesting thoughts.

How Can I Implement The Design Templates Most Effectively?

I am tired of writing media queries for every value, and I want to avoid design jumps. The maintenance and the result are not satisfying. So what is the best way to implement the designer’s contribution?

What about fluid sizes? There are handy calculators like Utopia or Min-Max-Calculator.

But for my projects, I usually need more setting options. The tablet view often turns out too small, and I can neither react to larger viewports nor to the aspect ratio.

And it would be nice to have a proportional synchronization across the whole project. I can define global variables with the calculated values, but I also want to be able to generate an interpolated value locally in the components without any effort. I would like to draw my own responsive line. So I need a tool that spits out the perfect value based on several anchor points (screen definitions) and automates the processes for most of my projects. My tool must be fast and easy to use, and the code must be readable and maintainable.

What Constants From The Design Specifications Should Our Calculations Be Based On?

Let’s take a closer look at our previous example:

The body font size should be 16px on mobile and 22px on desktop (we will deal with the complete style guide later on). The mobile sizes should start at 375px and continuously adjust to 1024px. Up to 1440px, the sizes are to remain statically at the optimum. After that, the values should scale linearly up to 2000px, after which the max-wrapper takes effect.

This gives us the following constants that apply to the whole project:

Xf    375px        global        screen-min        
Xa    1024px        global        screen-opt-start
Xb    1440px        global        screen-opt-end
Xu    2000px        global        screen-max

The body font size should be at least 16px, ideally 22px. The maximum font size at 2000px should be calculated automatically:

Yf    16px        local        size-min        
Ya    22px
Yb    22px        local        size-opt
Yu    auto

So, at the end of the day, my function should be able to take two arguments — in this case, 16 and 22.

fab-unit(16, 22);
The Calculation

If you are not interested in the mathematical derivation of the formula, feel free to jump directly to the section “How To Use The FabUnit?”.

So, let’s get started!

Define Main Clamps

First, we need to figure out which main clamps we want to set.

clamp1: clamp(Yf, slope1, Ya)
clamp2: clamp(Yb, slope2, Yu)

Combine And Nest The Clamps

Now we have to combine the two clamps. This might get a little tricky. We have to consider that the two lines, slope1 and slope2, can overwrite each other, depending on how steep they are. Since we know that slope2 should be 45 degrees or 100% (m = 1), we can query whether slope1 is above 1. This way, we can set a different clamp depending on how the lines intersect.

If slope1 is steeper than slope2, we combine the clamps like this:

clamp(Yf, slope1, clamp(Yb, slope2, Yu))

If slope1 is flatter than slope2, we do this calculation:

clamp(clamp(Yf, slope1, Ya), slope2, Yu)

Combined:

steep-slope
  ? clamp(Yf, slope1, clamp(Yb, slope2, Yu))
  : clamp(clamp(Yf, slope1, Ya), slope2, Yu)

Set The Maximum Wrapper Optionally

What if we don’t have a maximum wrapper that freezes the design above a certain width?

First, we need to figure out which main clamps we want to set.

clamp1: clamp(Yf, slope1, Ya)
max: max(Yb, slope2)

If slope1 is steeper than slope2:

clamp(Yf, slope1, max(Yb, slope2))

If slope1 is flatter than slope2:

max(clamp(Yf, slope1, Ya), slope2)

The calculation without wrapper — elastic upwards:

steep-slope
  ? clamp(Yf, slope1, max(Yb, slope2))
  : max(clamp(Yf, slope1, Ya), slope2)

Combined, with optional max wrapper (if screen-max Xu is set):

Xu
  ? steep-slope
    ? clamp(Yf, slope1, clamp(Yb, slope2, Yu))
    : max(clamp(Yf, slope1, Ya), Yu)
  : steep-slope
    ? clamp(Yf, slope1, max(Yb, slope2))
    : max(clamp(Yf, slope1, Ya), slope2)

Thus we have built the basic structure of the formula. Now we dive a little deeper.

Calculate The Missing Values

Let’s see which values we get in as an argument and which we have to calculate now:

steep-slope
? clamp(Yf, slope1, clamp(Yb, slope2, Yu))
: max(clamp(Yf, slope1, Ya), Yu)

steep-slope
Ya = Yb = 22px
Yf = 16px
slope1 = Mfa
slope2 = Mbu
Yu

  • steep-slope
    Check whether the slope Yf → Ya is above the slope Yb → Yu (m = 1) :
((Ya - Yf) / (Xa - Xf)) * 100 > 1
  • Mfa
    Linear interpolation, including the calculation of the slope Yf → Ya:
Yf + (Ya - Yf) * (100vw - Xf) / (Xa - Xf)
  • Mbu
    Linear interpolation between Yb and Yu (slope m = 1):
100vw / Xb * Yb
  • Yu
    Calculating the position of Yu:
(Xu / Xb) * Yb

Put All Together

Xu
  ? ((Ya - Yf) / (Xa - Xf)) * 100 > 1
    ? clamp(Yf, Yf + (Ya - Yf) * (100vw - Xf) / (Xa - Xf), clamp(Yb, 100vw / Xb * Yb, (Xu / Xb) * Yb))
    : max(clamp(Yf, Yf + (Ya - Yf) * (100vw - Xf) / (Xa - Xf), Ya), (Xu / Xb) * Yb)
  : ((Ya - Yf) / (Xa - Xf)) * 100 > 1
    ? clamp(Yf, Yf + (Ya - Yf) * (100vw - Xf) / (Xa - Xf), max(Yb, 100vw / Xb * Yb))
    : max(clamp(Yf, Yf + (Ya - Yf) * (100vw - Xf) / (Xa - Xf), Ya), 100vw / Xb * Yb)

We’d better store some calculations in variables:

steep-slope = ((Ya - Yf) / (Xa - Xf)) * 100 > 1
slope1 = Yf + (Ya - Yf) * (100vw - Xf) / (Xa - Xf)
slope2 = 100vw / Xb * Yb
Yu = (Xu / Xb) * Yb

Xu
  ? steep-slope
    ? clamp(Yf, slope1, clamp(Yb, slope2, Yu))
    : max(clamp(Yf, slope1, Ya), Yu)
  : steep-slope
    ? clamp(Yf, slope1, max(Yb, slope2))
    : max(clamp(Yf, slope1, Ya), slope2)
Include Aspect Ratio

Because we now see how the cat jumps, we treat ourselves to another cookie. In the case of an extremely wide format, e.g. mobile device landscape, we want to scale down the sizes again. It’s more pleasant and readable this way.

So what if we could include the aspect ratio in our calculations? In this example, we want to shrink the sizes when the screen is wider than the aspect ratio of 16:9.

aspect-ratio = 16 / 9
screen-factor = min(100vw, 100vh * aspect-ratio)

In both slope interpolations, we simply replace 100vw with the new screen factor.

slope1 = Yf + (Ya - Yf) * (screen-factor - Xf) / (Xa - Xf)
slope2 = screen-factor / Xb * Yb

So, finally, that’s it. Let’s look at the whole magic formula now.

Formula
screen-factor = min(100vw, 100vh * aspect-ratio)
steep-slope = ((Ya - Yf) / (Xa - Xf)) * 100 > 1
slope1 = Yf + (Ya - Yf) * (screen-factor - Xf) / (Xa - Xf)
slope2 = screen-factor / Xb * Yb
Yu = (Xu / Xb) * Yb

Xu
  ? steep-slope
    ? clamp(Yf, slope1, clamp(Yb, slope2, Yu))
    : max(clamp(Yf, slope1, Ya), Yu)
  : steep-slope
    ? clamp(Yf, slope1, max(Yb, slope2))
    : max(clamp(Yf, slope1, Ya), slope2)

Function

Now we can integrate the formula into our setup. In this article, we’ll look at how to implement it in Sass. The two helper functions ensure that we output the rem values correctly (I will not go into it in detail). Then we set the anchor points and the aspect ratio as constants (respectively, Sass variables). Finally, we replace the coordinate points of our formula with variable names, and the FabUnit is ready for use.

_fab-unit.scss

@use "sass:math";


/* Helper functions */

$rem-base: 10px;

@function strip-units($number) {
  @if (math.is-unitless($number)) {
      @return $number;
    } @else {
      @return math.div($number, $number * 0 + 1);
  }
}

@function rem($size){
  @if (math.compatible($size, 1rem) and not math.is-unitless($size)) {
    @return $size;
  } @else {
    @return math.div(strip-units($size), strip-units($rem-base)) * 1rem;
  }
}


/* Default values fab-unit 🪄 */

$screen-min: 375;
$screen-opt-start: 1024;
$screen-opt-end: 1440;
$screen-max: 2000;  // $screen-opt-end | int > $screen-opt-end | false
$aspect-ratio: math.div(16, 9);  // smaller values for larger aspect ratios


/* Magic function fab-unit 🪄 */

@function fab-unit(
    $size-min, 
    $size-opt, 
    $screen-min: $screen-min, 
    $screen-opt-start: $screen-opt-start, 
    $screen-opt-end: $screen-opt-end, 
    $screen-max: $screen-max,
    $aspect-ratio: $aspect-ratio
  ) {
  $screen-factor: min(100vw, 100vh  $aspect-ratio);
  $steep-slope: math.div(($size-opt - $size-min), ($screen-opt-start - $screen-min))  100 > 1;
  $slope1: calc(rem($size-min) + ($size-opt - $size-min)  ($screen-factor - rem($screen-min)) / ($screen-opt-start - $screen-min));
  $slope2: calc($screen-factor / $screen-opt-end  $size-opt);
  @if $screen-max {
    $size-max: math.div(rem($screen-max), $screen-opt-end) * $size-opt;
    @if $steep-slope {
      @return clamp(rem($size-min), $slope1, clamp(rem($size-opt), $slope2, $size-max));
    } @else {
      @return clamp(clamp(rem($size-min), $slope1, rem($size-opt)), $slope2, $size-max);
    }
  } @else {
    @if $steep-slope {
      @return clamp(rem($size-min), $slope1, max(rem($size-opt), $slope2));
    } @else {
      @return max(clamp(rem($size-min), $slope1, rem($size-opt)), $slope2);
    }
  }
}
How To Use The FabUnit?

The work is done, now it’s simple. The style guide from our example can be implemented in no time:

We read the related values from the style guide and pass them to the FabUnit as arguments: fab-unit(16, 22).

style.scss

@import "fab-unit";


/* overwrite default values 🪄 */
$screen-max: 1800;


/* Style guide variables fab-unit 🪄 */

$fab-font-size-body: fab-unit(16, 22);
$fab-font-size-body-small: fab-unit(14, 16);

$fab-font-size-h1: fab-unit(60, 160);
$fab-font-size-h2: fab-unit(42, 110);
$fab-font-size-h3: fab-unit(28, 60);

$fab-space-s: fab-unit(20, 30);
$fab-space-m: fab-unit(40, 80);
$fab-space-l: fab-unit(60, 120);
$fab-space-xl: fab-unit(80, 180);


/* fab-unit in action 🪄 */

html {
  font-size: 100% * math.div(strip-units($rem-base), 16);
}

body {
  font-size: $fab-font-size-body;
}

.wrapper {
  max-width: rem($screen-max);
  margin-inline: auto;
  padding: $fab-space-m;
}

h1 {
  font-size: $fab-font-size-h1;
  border-block-end: fab-unit(2, 10) solid plum;
}

…

p {
  margin-block: $fab-space-s;
}

…
/* other use cases for calling fab-unit 🪄 */

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(fab-unit(200, 500), 1fr));
  gap: $fab-space-m;
}

.thing {
  flex: 0 0 fab-unit(20, 30);
  height: fab-unit(20, 36, 660, 800, 1600, 1800);  /* min, opt, … custom anchor points */
}

We are now able to draw the responsive line by calling fab-unit() and specifying just two sizes, the minimum and the optimum. We can control the font sizes, paddings, margins and gaps, heights and widths, and even — if we want to — define grid columns and flex layouts with it. We are also able to move the predefined anchor points locally.

Let’s have a look at the compiled output:

…
font-size: clamp(clamp(1.3rem, 1.3rem + 2 * (min(100vw, 177.7777777778vh) - 37.5rem) / 649, 1.5rem), min(100vw, 177.7777777778vh) / 1440 * 15, 2.0833333333rem);
…

And the computed output:

font-size: 17.3542px

Accessibility Concerns

To ensure good accessibility, I recommend testing in each case whether all sizes are sufficiently zoomable. Arguments with a large difference might not behave as desired. For more information on this topic, you can check the article “Responsive Type and Zoom” by Adrian Roselli.

Conclusion

Now we have created a function that does all the work for us. It takes a minimum and an optimum value and spits out a calculation to our CSS property, considering the screen width, aspect ratio, and the specified anchor points — a single formula that drives the entire project. No media queries, no breakpoints, no design jumps.

The FabUnit presented here is based on my own experience and is optimized for most of my projects. I save a lot of time and am satisfied with the result. It may be that you and your team have another approach and therefore have other requirements for a FabUnit. It would be nice if you were now able to create your own FabUnit according to your needs.

I would be happy if my approach inspired you with new ideas. I would be honored if you directly use the npm package of the FabUnit from this article for your projects.

Thank you! 🙏🏻

FAB-UNIT LINKS

  • FabUnit CodeSandbox Example
  • FabUnit Github
  • FabUnit Npm Package
  • FabUnit Visualiser

Merci Eli, Roman, Patrik, Fidi.