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 than400px
and we will get our maximum number of items. - For a small screen width, the
100%
will be smaller than400px
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%)
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 the100%/(N + 1) + 0.1%
which is a positive value: We have N items per rowWhen
screen width (100vw) < 400px
the difference will be positive, we multiply with a big value (the1000
) so it will get clamped to the100%
: 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%)
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%)
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%)
- When the screen width is smaller than
W2
we fall into100%
: one item per row - When the screen width is bigger than
W2
we fall into the firstclamp()
: We do the logic there- when the screen width is smaller than
W1
we fall into100%/(M + 1) + 0.1%)
: M items per row - when the screen width is bigger than
W1
we fall into100%/(N + 1) + 0.1%)
: N items per row
- when the screen width is smaller than
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;
}
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.