How to Get a Pixel-Perfect, Linearly Scaled UI

Dynamically scaling CSS values based on the viewport width is hardly a new topic. You can find plenty of in-depth coverage right here on CSS-Tricks in articles like this one or this one.

Most of those examples, though, use relative CSS units and unitless values to achieve fluid scaling. That loses pixel perfection and usually introduces text wrapping and layout shifts once the screen goes below or above a certain threshold.

But what if we really do want pixel perfection? What if, let’s say, we are developing a complex real-time analytics dashboard to be viewed on large TVs at a conference room or as some PWA to be opened exclusively on mobile and tablet devices, as opposed to text-heavy blogs and news websites? Those are cases where we need more precision.

In other words, what if we want to scale designs uniformly? Of course, one can scale the content with CSS transforms based on the available width as covered in this article — this way, the correct ratios are preserved.

However, we can also achieve fluid proportional scaling UIs using pixel values in CSS. They scale appropriately based on the device screen real estate, all while preserving their pixel-perfect proportions. Further, we can still use pixel values and automatically convert them to relative CSS units if working in pixels is more comfortable or familiar.

Scaling our UI

Let’s try to implement this awesome dashboard, courtesy of Craftwork. We need to make it in such a way that it scales perfectly and preserves all the texts line counts, margins, image sizes, etc.

Let’s work in CSS pixel values and use SCSS for speed and convenience. So, if we are to target the title of one of these card widgets, our SCSS might look something like this:

.cardWidget {
  .cardHeading {
    font-size: 16px;
  }
}

Nothin’ fancy. Nothing we have not seen before. Being a pixel value, this will not scale.

This design was created with a container that’s 1600px wide. Let’s assume that at 1600px, the ideal font size for the titles of the cards should be 16px since that’s how it’s designed.

Now that we have the “ideal” container width font size for this width, let’s scale our CSS pixel values accordingly using the current* viewport width:

/*
  1600px is the ideal viewport width that the UI designers who
  created the dashboard used when designing their Figma artboards

  Please not we are not using pixel units here, treating it purely
  as a numeric value.
*/
--ideal-viewport-width: 1600;
/*
  The actual width of the user device
*/
--current-viewport-width: 100vw;

.cardWidget {
  .cardHeading {
    /*
      16px is the ideal font size that the UI designers want for
      1600px viewport width.

      Please note that we are not using pixel units here,
      treating it purely as a numeric value.
    */
    --ideal-font-size: 16;
    /*
      Calculate the actual font size:

      We take our idealFontSize and multiply it by the difference
      between the current viewport width and the ideal viewport width.
    */
    font-size: calc(
      var(--ideal-font-size) * (var(--current-viewport-width) / var(--ideal-viewport-width)
    );
  }
}

As you can see, we treat the ideal font size we obtained from the design as a base and multiply it by the difference between the current and ideal viewport widths. How does this look mathematically? Let’s say we are viewing this web app on a screen with the exact same width as the mockup:

--current-device-width: 100vw; // represents 1600px or full width of the screen
--ideal-viewport-width: 1600; // notice that the ideal and current width match
--ideal-font-size: 16;
// this evaluates to:
font-size: calc(16 * 1600px / 1600);
// same as:
font-size: calc(16 * 1px);
// final result:
font-size: 16px;

So, since our viewport width matches perfectly, our font-size ends being exactly 16px at the ideal viewport width of 1600px.

As another example, let’s say we are viewing the web app on a smaller laptop screen that’s 1366px wide. Here is the updated math:

font-size: calc(16 * 1366px / 1600);
// same as:
font-size: calc(16 * 0.85375px);
// final result:
font-size: 13.66px;

Or let’s say we are viewing this on a full high-definition display at 1920px wide:

font-size: calc(16 * 1920px / 1600);
// same as:
font-size: calc(16 * 1.2px);
// final result:
font-size: 19.2px;

You can see for yourself how even though we use pixel values as reference, we are actually able to proportionally scale our CSS values based on the difference in width between the ideal and current viewport sizes.

Here is a small demo I built to illustrate the technique:

Here’s a video for convienence:

Clamping the min and max viewport width

Using this current approach, the design scales to match the viewport size, no matter how big or small the viewport gets. We can prevent this with CSS clamp() which allows us to set a minimum width of 350px and maximum width of 3840px. This means that if we are to open the web app on a device with 5000px width, our layout will stay locked at 3840px:

--ideal-viewport-width: 1600;
--current-viewport-width: 100vw;
/*
  Set our minimum and maximum allowed layout widths:
*/
--min-viewport-width: 350px;
--max-viewport-width: 3840px;

.cardWidget {
  .cardHeading {
    --ideal-font-size: 16;
    font-size: calc(
      /*
        The clamp() function takes three comma separated expressions
        as its parameter, in the order of minimum value, preferred value
        and maximum value:
      */
      --clamped-viewport-width: clamp(var(--min-viewport-width), var(--current-viewport-width), var(--max-viewport-width);
      /*
        Use the clamped viewport width in our calculation
      */
      var(--ideal-font-size) * var(--clamped-viewport-width) / var(--ideal-viewport-width)
    );
  }
}

Let’s make a helper for the unit conversions

Our code is quite verbose. Let’s write a simple SCSS function that converts our values from pixels to relative units. That way, we can import and reuse anywhere this anywhere without so much duplication:

/*
  Declare a SCSS function that takes a value to be scaled and
  ideal viewport width:
*/
@function scaleValue(
  $value,
  $idealViewportWidth: 1600px,
  $min: 350px,
  $max: 3840px
) {
  @return calc(
    #{$value} * (clamp(#{$min}, 100vw, #{$max}) / #{$idealViewportWidth})
  );
}

/*
  We can then apply it on any numeric CSS value.

  Please note we are passing not pixel based, but numeric values:
*/
.myElement {
  width: #{scaleValue(500)};
  height: #{scaleValue(500)};
  box-shadow: #{scaleValue(2)} #{scaleValue(2)} rgba(black, 0.5);
  font-size: #{scaleValue(24)};
}

Porting this to Javascript

Sometimes CSS doesn’t cut it and we have to use JavaScript to size a component. Let’s say we are constructing an SVG dynamically and we need to size its width and height properties based on an ideal design width. Here is the JavaScript to make it happen:

/*
  Our helper method to scale a value based on the device width
*/
const scaleValue = (value, idealViewportWidth = 1600) => {
  return value * (window.innerWidth / idealViewportWidth)
}

/*
  Create a SVG element and set its width, height and viewbox properties
*/
const IDEAL_SVG_WIDTH = 512
const IDEAL_SVG_HEIGHT = 512

const svgEl = document.createElement('svg')
/* Scale the width and height */
svgEl.setAttribute('width', scaleValue(IDEAL_SVG_WIDTH))
svgEl.setAttribute('height', scaleValue(IDEAL_SVG_WIDTH))

/*
  We don't really need to scale the viewBox property because it will
  perfectly match the ratio of the scaled width and height
*/
svg.setAttribute('viewBox', `0 0 ${IDEAL_SVG_WIDTH} ${IDEAL_SVG_HEIGHT}`)

The drawbacks of this technique

This solution is not perfect. For example, one major drawback is that the the UIs are no longer zoomable. No matter how much the user zooms, the designs will stay locked as if they are viewed at 100% zoom.

That said, we can easily use traditional media queries, where we set different ideal numeric values at different viewport widths:

.myElement {
  width: #{scaleValue(500)};
  height: #{scaleValue(500)};
  box-shadow: #{scaleValue(2)} #{scaleValue(2)} rgba(black, 0.5);
  font-size: #{scaleValue(24)};
  @media (min-width: 64em) {
    width: #{scaleValue(800)};
    font-size: #{scaleValue(42)};
  }
}

Now we can benefit from both media queries and our pixel-perfect linear scaling.

Wrapping up

All of this is an alternative way to implement fluid UIs. We treat the pixel-perfect values as pure numeric values, and multiply them by the difference between the current viewport width and the “ideal” viewport width from the designs.

I have used this technique extensively in my own work and hope that you will find some use of it too.


The post How to Get a Pixel-Perfect, Linearly Scaled UI appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

How to Cancel Pending API Requests to Show Correct Data

I recently had to create a widget in React that fetches data from multiple API endpoints. As the user clicks around, new data is fetched and marshalled into the UI. But it caused some problems.

One problem quickly became evident: if the user clicked around fast enough, as previous network requests got resolved, the UI was updated with incorrect, outdated data for a brief period of time.

We can debounce our UI interactions, but that fundamentally does not solve our problem. Outdated network fetches will resolve and update our UI with wrong data up until the final network request finishes and updates our UI with the final correct state. The problem becomes more evident on slower connections. Furthermore, we’re left with useless networks requests that waste the user’s data.

Here is an example I built to illustrate the problem. It grabs game deals from Steam via the cool Cheap Shark API using the modern fetch() method. Try rapidly updating the price limit and you will see how the UI flashes with wrong data until it finally settles.

The solution

Turns out there is a way to abort pending DOM asynchronous requests using an AbortController. You can use it to cancel not only HTTP requests, but event listeners as well.

The AbortController interface represents a controller object that allows you to abort one or more Web requests as and when desired.

Mozilla Developer Network

The AbortController API is simple: it exposes an AbortSignal that we insert into our fetch() calls, like so:

const abortController = new AbortController()
const signal = abortController.signal
fetch(url, { signal })

From here on, we can call abortController.abort() to make sure our pending fetch is aborted.

Let’s rewrite our example to make sure we are canceling any pending fetches and marshalling only the latest data received from the API into our app:

The code is mostly the same with few key distinctions:

  1. It creates a new cached variable, abortController, in a useRef in the <App /> component.
  2. For each new fetch, it initializes that fetch with a new AbortController and obtains its corresponding AbortSignal.
  3. It passes the obtained AbortSignal to the fetch() call.
  4. It aborts itself on the next fetch.
const App = () => {
 // Same as before, local variable and state declaration
 // ...

 // Create a new cached variable abortController in a useRef() hook
 const abortController = React.useRef()

 React.useEffect(() => {
  // If there is a pending fetch request with associated AbortController, abort
  if (abortController.current) {
    abortController.abort()
  }
  // Assign a new AbortController for the latest fetch to our useRef variable
  abortController.current = new AbortController()
  const { signal } = abortController.current

  // Same as before
  fetch(url, { signal }).then(res => {
    // Rest of our fetching logic, same as before
  })
 }, [
  abortController,
  sortByString,
  upperPrice,
  lowerPrice,
 ])
}

Conclusion

That’s it! We now have the best of both worlds: we debounce our UI interactions and we manually cancel outdated pending network fetches. This way, we are sure that our UI is updated once and only with the latest data from our API.


The post How to Cancel Pending API Requests to Show Correct Data appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

Too Many SVGs Clogging Up Your Markup? Try `use`.

Recently, I had to make a web page displaying a bunch of SVG graphs for an analytics dashboard. I used a bunch of <rect>, <line> and <text> elements on each graph to visualize certain metrics.

This works and renders just fine, but results in a bloated DOM tree, where each shape is represented as separate nodes. Displaying all 50 graphs simultaneously on a web page results in 5,951 DOM elements in total, which is far too many.

We might display 50-60 different graphs at a time, all with complex DOM trees.

This is not optimal for several reasons:

  • A large DOM increases memory usage, longer style calculations, and costly layout reflows.
  • It will increases the size of the file on the client side.
  • Lighthouse penalizes the performance and SEO scores.
  • Maintainability is a nightmare — even if we use a templating system — because there’s still a lot of cruft and repetition.
  • It doesn’t scale. Adding more graphs only exacerbates these issues.

If we take a closer look at the graphs, we can see a lot of repeated elements.

Each graph ends up sharing lots of repeated elements with the rest.

Here’s dummy markup that’s similar to the graphs we’re using:

<svg
  xmlns="http://www.w3.org/2000/svg"
  version="1.1"
  width="500"
  height="200"
  viewBox="0 0 500 200"
>
  <!--
    📊 Render our graph bars as boxes to visualise our data.
    This part is different for each graph, since each of them displays different sets of data.
  -->
  <g class="graph-data">
    <rect x="10" y="20" width="10" height="80" fill="#e74c3c" />
    <rect x="30" y="20" width="10" height="30" fill="#16a085" />
    <rect x="50" y="20" width="10" height="44" fill="#16a085" />
    <rect x="70" y="20" width="10" height="110" fill="#e74c3c" />
    <!-- Render the rest of the graph boxes ... -->
  </g>

  <!--
    Render our graph footer lines and labels.
  -->
  <g class="graph-footer">
    <!-- Left side labels -->
    <text x="10" y="40" fill="white">400k</text>
    <text x="10" y="60" fill="white">300k</text>
    <text x="10" y="80" fill="white">200k</text>
    <!-- Footer labels -->
    <text x="10" y="190" fill="white">01</text>
    <text x="30" y="190" fill="white">11</text>
    <text x="50" y="190" fill="white">21</text>
    <!-- Footer lines -->
    <line x1="2" y1="195" x2="2" y2="200" stroke="white" strokeWidth="1" />
    <line x1="4" y1="195" x2="2" y2="200" stroke="white" strokeWidth="1" />
    <line x1="6" y1="195" x2="2" y2="200" stroke="white" strokeWidth="1" />
    <line x1="8" y1="195" x2="2" y2="200" stroke="white" strokeWidth="1" />
    <!-- Rest of the footer lines... -->
  </g>
</svg>

And here is a live demo. While the page renders fine the graph’s footer markup is constantly redeclared and all of the DOM nodes are duplicated.

The solution? The SVG element.

Luckily for us, SVG has a <use> tag that lets us declare something like our graph footer just once and then simply reference it from anywhere on the page to render it as many times as we want. From MDN:

The <use> element takes nodes from within the SVG document, and duplicates them somewhere else. The effect is the same as if the nodes were deeply cloned into a non-exposed DOM, then pasted where the use element is.

That’s exactly what we want! In a sense, <use> is like a modular component, allowing us to drop instances of the same element anywhere we’d like. But instead of props and such to populate the content, we reference which part of the SVG file we want to display. For those of you familiar with graphics programming APIs, such as WebGL, a good analogy would be Geometry Instancing. We declare the thing we want to draw once and then can keep reusing it as a reference, while being able to change the position, scale, rotation and colors of each instance.

Instead of drawing the footer lines and labels of our graph individually for each graph instance then redeclaring it over and over with new markup, we can render the graph once in a separate SVG and simply start referencing it when needed. The <use> tag allows us to reference elements from other inline SVG elements just fine.

Let’s put it to use

We’re going to move the SVG group for the graph footer — <g class="graph-footer"> — to a separate <svg> element on the page. It won’t be visible on the front end. Instead, this <svg> will be hidden with display: none and only contain a bunch of <defs>.

And what exactly is the <defs> element? MDN to the rescue once again:

The <defs> element is used to store graphical objects that will be used at a later time. Objects created inside a <defs> element are not rendered directly. To display them you have to reference them (with a <use> element for example).

Armed with that information, here’s the updated SVG code. We’re going to drop it right at the top of the page. If you’re templating, this would go in some sort of global template, like a header, so it’s included everywhere.

<!--
  ⚠️ Notice how we visually hide the SVG containing the reference graphic with display: none;
  This is to prevent it from occupying empty space on our page. The graphic will work just fine and we will be able to reference it from elsewhere on our page
-->
<svg
  xmlns="http://www.w3.org/2000/svg"
  version="1.1"
  width="500"
  height="200"
  viewBox="0 0 500 200"
  style="display: none;"
>
  <!--
    By wrapping our reference graphic in a <defs> tag we will make sure it does not get rendered here, only when it's referenced
-->
  <defs>
    <g id="graph-footer">
      <!-- Left side labels -->
      <text x="10" y="40" fill="white">400k</text>
      <text x="10" y="60" fill="white">300k</text>
      <text x="10" y="80" fill="white">200k</text>
      <!-- Footer labels -->
      <text x="10" y="190" fill="white">01</text>
      <text x="30" y="190" fill="white">11</text>
      <text x="50" y="190" fill="white">21</text>
      <!-- Footer lines -->
      <line x1="2" y1="195" x2="2" y2="200" stroke="white" strokeWidth="1" />
      <line x1="4" y1="195" x2="2" y2="200" stroke="white" strokeWidth="1" />
      <line x1="6" y1="195" x2="2" y2="200" stroke="white" strokeWidth="1" />
      <line x1="8" y1="195" x2="2" y2="200" stroke="white" strokeWidth="1" />
      <!-- Rest of the footer lines... -->
    </g>
  </defs>
</svg>

Notice that we gave our group an ID of graph-footer. This is important, as it is the hook for when we reach for <use>.

So, what we do is drop another <svg> on the page that includes the graph data it needs, but then reference #graph-footer in <use> to render the footer of the graph. This way, there’s no need to redeclaring the code for the footer for every single graph.

Look how how much cleaner the code for a graph instance is when <use> is in.. umm, use.

<svg
  xmlns="http://www.w3.org/2000/svg"
  version="1.1"
  width="500"
  height="200"
  viewBox="0 0 500 200"
>
  <!--
    📊 Render our graph bars as boxes to visualise our data.
    This part is different for each graph, since each of them displays different sets of data.
  -->
  <g class="graph-data">
    <rect x="10" y="20" width="10" height="80" fill="#e74c3c" />
    <rect x="30" y="20" width="10" height="30" fill="#16a085" />
    <rect x="50" y="20" width="10" height="44" fill="#16a085" />
    <rect x="70" y="20" width="10" height="110" fill="#e74c3c" />
    <!-- Render the rest of the graph boxes ... -->
  </g>

  <!--
    Render our graph footer lines and labels.
  -->
  <use xlink:href="graph-footer" x="0" y="0" />
</svg>

And here is an updated <use> example with no visual change:

Problem solved.

What, you want proof? Let’s compare the demo with <use> version against the original one.

DOM nodesFile sizeFile Size (GZIP compression)Memory usage
No <use>5,952664 KB40.8 KB20 MB
With <use>2,572294 KB40.4 KB18 MB
Savings56% fewer nodes42% smaller0.98% smaller10% less

As you can see, the <use> element comes in handy. And, even though the performance benefits were the main focus here, just the fact that it reduces huge chunks of code from the markup makes for a much better developer experience when it comes to maintaining the thing. Double win!

More information


The post Too Many SVGs Clogging Up Your Markup? Try `use`. appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.