Responsive tricks: how to proportionally scale an element based on screen width

Elizabeth Le
JOOR Engineering
Published in
7 min readMay 2, 2022

By Omar Quiroz

Desktop monitor, laptop, smartphone, and tablet device on a table

Long gone are the days when vertically centering elements and making things responsive with CSS was a nightmare. Nowadays you can just throw a couple of CSS lines and your elements will keep their aspect ratio while looking great without a care for the size of the screen they’re showing up in. However, sometimes you just get a special requirement that might complicate things a bit, or maybe you have to work with a third-party library that doesn’t play along with your setup. Well, we actually had to face both scenarios recently with one of our core brand products.

When responsiveness breaks the ratio

The story, to keep it short, is that one of our products was designed to allow brands to craft editorial experiences that looked and felt like lookbooks. These could be shared with retailers and external users alike, using an intuitive drag and drop interface to place different kinds of elements on a white canvas and customize their size and position at will.

In order to enable the whole grid experience we used a crafty open-source library called react-grid-layout that handles for us most of the grid-related interactions (such as collisions, placement, resizing, etc) while playing nicely with React, which we use for our frontend at Joor. react-grid-layout requires coordinates (x, y) and size (w, h) in units of columns and rows (kinda like CSS grid) for each element on the grid. With that information, along with the column count of the grid, elements are distributed and sized accordingly.

“Cool cool. So, what’s the issue?” you’re asking.

Well, when you have a number of columns in a responsive setting, what usually happens is that the grid itself changes size along with the screen while the number of columns remains the same. This means that if we have twelve 125px columns, and the screen width shrinks down to 1000px, then we end up with twelve columns that are each 75px wide. Apart from the screen size, what’s really changing is the width of our columns. You can imagine how things might look completely different for 75px wide columns compared to 125px wide ones, when you’re placing images inside a canvas.

Consider this an inaccurate and outdated representation of the hardware that brands use our product on.

A lot of our users in the fashion industry have big screens. Big, big screens. On the other hand, their customers might have a small screen instead, or even a cute-but-really-uncomfortable 11-inch laptop. So our goal was that our brands could have the certainty that any “lookbook-like” experiences they crafted on our platform were going to look exactly the same on their customers’ screen — regardless of screen size.

We had 2 options then:

  1. We could change the number of columns depending on the active breakpoint (the user’s screen size) and then map sizes to be equivalent between different column layouts (e.g. on large screens we would have 12 columns and an element would take 2 columns, on smaller screens we would have 6 columns and an element would take 1 column).
  2. Use the CSS property transform: scale() to resize our grid based on the screen’s width. This will make the whole grid decrease in size at the same time that the screen does while keeping the size of all the elements and columns inside just the same, which in turn keeps the proportions nice and pretty.

Option 1 had too many unknowns and seemed to leave out a lot of edge cases so we decided to try our luck with option 2 (which was also a bit nicer and prettier).

Scale up and down, up and down

Here’s a quick refresher of how CSS transform: scale() works: transform is a property with several possible values which lets you modify coordinates of an element. One of those available transformations is scale, which allows you to reduce or increase an element’s size in relation to its original size.

If you pass 1 as a value to scale, it means “stay the same”. 2 means “I want you to look twice as big”. If you want instead to make it smaller you can pass 0.5 to make it look half its size, 0.25 to make it look a quarter of its original size, and so on. You can also scale the element separately across the x and y axes using transform: scaleX() and transform: scaleY(). Finally, you can also control the origin point of the transformation using transform-origin.

Once we landed on transform: scale, there were a couple of things we still needed to figure out:

  1. What screen size are we going to call our “base” size? This is important as the actual value we’re going to end up passing to transform: scale() is scale in relation to this base size.
  2. What formula are we going to use to calculate the new scale?
  3. What does the implementation of all this actually look like in code?

For number 1, we knew the width was going to be at least greater than 1366px. But to get the actual number we tried different screen sizes above 1366px by increments and settled on the screen width where the grid and their elements looked their best.

Number 2 took a bit more time. You see, as always in math, there’s multiple ways to do it. We ended up doing it the analytical way as it was the first solution that came to mind. We knew that our scaling factor (the number we needed to pass to transform: scale()) was a variable that depended only on the user’s screen width. We also knew that the number would scale linearly in relation to the same screen’s width, which means it is a single-variable linear equation.

And if you’ve done calculus you will know that a single-variable’s linear equation is just a straight’s line’s equation that can be represented in the shape of

y = mx + b

Where y will be the scaling factor we want, x is the width of the screen, and m and b are constants (m is the slope of the line and b is the point where the line intercepts the y). That’s cool and all but how do we get the values of m and b if they’re constants then?

Well, there’s a form of the equation called two-points that looks like this:
y − y1 = m(x − x1)

Two-point form of the equation of a straight line

So what we did was to get the two points manually by measuring for ourselves. That meant that we picked a screen width above our base point and we manually set a scale factor that looked how we wanted the scaling to look at that level. That gave us the values for x (width) and y (scale factor). Then we repeated the process for a screen width below our base point and that gave us x1 and y1. So now with two points of the straight line that represents our desired scale we got our value for m.

Finally we can just replace values in the standard shape of the equation

y = mx + b

Finally knowing the value of m we can just replace it in the standard shape of the equation and replace the y and x with the measurements of one of the points we used in the previous step and from there, solve for b.

Once we replace the values in the equation, that’s it. We can pass whatever x (screen width) we want to create a getScale function and we will get our y (scale factor) that we can directly pass to transform: scale() to resize our grid in relation to the screen’s width.

Quick note: as we said, there’s multiple ways to get the formula. An easier way, for example, is just to do a quick rule of three where scale factor = screen width / base screen width. Talk about over-engineering, huh.

And finally, here’s a simplified representation on how this looks like using JSX.

Works well, looks well

So just by understanding a bit of how CSS transform: scale() works we can come up with a solution for scaling an element that: maintained its calculated with and height, kept the aspect ratio looking great, and with all click events working just the same.

We’re hiring at JOOR!

If you liked this article and you think you would be a good fit to working on problems and technologies similar to the ones we mentioned, please check out our open positions page and apply to join us today!

--

--