{"id":380,"date":"2024-05-22T15:00:00","date_gmt":"2024-05-22T15:00:00","guid":{"rendered":"https:\/\/kerrprogroup.com\/?p=380"},"modified":"2025-03-19T12:20:49","modified_gmt":"2025-03-19T12:20:49","slug":"modern-css-layouts-you-might-not-need-a-framework-for-that","status":"publish","type":"post","link":"https:\/\/kerrprogroup.com\/index.php\/2024\/05\/22\/modern-css-layouts-you-might-not-need-a-framework-for-that\/","title":{"rendered":"Modern CSS Layouts: You Might Not Need A Framework For That"},"content":{"rendered":"

Modern CSS Layouts: You Might Not Need A Framework For That<\/title><\/p>\n<article>\n<header>\n<h1>Modern CSS Layouts: You Might Not Need A Framework For That<\/h1>\n<address>Brecht De Ruyte<\/address>\n<p> 2024-05-22T15:00:00+00:00<br \/>\n 2025-03-19T12:04:52+00:00<br \/>\n <\/header>\n<p>Establishing layouts in CSS is something that we, as developers, often delegate to whatever framework we\u2019re most comfortable using. And even though it\u2019s possible to configure a framework to get just what we need out of it, how often have you integrated an entire CSS library simply for its layout features? I\u2019m sure many of us have done it at some point, dating back to the days of 960.gs, Bootstrap, Susy, and Foundation.<\/p>\n<p>Modern CSS features have significantly cut the need to reach for a framework simply for its layout. Yet, I continue to see it happen. Or, I empathize with many of my colleagues who find themselves re-creating the same Grid or Flexbox layout over and over again.<\/p>\n<p>In this article, we will gain greater control over web layouts. <strong>Specifically, we will create four CSS classes that you will be able to take and use immediately on just about any project or place where you need a particular layout that can be configured to your needs.<\/strong><\/p>\n<p>While the concepts we cover are key, the real thing I want you to take away from this is the <strong>confidence to use CSS for those things we tend to avoid doing ourselves<\/strong>. Layouts <em>used<\/em> to be a challenge on the same level of styling form controls. Certain creative layouts may still be difficult to pull off, but the way CSS is designed today solves the burdens of the established layout patterns we\u2019ve been outsourcing and re-creating for many years.<\/p>\n<h2 id=\"what-we-re-making\">What We\u2019re Making<\/h2>\n<p>We\u2019re going to establish four CSS classes, each with a different layout approach. The idea is that if you need, say, a fluid layout based on Flexbox, you have it ready. The same goes for the three other classes we\u2019re making.<\/p>\n<p>And what exactly are these classes? Two of them are Flexbox layouts, and the other two are Grid layouts, each for a specific purpose. We\u2019ll even extend the Grid layouts to leverage CSS Subgrid for when that\u2019s needed.<\/p>\n<p>Within those two groups of Flexbox and Grid layouts are two utility classes: one that auto-fills the available space \u2014 we\u2019re calling these <strong>\u201cfluid\u201d layouts<\/strong> \u2014 and another where we have greater control over the columns and rows \u2014 we\u2019re calling these <strong>\u201crepeating\u201d layouts<\/strong>.<\/p>\n<p>Finally, we\u2019ll integrate CSS Container Queries so that these layouts respond to their own size for responsive behavior rather than the size of the viewport. Where we\u2019ll start, though, is organizing our work into Cascade Layers, which further allow you to control the level of specificity and prevent style conflicts with your own CSS.<\/p>\n<div data-audience=\"non-subscriber\" data-remove=\"true\" class=\"feature-panel-container\">\n<aside class=\"feature-panel\">\n<div class=\"feature-panel-left-col\">\n<div class=\"feature-panel-description\">\n<p>Meet <strong><a data-instant href=\"https:\/\/www.smashingconf.com\/online-workshops\/\">Smashing Workshops<\/a><\/strong> on <strong>front-end, design & UX<\/strong>, with practical takeaways, live sessions, <strong>video recordings<\/strong> and a friendly Q&A. With Brad Frost, St\u00e9ph Walter and <a href=\"https:\/\/smashingconf.com\/online-workshops\/workshops\">so many others<\/a>.<\/p>\n<p><a data-instant href=\"smashing-workshops\" class=\"btn btn--green btn--large\">Jump to the workshops\u00a0\u21ac<\/a><\/div>\n<\/div>\n<div class=\"feature-panel-right-col\"><a data-instant href=\"smashing-workshops\" class=\"feature-panel-image-link\"><\/p>\n<div class=\"feature-panel-image\">\n<img decoding=\"async\" loading=\"lazy\" class=\"feature-panel-image-img lazyload\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Feature Panel\" width=\"257\" height=\"355\" data-src=\"\/images\/smashing-cat\/cat-scubadiving-panel.svg\"><\/p>\n<\/div>\n<p><\/a>\n<\/div>\n<\/aside>\n<\/div>\n<h2 id=\"setup-cascade-layers-css-variables\">Setup: Cascade Layers & CSS Variables<\/h2>\n<p>A technique that I\u2019ve used a few times is to define Cascade Layers at the start of a stylesheet. I like this idea not only because it keeps styles neat and organized but also because we can influence the specificity of the styles in each layer by organizing the layers in a specific order. All of this makes the utility classes we\u2019re making easier to maintain and integrate into your own work without running into specificity battles.<\/p>\n<p>I think the following three layers are enough for this work:<\/p>\n<pre><code class=\"language-css\">@layer reset, theme, layout;\n<\/code><\/pre>\n<p>Notice the order because it really, really matters. The <code>reset<\/code> layer comes first, making it the <em>least<\/em> specific layer of the bunch. The <code>layout<\/code> layer comes in at the end, making it the <em>most<\/em> specific set of styles, giving them higher priority than the styles in the other two layers. If we add an unlayered style, that one would be added last and thus have the highest specificity.<\/p>\n<figure class=\"\n \n break-out article__image\n \n \n \"><\/p>\n<p> <a href=\"https:\/\/files.smashing.media\/articles\/modern-css-layouts-no-framework-needed\/1-css-layers.png\"><\/p>\n<p> <img decoding=\"async\" loading=\"lazy\" width=\"800\" height=\"623\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Chrome DevTools in Chrome showing CSS layers\" class=\"lazyload\" data-src=\"https:\/\/res.cloudinary.com\/indysigner\/image\/fetch\/f_auto,q_80\/w_400\/https:\/\/files.smashing.media\/articles\/modern-css-layouts-no-framework-needed\/1-css-layers.png\"><\/p>\n<p> <\/a><figcaption class=\"op-vertical-bottom\">\n Figure 1: Inspecting Cascade Layers in Chrome\u2019s DevTools. (<a href=\"https:\/\/files.smashing.media\/articles\/modern-css-layouts-no-framework-needed\/1-css-layers.png\">Large preview<\/a>)<br \/>\n <\/figcaption><\/figure>\n<p><strong>Related<\/strong>: \u201c<a href=\"https:\/\/www.smashingmagazine.com\/2022\/01\/introduction-css-cascade-layers\/\">Getting Started With Cascade Layers<\/a>\u201d by Stephanie Eckles.<\/p>\n<p>Let\u2019s briefly cover how we\u2019ll use each layer in our work.<\/p>\n<h3 id=\"reset-layer\">Reset Layer<\/h3>\n<p>The <code>reset<\/code> layer will contain styles <a href=\"https:\/\/css-tricks.com\/reboot-resets-reasoning\/\">for any user agent styles we want to \u201creset\u201d<\/a>. You can add your own resets here, or if you already have a reset in your project, you can safely move on without this particular layer. However, do remember that un-layered styles will be read last, so wrap them in this layer if needed.<\/p>\n<p>I\u2019m just going to drop in <a href=\"https:\/\/www.smashingmagazine.com\/2012\/06\/coding-qa-with-chris-coyier-box-sizing-and-css-sprites\/#box-sizing\">the popular <code>box-sizing<\/code> declaration<\/a> that ensures all elements are sized consistently by the <code>border-box<\/code> in accordance with the CSS Box Model.<\/p>\n<pre><code class=\"language-css\">@layer reset {\n *,\n *::before,\n *::after {\n box-sizing: border-box;\n }\n\n body {\n margin: 0;\n }\n}\n<\/code><\/pre>\n<h3 id=\"theme-layer\">Theme Layer<\/h3>\n<p>This layer provides variables scoped to the <code>:root<\/code> element. I like the idea of scoping variables this high up the chain because layout containers \u2014 like the utility classes we\u2019re creating \u2014 are often wrappers around lots of other elements, and a global scope ensures that the variables are available anywhere we need them. That said, it is possible to scope these locally to another element if you need to.<\/p>\n<p>Now, whatever makes for \u201cgood\u201d default values for the variables will absolutely depend on the project. I\u2019m going to set these with particular values, but do not assume for a moment that you have to stick with them \u2014 this is very much a configurable system that you can adapt to your needs.<\/p>\n<p>Here are the only three variables we need for all four layouts:<\/p>\n<pre><code class=\"language-css\">@layer theme {\n :root {\n --layout-fluid-min: 35ch;\n --layout-default-repeat: 3;\n --layout-default-gap: 3vmax;\n }\n}\n<\/code><\/pre>\n<p>In order, these map to the following:<\/p>\n<ul>\n<li>Automatically-sized columns that are <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/length#ch\">at least 35 characters wide<\/a>,<\/li>\n<li>A layout with three <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/length#ch\">repeated columns<\/a>, and<\/li>\n<li>A <a href=\"https:\/\/css-tricks.com\/almanac\/properties\/g\/gap\/\">gap<\/a> between the layout items that is set to 3% of the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/length#vmax\">largest side of the viewport<\/a>.<\/li>\n<\/ul>\n<p><strong>Notice<\/strong>: <em>The variables are prefixed with <code>layout-<\/code>, which I\u2019m using as an identifier for layout-specific values. This is my personal preference for structuring this work, but please choose a naming convention that fits your mental model \u2014 <a href=\"https:\/\/css-tricks.com\/naming-things-is-only-getting-harder\/\">naming things can be hard<\/a>!<\/em><\/p>\n<h3 id=\"layout-layer\">Layout Layer<\/h3>\n<p>This layer will hold our utility class rulesets, which is where all the magic happens. For the grid, we will include a fifth class specifically for using CSS Subgrid within a grid container for those possible use cases.<\/p>\n<pre><code class=\"language-css\">@layer layout { \n .repeating-grid {}\n .repeating-flex {}\n .fluid-grid {}\n .fluid-flex {}\n\n .subgrid-rows {}\n}\n<\/code><\/pre>\n<p>Now that all our layers are organized, variables are set, and rulesets are defined, we can begin working on the layouts themselves. We will start with the \u201crepeating\u201d layouts, one based on CSS Grid and the other using Flexbox.<\/p>\n<h2 id=\"repeating-grid-and-flex-layouts\">Repeating Grid And Flex Layouts<\/h2>\n<p>I think it\u2019s a good idea to start with the \u201csimplest\u201d layout and scale up the complexity from there. So, we\u2019ll tackle the \u201cRepeating Grid\u201d layout first as an introduction to the overarching technique we will be using for the other layouts.<\/p>\n<h3 id=\"repeating-grid\">Repeating Grid<\/h3>\n<p>If we head into the <code>@layout<\/code> layer, that\u2019s where we\u2019ll find the <code>.repeating-grid<\/code> ruleset, where we\u2019ll write the styles for this specific layout. Essentially, we are setting this up as a grid container and applying the variables we created to it to establish layout columns and spacing between them.<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-css\">.repeating-grid {\n display: grid;\n grid-template-columns: repeat(var(--layout-default-repeat), 1fr);\n gap: var(--layout-default-gap);\n}\n<\/code><\/pre>\n<\/div>\n<p>It\u2019s not too complicated so far, right? We now have a grid container with three equally sized columns that take up one fraction (<code>1fr<\/code>) of the available space with a gap between them.<\/p>\n<p>This is all fine and dandy, but we do want to take this a step further and turn this into a system where you can configure the number of columns and the size of the gap. I\u2019m going to introduce two new variables scoped to this grid:<\/p>\n<ul>\n<li><strong><code>--_grid-repeat<\/code><\/strong>: The number of grid columns.<\/li>\n<li><strong><code>--_repeating-grid-gap<\/code><\/strong>: The amount of space between grid items.<\/li>\n<\/ul>\n<p>Did you notice that I\u2019ve prefixed these variables with an underscore? This was actually a JavaScript convention to specify variables that are \u201cprivate\u201d \u2014 or locally-scoped \u2014 before we had <code>const<\/code> and <code>let<\/code> to help with that. Feel free to rename these however you see fit, but I wanted to note that up-front in case you\u2019re wondering why the underscore is there.<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-css\">.repeating-grid {\n --_grid-repeat: var(--grid-repeat, var(--layout-default-repeat));\n --_repeating-grid-gap: var(--grid-gap, var(--layout-default-gap));\n\n display: grid;\n grid-template-columns: repeat(var(--layout-default-repeat), 1fr);\n gap: var(--layout-default-gap);\n}\n<\/code><\/pre>\n<\/div>\n<p><strong>Notice<\/strong>: <em>These variables are set to the variables in the <code>@theme<\/code> layer. I like the idea of assigning a global variable to a locally-scoped variable. This way, we get to leverage the default values we set in <code>@theme<\/code> but can easily override them without interfering anywhere else the global variables are used.<\/em><\/p>\n<p>Now let\u2019s put those variables to use on the style rules from before in the same <code>.repeating-grid<\/code> ruleset:<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-css\">.repeating-grid {\n --_grid-repeat: var(--grid-repeat, var(--layout-default-repeat));\n --_repeating-grid-gap: var(--grid-gap, var(--layout-default-gap));\n\n display: grid;\n grid-template-columns: repeat(var(--_grid-repeat), 1fr);\n gap: var(--_repeating-grid-gap);\n}\n<\/code><\/pre>\n<\/div>\n<p>What happens from here when we apply the <code>.repeating-grid<\/code> to an element in HTML? Let\u2019s imagine that we are working with the following simplified markup:<\/p>\n<pre><code class=\"language-html\"><section class=\"repeating-grid\">\n <div><\/div>\n <div><\/div>\n <div><\/div>\n<\/section>\n<\/code><\/pre>\n<p>If we were to apply a <code>background-color<\/code> and <code>height<\/code> to those divs, we would get a nice set of boxes that are placed into three equally-sized columns, where any divs that do not fit on the first row automatically wrap to the next row.<\/p>\n<figure class=\"break-out\">\n<p data-height=\"480\" data-theme-id=\"light\" data-slug-hash=\"gOJrqmL\" data-user=\"smashingmag\" data-default-tab=\"result\" class=\"codepen\">See the Pen [Layout Utility: Repeating Grid [forked]](https:\/\/codepen.io\/smashingmag\/pen\/gOJrqmL) by <a href=\"https:\/\/codepen.io\/geoffgraham\">Geoff Graham<\/a>.<\/p><figcaption>See the Pen <a href=\"https:\/\/codepen.io\/smashingmag\/pen\/gOJrqmL\">Layout Utility: Repeating Grid [forked]<\/a> by <a href=\"https:\/\/codepen.io\/geoffgraham\">Geoff Graham<\/a>.<\/figcaption><\/figure>\n<p>Now, of course, we don\u2019t <em>have<\/em> to have just three columns. Let\u2019s say we want a product grid where we want to change the repeating columns from <code>3<\/code> to <code>5<\/code> while updating the <code>gap<\/code> from <code>2vw<\/code> to <code>3vw<\/code> using the same HTML, only with a new class we can use override those values.<\/p>\n<pre><code class=\"language-html\"><section class=\"repeating-grid products-grid\">\n <div><\/div>\n <div><\/div>\n <div><\/div>\n <div><\/div>\n <div><\/div>\n<\/section>\n<\/code><\/pre>\n<p>See how this is shaping up? We have a grid layout based on a set of globally-scoped variables that we can re-assign to variables that are locally-scoped to the utility class and further customized with a class of our own that adds context to the element\u2019s purpose and allows you to adjust the responsive behavior.<\/p>\n<pre><code class=\"language-css\">.products-grid {\n --grid-repeat: 2;\n --grid-gap: 2vw;\n\n @media (width >= 1000px) {\n --grid-repeat: 3;\n --grid-gap: 3vw;\n }\n}\n<\/code><\/pre>\n<figure class=\"break-out\">\n<p data-height=\"480\" data-theme-id=\"light\" data-slug-hash=\"YzbqBVy\" data-user=\"smashingmag\" data-default-tab=\"result\" class=\"codepen\">See the Pen [Layout Utility: Repeating Grid [forked]](https:\/\/codepen.io\/smashingmag\/pen\/YzbqBVy) by <a href=\"https:\/\/codepen.io\/geoffgraham\">Geoff Graham<\/a>.<\/p><figcaption>See the Pen <a href=\"https:\/\/codepen.io\/smashingmag\/pen\/YzbqBVy\">Layout Utility: Repeating Grid [forked]<\/a> by <a href=\"https:\/\/codepen.io\/geoffgraham\">Geoff Graham<\/a>.<\/figcaption><\/figure>\n<p>The benefit is that we can overwrite our default values without polluting the HTML with superfluous classes. This is the <strong>overarching approach<\/strong> we will also use in the three other layout classes. Next up is the \u201cRepeating Flex\u201d version of what we just made.<\/p>\n<h3 id=\"repeating-flex\">Repeating Flex<\/h3>\n<p>The \u201cRepeating Grid\u201d layout is great, but you might not always want equally-sized columns. CSS Grid is certainly capable of auto-filling elements with whatever space is available, but Flexbox is extremely proficient at it.<\/p>\n<p>Let\u2019s say we have the same five divs from before. That leaves us with two divs on the second row next to an empty column on the right. Perhaps we want those last two leftover divs to stretch out and take up the space in the empty column.<\/p>\n<figure class=\"\n \n break-out article__image\n \n \n \"><\/p>\n<p> <a href=\"https:\/\/files.smashing.media\/articles\/modern-css-layouts-no-framework-needed\/2-repeating-flex.png\"><\/p>\n<p> <img decoding=\"async\" loading=\"lazy\" width=\"800\" height=\"262\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Two rows of pink rectangles, with three boxes on the top and two boxes on the bottom.\" class=\"lazyload\" data-src=\"https:\/\/res.cloudinary.com\/indysigner\/image\/fetch\/f_auto,q_80\/w_400\/https:\/\/files.smashing.media\/articles\/modern-css-layouts-no-framework-needed\/2-repeating-flex.png\"><\/p>\n<p> <\/a><figcaption class=\"op-vertical-bottom\">\n Figure 2: Flexible items automatically fill any remaining space that would otherwise be presented as an empty third column on the second row. (<a href=\"https:\/\/files.smashing.media\/articles\/modern-css-layouts-no-framework-needed\/2-repeating-flex.png\">Large preview<\/a>)<br \/>\n <\/figcaption><\/figure>\n<p>Time to put the process we established with the Repeating Grid layout to use in this Repeating Flex layout. This time, we jump straight to defining the private variables on the <code>.repeating-flex<\/code> ruleset in the <code>@layout<\/code> layer since we already know what we\u2019re doing.<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-css\">.repeating-flex {\n --_flex-repeat: var(--flex-repeat, var(--layout-default-repeat));\n --_repeating-flex-gap: var(--flex-gap, var(--layout-default-gap));\n}\n<\/code><\/pre>\n<\/div>\n<p>Again, we have two locally-scoped variables used to override the default values assigned to the globally-scoped variables. Now, we apply them to the style declarations.<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-css\">.repeating-flex {\n --_flex-repeat: var(--flex-repeat, var(--layout-default-repeat));\n --_repeating-flex-gap: var(--flex-gap, var(--layout-default-gap));\n\n display: flex;\n flex-wrap: wrap;\n gap: var(--_repeating-flex-gap);\n}\n<\/code><\/pre>\n<\/div>\n<p>We\u2019re only using one of the variables to set the gap size between flex items at the moment, but that will change in a bit. For now, the important thing to note is that <strong>we are using the <code>flex-wrap<\/code> property to tell Flexbox that it\u2019s OK to let additional items in the layout wrap into multiple rows rather than trying to pack everything in a single row<\/strong>.<\/p>\n<p>But once we do that, we also have to configure how the flex items shrink or expand based on whatever amount of available space is remaining. Let\u2019s <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/CSS_nesting\/Using_CSS_nesting\">nest those styles<\/a> inside the parent ruleset:<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-css\">.repeating-flex {\n --_flex-repeat: var(--flex-repeat, var(--layout-default-repeat));\n --_repeating-flex-gap: var(--flex-gap, var(--layout-default-gap));\n\n display: flex;\n flex-wrap: wrap;\n gap: var(--_repeating-flex-gap);\n\n > * {\n flex: 1 1 calc((100% \/ var(--_flex-repeat)) - var(--_gap-repeater-calc));\n }\n}\n<\/code><\/pre>\n<\/div>\n<p>If you\u2019re wondering why I\u2019m using the universal selector (<code>*<\/code>), it\u2019s because we can\u2019t assume that the layout items will always be divs. Perhaps they are <code><\/p>\n<article><\/article>\n<p><\/code> elements, <code><\/p>\n<section><\/section>\n<p><\/code>s, or something else entirely. The child combinator (<code>><\/code>) ensures that we\u2019re only selecting elements that are <em>direct<\/em> children of the utility class to prevent leakage into other ancestor styles.<\/p>\n<p>The <code>flex<\/code> shorthand property is one of those that\u2019s been around for many years now but still seems to mystify many of us. Before we unpack it, did you also notice that we have a new locally-scoped <code>--_gap-repeater-calc<\/code> variable that needs to be defined? Let\u2019s do this:<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-css\">.repeating-flex {\n --_flex-repeat: var(--flex-repeat, var(--layout-default-repeat));\n --_repeating-flex-gap: var(--flex-gap, var(--layout-default-gap));\n\n \/* New variables *\/\n --_gap-count: calc(var(--_flex-repeat) - 1);\n --_gap-repeater-calc: calc(\n var(--_repeating-flex-gap) \/ var(--_flex-repeat) * var(--_gap-count)\n );\n \n display: flex;\n flex-wrap: wrap;\n gap: var(--_repeating-flex-gap);\n\n > * {\n flex: 1 1 calc((100% \/ var(--_flex-repeat)) - var(--_gap-repeater-calc));\n }\n}\n<\/code><\/pre>\n<\/div>\n<p>Whoa, we actually created a second variable that <code>--_gap-repeater-calc<\/code> can use to properly calculate the third <code>flex<\/code> value, which corresponds to the <code>flex-basis<\/code> property, i.e., the \u201cideal\u201d size we want the flex items to be.<\/p>\n<p>If we take out the variable abstractions from our code above, then this is what we\u2019re looking at:<\/p>\n<pre><code class=\"language-css\">.repeating-flex {\n display: flex;\n flex-wrap: wrap;\n gap: 3vmax\n\n > * {\n flex: 1 1 calc((100% \/ 3) - calc(3vmax \/ 3 * 2));\n }\n}\n<\/code><\/pre>\n<p>Hopefully, this will help you see what sort of math the browser has to do to size the flexible items in the layout. Of course, those values change if the variables\u2019 values change. But, in short, elements that are direct children of the <code>.repeating-flex<\/code> utility class are allowed to grow (<code>flex-grow: 1<\/code>) and shrink (<code>flex-shrink: 1<\/code>) based on the amount of available space while we inform the browser that the initial size (i.e., <code>flex-basis<\/code>) of each flex item is equal to some <code>calc()<\/code>-ulated value.<\/p>\n<p>Because we had to introduce a couple of new variables to get here, I\u2019d like to at least explain what they do:<\/p>\n<ul>\n<li><strong><code>--_gap-count<\/code><\/strong>: This stores the number of gaps between layout items by subtracting 1 <code>from --_flex-repeat<\/code>. There\u2019s one less gap in the number of items because there\u2019s no gap before the first item or after the last item.<\/li>\n<li><strong><code>--_gap-repeater-calc<\/code><\/strong>: This calculates the total gap size based on the individual item\u2019s gap size and the total number of gaps between items.<\/li>\n<\/ul>\n<p>From there, we calculate the total gap size more efficiently with the following formula:<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-javascript\">calc(var(--_repeating-flex-gap) \/ var(--_flex-repeat) * var(--_gap-count))\n<\/code><\/pre>\n<\/div>\n<p>Let\u2019s break that down further because it\u2019s an inception of variables referencing other variables. In this example, we already provided our repeat-counting private variable, which falls back to the default repeater by setting the <code>--layout-default-repeat<\/code> variable.<\/p>\n<p>This sets a gap, but we\u2019re not done yet because, with flexible containers, we need to define the <code>flex<\/code> behavior of the container\u2019s direct children so that they grow (<code>flex-grow: 1<\/code>), shrink (<code>flex-shrink: 1<\/code>), and with a <code>flex-basis<\/code> value that is calculated by multiplying the repeater by the total number of gaps between items.<\/p>\n<p>Next, we divide the individual gap size (<code>--_repeating-flex-gap<\/code>) by the number of repetitions (<code>--_flex-repeat)<\/code>) to equally distribute the gap size between each item in the layout. Then, we multiply that gap size value by one minus the total number of gaps with the <code>--_gap-count<\/code> variable.<\/p>\n<p>And that concludes our repeating grids! Pretty fun, or at least interesting, right? I like a bit of math.<\/p>\n<p>Before we move to the final two layout utility classes we\u2019re making, you might be wondering why we want so many abstractions of the same variable, as we start with one globally-scoped variable referenced by a locally-scoped variable which, in turn, can be referenced and overridden again by yet another variable that is locally scoped to another ruleset. We could simply work with the global variable the whole time, but I\u2019ve taken us through the extra steps of abstraction.<\/p>\n<p>I like it this way because of the following:<\/p>\n<ol>\n<li>I can peek at the HTML and instantly see which layout approach is in use: <code>.repeating-grid<\/code> or <code>.repeating-flex<\/code>.<\/li>\n<li>It maintains a certain separation of concerns that keeps styles in order without running into specificity conflicts.<\/li>\n<\/ol>\n<p>See how clear and understandable the markup is:<\/p>\n<pre><code class=\"language-html\"><section class=\"repeating-flex footer-usps\">\n <div><\/div>\n <div><\/div>\n <div><\/div>\n<\/section>\n<\/code><\/pre>\n<p>The corresponding CSS is likely to be a slim ruleset for the semantic <code>.footer-usps<\/code> class that simply updates variable values:<\/p>\n<pre><code class=\"language-css\">.footer-usps {\n --flex-repeat: 3;\n --flex-gap: 2rem;\n}\n<\/code><\/pre>\n<p>This gives me all of the context I need: the type of layout, what it is used for, and where to find the variables. I think that\u2019s handy, but you certainly could get by without the added abstractions if you\u2019re looking to streamline things a bit.<\/p>\n<div class=\"partners__lead-place\"><\/div>\n<h2 id=\"fluid-grid-and-flex-layouts\">Fluid Grid And Flex Layouts<\/h2>\n<p>All the repeating we\u2019ve done until now is fun, and we can manipulate the number of repeats with container queries and media queries. But rather than repeating columns manually, let\u2019s make the browser do the work for us with fluid layouts that automatically fill whatever empty space is available in the layout container. We may sacrifice a small amount of control with these two utilities, but we get to leverage the browser\u2019s ability to \u201cintelligently\u201d place layout items with a few CSS hints.<\/p>\n<h3 id=\"fluid-grid\">Fluid Grid<\/h3>\n<p>Once again, we\u2019re starting with the variables and working our way to the calculations and style rules. Specifically, we\u2019re defining a variable called <code>--_fluid-grid-min<\/code> that manages a column\u2019s minimum width.<\/p>\n<p>Let\u2019s take a rather trivial example and say we want a grid column that\u2019s at least <code>400px<\/code> wide with a <code>20px<\/code> gap. In this situation, we\u2019re essentially working with a two-column grid when the container is greater than <code>820px<\/code> wide. If the container is narrower than <code>820px<\/code>, the column stretches out to the container\u2019s full width.<\/p>\n<p>If we want to go for a three-column grid instead, the container\u2019s width should be about <code>1240px<\/code> wide. It\u2019s all about controlling the minimum sizing values in the gap.<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-css\">.fluid-grid {\n --_fluid-grid-min: var(--fluid-grid-min, var(--layout-fluid-min));\n --_fluid-grid-gap: var(--grid-gap, var(--layout-default-gap));\n}\n<\/code><\/pre>\n<\/div>\n<p>That establishes the variables we need to calculate and set styles on the <code>.fluid-grid<\/code> layout. This is the full code we are unpacking:<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-css\"> .fluid-grid {\n --_fluid-grid-min: var(--fluid-grid-min, var(--layout-fluid-min));\n --_fluid-grid-gap: var(--grid-gap, var(--layout-default-gap));\n\n display: grid;\n grid-template-columns: repeat(\n auto-fit,\n minmax(min(var(--_fluid-grid-min), 100%), 1fr)\n );\n gap: var(--_fluid-grid-gap);\n}\n<\/code><\/pre>\n<\/div>\n<p>The <code>display<\/code> is set to <code>grid<\/code>, and the <code>gap<\/code> between items is based on the <code>--fluid-grid-gap<\/code> variable. The magic is taking place in the <code>grid-template-columns<\/code> declaration.<\/p>\n<p>This grid uses the <code>repeat()<\/code> function just as the <code>.repeating-grid<\/code> utility does. By declaring <code>auto-fit<\/code> in the function, the browser automatically packs in as many columns as it possibly can in the amount of available space in the layout container. Any columns that can\u2019t fit on a line simply wrap to the next line and occupy the full space that is available there.<\/p>\n<p>Then there\u2019s the <code>minmax()<\/code> function for setting the minimum and maximum width of the columns. What\u2019s special here is that we\u2019re nesting yet another function, <code>min()<\/code>, within <code>minmax()<\/code> (which, remember, is nested in the <code>repeat()<\/code> function). This a bit of extra logic that sets the minimum width value of each column somewhere in a range between <code>--_fluid-grid-min<\/code> and <code>100%<\/code>, where <code>100%<\/code> is a fallback for when <code>--_fluid-grid-min<\/code> is undefined or is less than <code>100%<\/code>. In other words, each column is at least the full 100% width of the grid container.<\/p>\n<p>The \u201cmax\u201d half of <code>minmax()<\/code> is set to <code>1fr<\/code> to ensure that each column grows proportionally and maintains equally sized columns.<\/p>\n<figure class=\"break-out\">\n<p data-height=\"480\" data-theme-id=\"light\" data-slug-hash=\"GRaZzMN\" data-user=\"smashingmag\" data-default-tab=\"result\" class=\"codepen\">See the Pen [Fluid grid [forked]](https:\/\/codepen.io\/smashingmag\/pen\/GRaZzMN) by <a href=\"https:\/\/codepen.io\/utilitybend\">utilitybend<\/a>.<\/p><figcaption>See the Pen <a href=\"https:\/\/codepen.io\/smashingmag\/pen\/GRaZzMN\">Fluid grid [forked]<\/a> by <a href=\"https:\/\/codepen.io\/utilitybend\">utilitybend<\/a>.<\/figcaption><\/figure>\n<p>That\u2019s it for the Fluid Grid layout! That said, please do take note that this is a <em>strong grid<\/em>, particularly when it is combined with modern relative units, e.g. <code>ch<\/code>, as it produces a grid that only scales from one column to multiple columns based on the size of the content.<\/p>\n<h3 id=\"fluid-flex\">Fluid Flex<\/h3>\n<p>We pretty much get to re-use all of the code we wrote for the Repeating Flex layout for the Fluid Flex layout, but only we\u2019re setting the <code>flex-basis<\/code> of each column by its minimum size rather than the number of columns.<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-css\">.fluid-flex {\n --_fluid-flex-min: var(--fluid-flex-min, var(--layout-fluid-min));\n --_fluid-flex-gap: var(--flex-gap, var(--layout-default-gap));\n\n display: flex;\n flex-wrap: wrap;\n gap: var(--_fluid-flex-gap);\n\n > * {\n flex: 1 1 var(--_fluid-flex-min);\n }\n}\n<\/code><\/pre>\n<\/div>\n<p>That completes the fourth and final layout utility \u2014 but there\u2019s one bonus class we can create to use together with the Repeating Grid and Fluid Grid utilities for even more control over each layout.<\/p>\n<h2 id=\"optional-subgrid-utility\">Optional: Subgrid Utility<\/h2>\n<p>Subgrid is handy because it turns any grid item into a grid container of its own that shares the parent container\u2019s track sizing to keep the two containers aligned without having to redefine tracks by hand. It\u2019s got <a href=\"https:\/\/caniuse.com\/css-subgrid\">full browser support<\/a> and makes our layout system just that much more robust. That\u2019s why we can set it up as a utility to use with the Repeating Grid and Fluid Grid layouts if we need any of the layout items to be grid containers for laying out any child elements they contain.<\/p>\n<p>Here we go:<\/p>\n<pre><code class=\"language-css\">.subgrid-rows {\n > * {\n display: grid;\n gap: var(--subgrid-gap, 0);\n grid-row: auto \/ span var(--subgrid-rows, 4);\n grid-template-rows: subgrid;\n }\n}\n<\/code><\/pre>\n<p>We have two new variables, of course:<\/p>\n<ul>\n<li><strong><code>--subgrid-gap<\/code><\/strong>: The vertical gap between grid items.<\/li>\n<li><strong><code>--subgrid-rows<\/code><\/strong> The number of grid rows defaulted to <code>4<\/code>.<\/li>\n<\/ul>\n<p>We have a bit of a challenge: <strong>How do we control the subgrid items in the rows?<\/strong> I see two possible methods.<\/p>\n<h3 id=\"method-1-inline-styles\">Method 1: Inline Styles<\/h3>\n<p>We already have a variable that can technically be used directly in the HTML as an inline style:<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-html\"><section class=\"fluid-grid subgrid-rows\" style=\"--subgrid-rows: 4\">\n <!-- items -->\n<\/section>\n<\/code><\/pre>\n<\/div>\n<p>This works like a charm since the variable informs the subgrid how much it can grow.<\/p>\n<h3 id=\"method-2-using-the-has-pseudo-class\">Method 2: Using The <code>:has()<\/code> Pseudo-Class<\/h3>\n<p>This approach leads to verbose CSS, but sacrificing brevity allows us to automate the layout so it handles practically anything we throw at it without having to update an inline style in the markup.<\/p>\n<p>Check this out:<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-css\">.subgrid-rows {\n &:has(> :nth-child(1):last-child) { --subgrid-rows: 1; }\n &:has(> :nth-child(2):last-child) { --subgrid-rows: 2; }\n &:has(> :nth-child(3):last-child) { --subgrid-rows: 3; }\n &:has(> :nth-child(4):last-child) { --subgrid-rows: 4; }\n &:has(> :nth-child(5):last-child) { --subgrid-rows: 5; }\n \/* etc. *\/\n\n > * {\n display: grid;\n gap: var(--subgrid-gap, 0);\n grid-row: auto \/ span var(--subgrid-rows, 5);\n grid-template-rows: subgrid;\n }\n}\n<\/code><\/pre>\n<\/div>\n<p>The <code>:has()<\/code> selector checks if a subgrid row is the last child item in the container when that item is either the first, second, third, fourth, fifth, and so on item. For example, the second declaration:<\/p>\n<pre><code class=\"language-css\">&:has(> :nth-child(2):last-child) { --subgrid-rows: 2; }\n<\/code><\/pre>\n<p>\u2026is pretty much saying, <em>\u201cIf this is the second subgrid item and it happens to be the last item in the container, then set the number of rows to <code>2<\/code>.\u201d<\/em><\/p>\n<p>Whether this is too heavy-handed, I don\u2019t know; but I love that we\u2019re able to do it in CSS.<\/p>\n<p>The final missing piece is to declare a container on our children. Let\u2019s give the columns a general class name, <code>.grid-item<\/code>, that we can override if we need to while setting each one as a <code>container<\/code> we can query for the sake of updating its layout when it is a certain size (as opposed to responding to the viewport\u2019s size in a media query).<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-css\">:is(.fluid-grid:not(.subgrid-rows),\n.repeating-grid:not(.subgrid-rows),\n.repeating-flex, .fluid-flex) {\n > * {\n container: var(--grid-item-container, grid-item) \/ inline-size;\n }\n}\n<\/code><\/pre>\n<\/div>\n<p>That\u2019s a wild-looking selector, but the verbosity is certainly kept to a minimum thanks to the <code>:is()<\/code> pseudo-class, which saves us from having to write this as a larger chain selector. It essentially selects the direct children of the other utilities without leaking into <code>.subgrid-rows<\/code> and inadvertently selecting its direct children.<\/p>\n<p>The <code>container<\/code> property is a shorthand that combines <code>container-name<\/code> and <code>container-type<\/code> into a single declaration separated by a forward slash (<code>\/<\/code>). The name of the container is set to one of our variables, and the type is always its <code>inline-size<\/code> (i.e., width in a horizontal writing mode).<\/p>\n<p>The <code>container-type<\/code> property can only be applied to grid <em>containers<\/em> \u2014 not grid <em>items<\/em>. This means we\u2019re unable to combine it with the <code>grid-template-rows: subgrid<\/code> value, which is why we needed to write a more complex selector to exclude those instances.<\/p>\n<div class=\"partners__lead-place\"><\/div>\n<h2 id=\"demo\">Demo<\/h2>\n<p>Check out the following demo to see how everything comes together.<\/p>\n<figure class=\"break-out\">\n<p data-height=\"480\" data-theme-id=\"light\" data-slug-hash=\"mdYPvLR\" data-user=\"smashingmag\" data-default-tab=\"result\" class=\"codepen\">See the Pen [Grid system playground [forked]](https:\/\/codepen.io\/smashingmag\/pen\/mdYPvLR) by <a href=\"https:\/\/codepen.io\/utilitybend\">utilitybend<\/a>.<\/p><figcaption>See the Pen <a href=\"https:\/\/codepen.io\/smashingmag\/pen\/mdYPvLR\">Grid system playground [forked]<\/a> by <a href=\"https:\/\/codepen.io\/utilitybend\">utilitybend<\/a>.<\/figcaption><\/figure>\n<p>The demo is pulling in styles from <a href=\"https:\/\/codepen.io\/utilitybend\/pen\/YzMvJmN\">another pen that contains the full CSS for everything we made<\/a> together in this article. So, if you were to replace the <code>.fluid-flex<\/code> classname from the parent container in the HTML with another one of the layout utilities, the layout will update accordingly, allowing you to compare them.<\/p>\n<p>Those classes are the following:<\/p>\n<ul>\n<li><code>.repeating-grid<\/code>,<\/li>\n<li><code>.repeating-flex<\/code>,<\/li>\n<li><code>.fluid-grid<\/code>,<\/li>\n<li><code>.fluid-flex<\/code>.<\/li>\n<\/ul>\n<p>And, of course, you have the option of turning any grid items into grid containers using the optional <code>.subgrid-rows<\/code> class in combination with the <code>.repeating-grid<\/code> and <code>.fluid-grid<\/code> utilities.<\/p>\n<h2 id=\"conclusion-write-once-and-repurpose\">Conclusion: Write Once And Repurpose<\/h2>\n<p>This was quite a journey, wasn\u2019t it? It might seem like a lot of information, but we made something that <strong>we only need to write once and can use practically anywhere we need a certain type of layout using modern CSS approaches<\/strong>. I strongly believe these utilities can not only help you in a bunch of your work but also cut any reliance on CSS frameworks that you may be using simply for its layout configurations.<\/p>\n<p>This is a combination of many techniques I\u2019ve seen, <a href=\"https:\/\/www.youtube.com\/watch?v=Y50iqMlrqU8\">one of them being a presentation Stephanie Eckles gave at CSS Day 2023<\/a>. I love it when people handcraft modern CSS solutions for things we used to work around. Stephanie\u2019s demonstration was clean from the start, which is refreshing as so many other areas of web development are becoming ever more complex.<\/p>\n<p>After learning a bunch from CSS Day 2023, I played with Subgrid on my own and <a href=\"https:\/\/utilitybend.com\/blog\/grid-ideas-creating-a-css-subgrid-utility-class-for-rows\">published different ideas from my experiments<\/a>. That\u2019s all it took for me to realize <strong>how extensible modern CSS layout approaches are<\/strong> and inspired me to create a set of utilities I could rely on, perhaps for a long time.<\/p>\n<p>By no means am I trying to convince you or anyone else that these utilities are perfect and should be used everywhere or even that they\u2019re better than <code><\/code>. One thing that I do know for certain is that by experimenting with the ideas we covered in this article, you will get a solid feel of how CSS is capable of making layout work much more convenient and robust than ever.<\/p>\n<p>Create something out of this, and share it in the comments if you\u2019re willing \u2014 I\u2019m looking forward to seeing some fresh ideas!<\/p>\n<div class=\"signature\">\n <img decoding=\"async\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Smashing Editorial\" width=\"35\" height=\"46\" loading=\"lazy\" class=\"lazyload\" data-src=\"https:\/\/www.smashingmagazine.com\/images\/logo\/logo--red.png\"><br \/>\n <span>(gg, yk)<\/span>\n<\/div>\n<\/article>\n","protected":false},"excerpt":{"rendered":"<p>Modern CSS Layouts: You Might Not Need A Framework For That Modern CSS Layouts: You Might Not Need A Framework For That Brecht De Ruyte 2024-05-22T15:00:00+00:00 2025-03-19T12:04:52+00:00 Establishing layouts in CSS is something that we, as developers, often delegate to whatever framework we\u2019re most comfortable using. And even though it\u2019s possible to configure a framework to get just what we need out of it, how often have you integrated an…<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[11],"tags":[],"_links":{"self":[{"href":"https:\/\/kerrprogroup.com\/index.php\/wp-json\/wp\/v2\/posts\/380"}],"collection":[{"href":"https:\/\/kerrprogroup.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/kerrprogroup.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/kerrprogroup.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/kerrprogroup.com\/index.php\/wp-json\/wp\/v2\/comments?post=380"}],"version-history":[{"count":1,"href":"https:\/\/kerrprogroup.com\/index.php\/wp-json\/wp\/v2\/posts\/380\/revisions"}],"predecessor-version":[{"id":381,"href":"https:\/\/kerrprogroup.com\/index.php\/wp-json\/wp\/v2\/posts\/380\/revisions\/381"}],"wp:attachment":[{"href":"https:\/\/kerrprogroup.com\/index.php\/wp-json\/wp\/v2\/media?parent=380"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/kerrprogroup.com\/index.php\/wp-json\/wp\/v2\/categories?post=380"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/kerrprogroup.com\/index.php\/wp-json\/wp\/v2\/tags?post=380"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}