If you want to learn how to create a fluid CSS grid with static measurements (such as px), this article is for you!
What is a fluid grid?
Creating a responsive CSS Grid is pretty easy in theory. You could set up your body tag as a 3X3 fluid grid by setting the display to grid and the shorthand property grid-template to repeat(3, 1fr) / repeat(3/1fr)
body {
display: grid;
grid-template: repeat(3, 1fr) / repeat(3, 1fr);
}
However, in practice, a fluid grid using fr isn’t always the correct answer to our problems.
The fr unit
The fr unit stands for “fraction”, and essentially stands for the fraction of space it takes up in the grid. For instance, if I have a 2 column grid with each column set to 1fr, then the width of each column would be 50% (or 1/2).
// in other words:
repeat(2, 1fr) === 1/2 //column width
Display Grid and Grid Template Columns
In general, when we’re designing a layout with grid, we can make the rows fluid, but often need to make the columns use a defined width. Let’s take a website that needs to have a header, a sidebar, a footer, and a main article area.
Here is that layout represented in HTML:
<body>
<header></header>
<main>
<article></article>
<aside></aside>
</main>
<footer></footer>
</body>
To design this grid we could set the body element to display grid and create our 3 column, 3 row responsive grid:
body {
display: grid;
grid-template: repeat(3, 1fr) / repeat(3, 1fr);
}
Next we need to asign the children of the body tag to the correct row and column of our fluid grid:
header {
grid-row: 1;
grid-column: 1 / span 3;
}
main {
grid-row: 2;
grid-column: 1 / span 3;
}
footer {
grid-row: 3;
grid-column: 1 / span 3;
}
The header will be on row 1 and span all 3 columns. The main area will be in row 2 and span all 3 columns and the footer area will be in row 3 and also span all columns.
Now we can set the main tag to display grid with some defined columns to set up our aside and article tags:
main {
grid-row: 2;
grid-column: 1 / span 3;
// add this to the main css
display: grid;
grid-template-columns: 1fr 50% 10% 20% 1fr;
}
Let’s now assign our article and aside to the main grid:
article {
grid-column: 2 / span 1;
}
aside {
grid-column: 4 / span 1;
}
And voila! A completely fluid grid is born! You can now shrink and grow this grid and it will stay fluid. Add as much content inside these html elements and your grid will never break!
If you’re into designing with grid, don’t miss this article on how to create a Chess Board with CSS Subgrid.
What if the grid looks ugly on mobile?
There is a problem with using percentage measurements and no media queries with a fluid grid, and that problem is that smaller percentages squish content into too small a space on mobile. We should still use media queries to determine our grid column widths, while maintaining its fluidity.
One simple way of accomplishing this is using a min-width media query for just desktops:
// responsive mobile first styles
main {
grid-row: 2;
grid-column: 1 / span 3;
display: grid;
grid-template-columns: 1fr 80% 1fr;
grid-template-rows: auto;
}
article {
grid-row: 1;
grid-column: 2 / span 1;
}
aside {
grid-column: 2 / span 1;
grid-row: 2;
}
@media screen and (min-width: 1078px) {
main {
grid-template-columns: 1fr 50% 10% 20% 1fr;
}
article {
grid-row: 1;
grid-column: 2 / span 1;
}
aside {
grid-row: 1;
grid-column: 4 / span 1;
}
}
Notice that by setting the main tag to grid-rows: auto; And then assigning the children of the main tag to a row, the grid knows when to make 1 or 2 rows.
Check out the complete fluid grid below. Notice how shrinking the viewport (or selecting the 0.5x or 0.25x buttons on the codepen below) will allow you to view the responsiveness of the grid.
Make a fluid CSS Grid with static measurements like px
Let’s say your design requires pixel perfect widths. How do you make a fluid CSS Grid with static measurements?
There is a way to ensure that your design is fluid and responsive and uses px widths. Enter the min() CSS function.
What the min() CSS function does
According to MDN, the min() CSS function lets you set the smallest (most negative) value from a list of comma-separated expressions. In other words, of every value you put inside the min() function, the value that will be set by the browser is the smallest.
Let’s take for example three pixel widths. Inside the min function below, the smallest is easily the 20px. Therefore the width of this paragraph will always be 20px.
p {
width: min(100px, 50px, 20px);
}
This function written like this is essentially useless. We can tell which measurement is smallest above so it would be much easier to just write:
p {
width:20px;
}
Now, let’s argue instead that we need to set the article width to 800px wide for all screens large enough to handle 800px wide. I could write a bunch of media queries for 1078, 800, etc. OR, I could use the min() function to determine which is the smallest value, and use that.
Because my article is in column 2, I want to alter the width of my column 2:
main {
grid-template-columns: 1fr min(80%, 800px) 1fr;
}
@media screen and (min-width: 1078px) {
main {
grid-template-columns: 1fr min(800px, 60%) 10% 20% 1fr;
}
}
The above code will make the article tag, or second column, either 800px wide or 80%, whichever is smallest. Then for screens over 1078px, we’ll have a width of 800px, or 60%, whichever is smaller.
You’ll notice with the above changes to the CSS, that as you grow the screen above 1078px, the article is never over 800px wide. Alternatively, as you shrink the screen the article can be 800px wide as long as 800px is smaller than 80%. Now we’ve created a fluid grid with static measurements!
Can I use calc() inside of min()?
No, you cannot use the CSS calc() function inside of the min() function, but the good news is that you don’t have to! You can throw your calculations directly in the min() function as one of its arguments. For example, below we see the width of the element represented as either 728px, 100% – 30px, or 40em.
width: min(728px, 100% - 30px, 40em)
If I have an element with a set right and left padding of 15px, I could throw my calculation in the min function for every value:
min(100% - 30px, 728px - 30px, 40em - 30px)
What if I need to allow users to edit the width of the layout without breaking it?
Let’s assume you’re working with a Content Management System, or CMS. Let’s also assume you’re allowing the user to insert whatever width they want to your second column. We could let the user set the second column to any width, but doing so might break the responsiveness of the design, and it might also break our design we were told must be pixel perfect.
We can use the grid-template-columns, the CSS min() function, and some vanilla Javascript to allow users to alter the width of the column without breaking the design.
Let’s create some inputs to allow the user to alter the width of the second column.
<body class="body" onload="tbd">
<div class="main">
<div class="header">
<div class="content-width-form">
<label for="content-width">Content Width:</label>
<input type="text" name="content-width" id="content-width" />
<select name="measurement" id="measurement">
<option value="px">px</option>
<option value="%">%</option>
<option value="em">em</option>
</select>
<button onclick="tbd" id="save">Save</button>
<button onclick="tbd" id="clear">Clear</button>
</div>
</div>
</div>
</body>
Let’s create some variables to store our inputs in:
const contentWidth = document.getElementById('content-width')
const measurement = document.getElementById('measurement')
Next let’s create a couple of functions to be called when the buttons are clicked:
<button onclick="saveWidth()" id="save">Save</button>
<button onclick="clearWidth()" id="clear">Clear</button>
The JS will be using localStorage to save our values:
const saveWidth = () => {
localStorage.setItem('content-width', width)
}
const clearWidth = () => {
localStorage.removeItem('content-width')
}
Let’s create some mutable variables to store the width and measurement types from our inputs:
let meas = measurement.options[0].value
let width = localStorage.getItem('content-width') ? localStorage.getItem('content-width') : 728
Note that if the localStorage item ‘content-width’ is available, we’ll use that value, otherwise we’ll start with 728px.
Next, we’ll add a couple of event listeners to alter our width and measurement values when the change event for the input is fired.
measurement.addEventListener('change', (v) => {
meas = v.target.value
})
contentWidth.addEventListener('change', (v) => {
width = v.target.value
})
When developing around text inputs, sometimes devs need to create unit tests with Jest to ensure the input acts the way we’d expect. Don’t miss this article on testing React text inputs with userEvent.
Creating a dynamic CSS Variable to set a grid column width
All we have to do now is create a CSS variable to store the content width value in and then use it in our CSS.
I’ll update my grid-template-columns like so.
Before:
.main{
height: 100vh;
display: grid;
/*min() i only need the smallest value */
grid-template-columns: 1fr min(100% - 30px, 728px) 1fr;
grid-template-rows: 1fr 80vh 1fr;
background: #444;
}
After:
.main{
height: 100vh;
display: grid;
grid-template-columns: 1fr min(100% - 30px, 728px, var(--content-size)) 1fr;
grid-template-rows: 1fr 80vh 1fr;
background: #444;
}
Now let’s create a function to insert this CSS variable into our stylesheet:
const insertCssVar = (width) => {
const styles = `:root {--content-size: ${width}${meas};}`
const styleSheet = document.createElement('style')
styleSheet.innerText = styles
styleSheet.id = `content_width`
document.head.appendChild(styleSheet)
}
Last but not least, we need to run this function when the body tag loads, and when the Save button is clicked.
//update the body tag onload event:
<body class="body" onload="insertCssVar(728)">
Note that I’m inserting a starting width of 728 when the body loads.
Next we update the saveWidth function:
const saveWidth = () => {
localStorage.setItem('content-width', width)
insertCssVar(width)
}
Alerting the user when they use a width that’s too large
Okay, now we have an input that allows our users to insert a width for the content area. And they can choose em, %, or pixel widths. But our CSS is limiting them to a maximum of 728px or smaller. What happens if they put in a value that’s larger than 728px? The layout won’t change and they’ll be stuck there scratching their heads!
Let’s create a function to run inside the saveWidth function that will alert the user that the width they’ve chosen is too large.
First, let’s create a function:
const alertUser = (width, meas) => {
if ((width > 45.5 && meas === 'em') || (width > 728 && meas === 'px') || (width > 40 && meas === '%')) {
contentWidth.classList.add('error')
window.confirm("This size is larger than the allowed size. Please try a smaller size.")
}
if ((width <= 45.5 && meas === 'em') || (width <= 728 && meas === 'px') || (width <= 40 && meas === '%')) {
contentWidth.classList.remove('error')
}
}
The above code is taking in the width, and measurement as arguments and then checks if the measurement is greater than 45.5em, 728px, or 40%. If it is, we’re setting a class of “error” and triggering a confirmation box to alert the user they chose a width that was too large.
The function is also checking that if the width is equal to or less than 45.5em, 728px, and 40%, remove the error class.
Speaking of the error class, let’s go ahead and set that up. In our CSS let’s add this class:
.error {
outline: 2px solid red;
color: red;
}
Now, we just need to call this function inside the save function:
const saveWidth = () => {
localStorage.setItem('content-width', width)
alertUser(width, meas)
insertCssVar(width)
}
And to wrap it all up, when the user clicks the clear button, let’s reset the input and remove the error class as well:
const clearWidth = () => {
localStorage.removeItem('content-width')
contentWidth.classList.remove('error')
contentWidth.value = ''
}
Since we’re using local storage in our project, you’ll want to visit the actual pen to test this code out. To see the full use of this, see the code on codepen here.
And that’s it! In this article we covered making a fluid grid with fraction measurements, percentages, and static measurements like pixels. We’ve covered how to use static measurements while still maintaining a responsive grid. And I’ve even shown you how to maintain a fluid CSS grid with static measurements AND user input! I hope you had fun, I know I did!
Photo by Kelly Sikkema on Unsplash