Build your Responsive website without media query

Temani Afif - May 5 '21 - - Dev Community

We cannot talk about web development without talking about Responsive Design. The latter is now a must and everyone will use Media Queries to build a responsive website.

Since the introduction of media queries (before 2000), CSS has evolved, and now (in 2021) there are a lot of tricks that can help you drastically reduce the usage of media queries and create an optimized code. I will even show you how to replace multiple media queries with only one CSS declaration.

PS: you have to run all the examples outside DEV to better see the results since the embedded version is very small


I will start with the trivial examples that are widely used but still limited:

flex & flex-wrap

Demo: https://codepen.io/t_afif/pen/zYNggoq

flex: 400px will set a base width equal to 400px. The items will then wrap if there isn't enough space for the 400px. They will grow to fill the empty spaces and will shrink if the container width is bigger than 400px.

✔️ Easy to use, only 2 lines of code are required
❌ We cannot control when the items will wrap
❌ We cannot control the number of items per row
❌ The items in the last row will have a different width


auto-fit & minmax

Demo: https://codepen.io/t_afif/pen/wvgVVPN

Similar to the previous example, the repeat(auto-fit,minmax(400px,1fr)) will define the base width and we will have a similar wrapping behavior.

✔️ Easy to use, only 1 line of code is required
✔️ The items in the last row will keep the same width
❌ We cannot control when the items will wrap
❌ We cannot control the number of items per row
❌ We don't have the shrink effect of the flexbox so we may face overflow


We will try to optimize the above examples with some CSS tricks to overcome the drawbacks.


Controlling the number of items

In our first example, let's change flex: 400px to flex: max(400px, (100% - 20px)/3). Resize the screen and you will notice that each row will not have more than 3 items (even for a large screen width).

Demo: https://codepen.io/t_afif/pen/abpeeeV

The logic is easy. When the screen width increase, 100%/3 will be bigger than 400px so it's the max value that will get used. We cannot have more than 3 items per row if all of them have a width equal to 100%/3.

What the hell is the 20px??

It's twice the gap we defined. For 3 items we will have 2 gaps so for N items we should use max(400px, (100% - (N - 1)*gap)/N).

We can still optimize the formula to remove the gap and use max(400px, 100%/(N + 1) + 0.1%). We tell the browser that each item will be equal to 100%/(N + 1) so N + 1 items per row but we add a tiny percentage (the 0.1%) thus one of the items will wrap and we end with only N items per row!

Demo: https://codepen.io/t_afif/pen/wvJwzbL

✔️ Now we can control the maximum number of items per row.

The same can also be applied to the CSS grid example:

Demo: https://codepen.io/t_afif/pen/BaWBLge

I have added CSS variables to easily control the different values.


Controlling the shrink effect

Using CSS grid we may have an overflow if the base width is bigger than the container width unlike with Flexbox where we have the flex-shrink.

To overcome this we change max(400px, 100%/(N + 1) + 0.1%) to clamp(100%/(N + 1) + 0.1%, 400px, 100%).

  • For a large screen width, the 100%/(N + 1) + 0.1% will be bigger than 400px and we will get our maximum number of items.
  • For a small screen width, the 100% will be smaller than 400px and our items will not exceed the container width.

Demo: https://codepen.io/t_afif/pen/ZEezBGL

✔️ We have our shrink effect and no more overflow


Controlling the wrap

In all the previous examples, we have no control over the wrap. We don't know when it will happen. It depends on the base width, the gap, the container width, etc

To control this we will change our base width (the 400px) with (400px - 100vw)*1000 to get the following

clamp(100%/(N + 1) + 0.1%, (400px - 100vw)*1000, 100%)
Enter fullscreen mode Exit fullscreen mode

It looks a bit strange but is easy to understand. The 100vw is our screen width and logically this value will change on screen resize while the 400px will remain fixed. This will lead us to the following logic:

  • When screen width (100vw) > 400px the difference will be negative so it will get clamped to the 100%/(N + 1) + 0.1% which is a positive value: We have N items per row

  • When screen width (100vw) < 400px the difference will be positive, we multiply with a big value (the 1000) so it will get clamped to the 100%: We have 1 item per row

Demo: https://codepen.io/t_afif/pen/BaWBQqK

We did our first media query!

We were able to move from N columns to 1 column without using @media and with only one CSS declaration. Our base width has become a breakpoint.

✔️ We can control when the items will wrap
✔️ We can control the number of items per row


What about moving from N columns to M columns?

We simply update our clamp() function like below:

clamp(100%/(N + 1) + 0.1%, (400px - 100vw)*1000, 100%/(M + 1) + 0.1%)
Enter fullscreen mode Exit fullscreen mode

I think everyone got the trick now. When the screen width is bigger than 400px we fall into the first rule (N items per row). When the screen width is smaller than 400px we fall into the second one (M items per row).

Demo: https://codepen.io/t_afif/pen/ZEezBgo

We can easily control the number of items per row and we can decide when to change that number. All this using only one CSS declaration!


What about moving from N columns to M columns to 1 column?

We can do this by nesting clamp() functions like the below:

clamp(clamp(100%/(N + 1) + 0.1%, (W1 - 100vw)*1000,100%/(M + 1) + 0.1%), (W2 - 100vw)*1000, 100%)
Enter fullscreen mode Exit fullscreen mode

We have two breakpoints so we will logically need two widths (W1 and W2).

We can see our function like:

clamp(clamp( .. ), (W2 - 100vw)*1000, 100%)
Enter fullscreen mode Exit fullscreen mode
  • When the screen width is smaller than W2 we fall into 100%: one item per row
  • When the screen width is bigger than W2 we fall into the first clamp(): We do the logic there
    • when the screen width is smaller than W1 we fall into 100%/(M + 1) + 0.1%): M items per row
    • when the screen width is bigger than W1 we fall into 100%/(N + 1) + 0.1%): N items per row

Let's see this in play:

Demo: https://codepen.io/t_afif/pen/xxqKgZe

We did 2 media queries using only one CSS declaration! Not only this, but we can easily adjust that declaration using CSS variables which means that we can update the logic for different containers easily

Demo: https://codepen.io/t_afif/pen/mdWbRRE

How many media queries until now? well, I stopped the count ...

Do you want more breakpoints? You simply nest another clamp() function and you have

From N columns to M columns to P columns to 1 column

Demo: https://codepen.io/t_afif/pen/bGqbgYY

We have our responsive design without any single media queries

✔️ Only one line of code
✔️ Easy to update using CSS variables
✔️ We can control the number of items per row
✔️ We can control when the items will wrap
✔️ We don't have an overflow on small screens
✔️ All the items have the same width
✔️ Each container can have its breakpoints


Container Queries

Everyone is excited to use this new feature that considers the width of the element instead of the screen to create media queries but no need to wait for it.

The trick I made already covers this feature. We simply change 100vw with 100% and all the logic we made previously will now consider the container width instead of the screen width.

Resize the below containers and see the magic in the play

Demo: https://codepen.io/t_afif/pen/gOmYmgz


Bonus

I will end this post with a final trick that allows you to change the coloration of your items without using media queries as well.

div {
  background:
   linear-gradient(purple 0 0) 0 /calc(var(--w3) - 100vw) 1px,
   linear-gradient(blue   0 0) 0 /calc(var(--w2) - 100vw) 1px,
   linear-gradient(green  0 0) 0 /calc(var(--w1) - 100vw) 1px,
   red;
}
Enter fullscreen mode Exit fullscreen mode

We consider 3 gradient layers plus a background-color. The size of each gradient is defined using one of the breakpoints. If calc() is negative then the gradient will not show. If calc() is positive then the size will also be positive and thanks to the repeat feature, it will cover all the area.

The order is very important. Below is a table to better understand:

[0 W3[ [W3 W2[ [W2 W1[ [W1 infinity[
✔️purple ❌purple ❌purple ❌purple
✔️blue ✔️blue ❌blue ❌blue
✔️green ✔️green ✔️green ❌green
✔️red ✔️red ✔️red ✔️red

The red color is always shown and at each breakpoint one of the gradients is displayed covering the bottom layer.

Here is a demo with all the features together. Run at full screen and resize:

Demo: https://codepen.io/t_afif/pen/wvJwdRW

To make the coloration work based on the container width, we update the code slightly and use a pseudo-element that we position relatively to the container and we clip the overflow

Demo: https://codepen.io/t_afif/pen/zYZOwQJ

A related Stack Overflow question where I am using such a trick: How to change the color of <div> Element depending on its height or width?. I am also changing the text coloration and the borders based on the width or the height.


That's it!

Now you have a good trick that allows you to control your responsive layout without using media queries and with only a few lines of code. Of course, this is not a replacement for media queries. It's an optimization that can help you reduce the amount of code.


buy me a coffee

OR

Become a patron

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .