In my current job I maintain a research institute website that’s been around for awhile and will (goddess willing) continue to stick around, so I’m always looking for ways to future-proof the codebase to go the distance. One way I make the site more maintainable is by updating its codebase to take advantage of modern CSS, especially in places I can reduce dependence on third-party frameworks.
My previous post proclaimed the power of native CSS to create adaptive and responsive image galleries. This post continues in the theme of writing techy love letters to modern CSS layout tools, but this time I’ll focus on using CSS Grid to recreate a Bootstrap layout.
I recently worked on a project involving a grid-style layout of cards holding calendar event info. My task was to add a new, separate data stream of calendar events below the existing one. To maintain visual consistency, I needed to match the responsive behavior of the new calendar section to the old, creating a seamless grid between sections and across viewports.
Two separate sections, same grid layout
The existing calendar section used Bootstrap to arrange events (list elements) into a responsive grid:
<ul class="row">
<li class="col-sm-6 col-md-4">...</li>
<li class="col-sm-6 col-md-4">...</li>
.
.
.
<li class="col-sm-6 col-md-4">...</li>
</ul>
For anyone unfamiliar with this layout tool, Bootstrap is a CSS library that works on a 12 column grid system. The way you make responsive layouts is by using classes to tell the browser how many of those 12 columns an element should take up at different predefined breakpoints.
In the above HTML block, the row class is a special Bootstrap container class used to hold child column elements (<li> elements in this case). The responsiveness of these column elements is defined by two Bootstrap classes:
col-6-sm class makes the element 6 columns wide, or half the container width, at small screens.col-md-4 makes the element 4 columns wide, or one-third the container width, at medium screens.There’s actually an additional hidden breakpoint for small screens. By default Bootstrap stretches column elements to 100% the width of the container whenever the viewport falls below the smallest breakpoint (576px), creating a single column of stacked elements. It’s like there’s an implicit class of col-xs-12 applied to every column element.
You might be wondering, why fix something that ain’t broke? Why not just apply the same Bootstrap layout to the new calendar section and be done with it? And to that I say, where’s the fun in that? Practially speaking, CSS Grid is far more powerful than Bootstrap1, and as I hope to show here, makes for a neater, more maintainable codebase.
Here’s the layout implemented with CSS Grid that results in almost the exact same (IMO slightly better) responsive behavior as the above Bootstrap version.
<ul class="max-4-grid">
<li>...</li>
<li>...</li>
.
.
.
<li>...</li>
</ul>
.max-4-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(max(260px, 100%/4), 1fr));
gap: 1.5rem;
}
At first it can look like an unappetizing parentheses salad so let’s break down the nested functions of the grid-template-columns line.
repeat + auto-fitWith the repeat and auto-fit combo, we’re telling Grid to place as many equal-size elements across the viewport as will fit within the Grid container. The auto-fit ensures that any overflowing elements will be wrapped to a new row.
The size of the individual grid elements placed by auto-fit is controlled by the minmax(max(260px, 100%/4), 1fr). It may seem confusing that we’re using a max function to set the minimum element width, but this setup allows us to set both an absolute and a relative minimum.
The max(260px, 100%/4) function specifies that an element cannot shrink smaller than 260 px wide or 1/4 the container width - whichever value is larger. Functionally this means an element can never be narrower than 260 px (absolute minimum) and also limits the grid to no more than 4 elements per row (relative minimum).
A sidenote: In practice, I never get more than 3 elements per row with this Grid columns logic. Glossing over the math of why2, this is due to the sneaky added width from gaps, padding, margins, etc., and the fact that I had set an absolute max width on the parent container.
Each element’s maximum width is set to 1fr, or 1 “fractional unit”, which basically means “take up whatever space is available”. This bit of code allows the grid elements to stretch evenly across the container so that no empty space is left at the end of a row.
Fun fact: This Grid pattern that uses the magic seasonings of
repeat,auto-fill, andminmaxhas been around since the early days of Grid, as evidenced by its appearance in the 2016 W3C Working Draft for Grid.
To summarize the grid-template-columns logic:
repeat and auto-fit place as many equal-width elements as will fit on a row(max(260px, 100%/4) sets minimum element width to 260px or 4 elements per row, whichever is larger1fr lets elements stretch to fill up any extra row spaceThe CSS Grid approach provides many advantages over the Bootstrap one, particularly when it comes to large projects with longevity in mind.
Grid is more flexible across pages and projects than Bootstrap. It requires no media queries or hard-coded breakpoints locked to a 12-col grid. For a real showstopper of a layout contortionist, you can even combine Grid with container queries (oooo aaaahhhh).
No empty space is left at the end of a row whenever there are fewer elements than can fill a row. This is what I meant when I said the Bootstrap and Grid layouts are almost exactly matched: the Bootstrap version reserves placeholder space while the Grid version allows elements to stretch and fill the row.
Bootstrap = rigid elements and unclaimed space; CSS Grid = stretchy elements that fill the row
The Grid implementation relies on native CSS instead of third-party framework tools. That means fewer dependencies and smaller bundle sizes. In my playbook, every small step towards escaping dependency hell is a major win. Plus it makes the layout easier to transpose into other projects where Bootstrap is nowhere to be seen.
Grid makes for more concise HTML, streamlines editing, and reduces “class soup”. The Grid layout class is plug-and-playable across the codebase as a single class that gets applied once to the parent grid element. Whenever I need to tweak this layout, I only need to change it in one place in the CSS.
With Bootstrap, I have to comb through the HTML to add/change multiple Bootstrap classes (class="col-sm-6 col-md-4 col-lg-3") on every child element. No thank you, that’s precious time I could’ve spent writing more love letters to CSS Grid.
With all these benefits over Bootstrap, I will continue to reach for CSS Grid as my go-to layout tool and look for opportunities to overhaul Bootstrap code in favor of modern CSS.
Official W3C specification for CSS Grid
Sara Soueidan’s CSS Tricks article from 2022 breaks down the Grid auto-fit vs auto-fill behavior
The focus of this exercise was to recreate one specific layout using CSS Grid. If you are interested in recreating a flexible and modular 12-column Bootstrap copycat using CSS Grid, MDN Web Docs shows you how.
Don’t get me wrong, Bootstrap is an amazing tool. I used it to build my first few websites and will continue to sing its praises as a wonderful starting point to learn responsive design or quickly launch a prototype. ↩
The reason we only get 3 instead of 4 elements per row has to do with a few things. First of all, the max conatiner width is set at 1200px. Based on our Grid logic, that means the maximum width of an element is 1200px/4 = 300px. But that doesn’t include spacing like grid gaps, which I had set to 1.5rem (24px). When we add 3 grid gaps between 4 cols, we get an extra 24px * 3 = 72 pixels that need to fit inside the container, making the total width required 4 * 300px + 72 px = 1272px. This exceeds the capactiy of our 1200px container, so auto-fit bumps the fourth element to the next row, and we never see more than 3 elements per row. [tldr;] spacing between elements often means that only n-1 elements will fit in a row. ↩