CSS grid is the latest layout mode which makes it a joy to create two-dimensional layouts, and with great browser support it can be used just about anywhere, even IE 10!
During an interview process with a company I came across a rather peculiar layout on their website, and since I was in the process of building this blog I just had to dig into it. Like many layouts it involves a centered element with a maximum width which contains the entire site's content, including an aside
along the main content.
But here's where things got really interesting.
The content on the page was effectively laid out into two columns, but the grid itself had 12 columns! The property in question was grid-template-columns: repeat(12,1fr)
, what was the purpose of this technique?
It seemed rather unconventional, and I'm still not sure if this is how grid is intended to be used, but it's clever! Let's work through what this technique can do for us with an interactive demo.
In the sandbox below is the 12 column layout I described earlier, more or less the way I found it. Try playing with the number of columns, the grid lines the article
or aside
span, or even adding some content!
import styles from './App.module.css'; export default function App() { return ( <main> <section> <h1>CSS Grid is Awesome</h1> <p>Grid is the latest and greatest layout mode.</p> <p> This grid is made of multiple columns in order to distribute elements dynamically based on the screen size. </p> <p> Try editing the number of columns and/or the grid-column property on the article and aside elements to change how much space they take up! </p> </section> <section> <h2 id="demo-1-grid-properties">Grid Properties</h2> <p> <code>grid-template-columns</code> controls the number and size of columns </p> <p> <code>grid-template-rows</code> controls the number and size of rows </p> <p> <code>gap</code> controls the amount of space between columns and rows </p> <p> <code>grid-column</code> controls the columns a particular element spans </p> <p> <code>grid-row</code> controls the rows a particular element spans </p> <p> <code>grid-area</code> controls the position of a particular element within defined grid areas </p> </section> <aside> <h3>Other Layout Modes</h3> <ul> <li>Flow</li> <li>Positioned</li> <li>Flexbox</li> </ul> </aside> </main> ); }
Notice how our grid is divided into 12 equal columns thanks to the fr unit, and we leave one full column in between the main content and the aside. The more columns we have, the smaller each individual column is and vice versa.
The gap property is great for adding a static amount of space between columns/rows, but here we're using a fractional column that is 1/12 of the width of the available space to create a more fluid experience, so I don't think we really need a gap between columns.
Go ahead, try changing grid-row-gap
to gap
and increase it, at some point a nasty horizontal scrollbar will appear! That is because the minimum content size of each column plus the gap exceeds the available space.
By adding a gap to each column we're effectively reducing its content space up to a certain point, until the size of the gap exceeds the size of the column, then its size is determined by the gap!
You see, once we start using grid columns as dynamic gaps, using the gap-column-gap
property itself becomes redundant since we're inserting it manually. We're essentially making our whole layout, including gaps, a function of the available space.
Our content takes up 8/12 or 2/3 of the available space, our aside takes 3/12, our gap takes 1/12, and each of those variables can be adjusted as needed, pretty neat!
You might now be wondering whether we need 12 columns, and we don't! We technically only need as many columns as we have content, plus however many columns for dynamic gaps.
We can achieve the same exact proportions from the 12 column layout by making a couple changes: set grid-template-columns: 8fr 1fr 3fr
on the main
element, grid-column: 1
on the section
, and grid-column: 3
on the aside
.
Since our fractions add up to 12 we're back to where we started and it even seems simpler, but we actually lost the flexibility of easily adding elements which partially span the main or aside elements. It might be rare, but it's something to consider!
We usually can't use the same fr
trick with rows like with columns, so I tend to use it as a static gap. As soon as an element's height exceeds its container and scrollbars are added the concept of fr
becomes less useful.
Watch what happens when we set grid-template-rows
and insert a row between the two section
elements:
import styles from './App.module.css'; export default function App() { return ( <main> <section> <h1>CSS Grid is Awesome</h1> <p>Grid is the latest and greatest layout mode.</p> <p> This grid is made of multiple columns in order to distribute elements dynamically based on the screen size. </p> <p> Try editing the number of columns and/or the grid-column property on the article and aside elements to change how much space they take up! </p> </section> <section className={styles.properties}> <h2 id="demo-2-grid-properties">Grid Properties</h2> <p> <code>grid-template-columns</code> controls the number and size of columns </p> <p> <code>grid-template-rows</code> controls the number and size of rows </p> <p> <code>gap</code> controls the amount of space between columns and rows </p> <p> <code>grid-column</code> controls the columns a particular element spans </p> <p> <code>grid-row</code> controls the rows a particular element spans </p> <p> <code>grid-area</code> controls the position of a particular element within defined grid areas </p> </section> <aside> <h3>Other Layout Modes</h3> <ul> <li>Flow</li> <li>Positioned</li> <li>Flexbox</li> </ul> </aside> </main> ); }
Essentially, the minimum height of the tallest 1fr
row becomes the value of 1fr
, so since we set each row to 1fr
, the height of each becomes the height of the second section which is the tallest.
That isn't what we want in the case of our dynamic divider, but there is one case I can think of fr
being useful in the context of rows...
Let's say we have a page that we don't envision growing in height beyond its container, for instance, a 404 page where we want to show our site's header, footer and some content in the middle.
We can make this easy by using grid, letting the header and footer rows be their minimum content size, then letting the middle row fill the rest of the space with fr
. Check it out!
import styles from './App.module.css'; export default function App() { return ( <div className={styles.wrapper}> <header className={styles.header}> <a>Home</a> <a>Blog</a> <a>About</a> </header> <main> <h1>404 Not Found</h1> <p>This page does not exist. Please check the URL and try again.</p> </main> <footer> <div className={styles.links}> <h3>Links</h3> <a>Github</a> <a>Twitter</a> <a>Stackoverflow</a> </div> </footer> </div> ); }
Here we're using the default grid flow where it lays content out in a single column, but we're setting three explicit rows with grid-template-rows: min-content 1fr min-content
.
The first and third elements within the grid, the header and footer, will be assigned to the first and third rows whose height will be min-content
. The second element, main
, will be assigned to the second row which fills the remaining space with 1fr
.
One important style here is min-height: 100svh
so the app fills the entire viewport and our footer sits nicely at the bottom of the screen. This property typically goes into an app's CSS reset on the body. We used to have to put width: 100%
on html
as well as body
, but now that dynamic viewport units are well-supported we can use those on the body and it just works.
We would still need this property if we wanted to achieve such a layout differently, for example we could change wrapper
to flexbox layout with display: flex; flex-direction: column;
and set margin-bottom: auto
on the main
element.
Pick your poison, it's a matter of feasibility and preference.
With a little knowledge of how grid works we can happily use sticky positioning with grid in most cases. If the content within the sticky element won't need to be scrolled, like the table of contents on this blog, all we need are these three properties:
Since grid children stretch by default like flex children, we need to set align-self: start
because if the bottom of the element is touching the bottom of its container it will scroll with the page, but that's usually all we need.
If the sticky element does need to be scrolled we hit a bit of a snag. First of all, the grid must have some scrollable content otherwise the element has no reason to stick.
Essentially how it behaves is, as you scroll the element will stick until a point depending on the height of the grid vs the height of the sticky element.
The sticky positioning algorithm will determine the right moment to scroll the sticky element so it finishes scrolling at the same time as the grid, that is when the bottom of an element lines up with the end of its containing block.
You see, grid areas themselves are not containing blocks. Sticky positioned elements are still technically in-flow and in flow layout, elements are contained by their parents which in this case is the grid itself.
Fear not, because there is a way to fix this by adding a new containing block for our tall, sticky friend. In the demo below is a holy grail layout with a single, sticky sidebar.
import styles from './App.module.css'; export default function App() { return ( <div className={styles.container}> <header> <h1>Yet Another Developer Blog</h1> </header> <main> <section> <h2 id="demo-4-title">CSS Grid is Awesome</h2> <p>Grid is the latest and greatest layout mode.</p> <p> This grid is made of multiple columns in order to distribute elements dynamically based on the screen size. </p> <p> Try editing the number of columns and/or the grid-column property on the article and aside elements to change how much space they take up! </p> </section> <section> <h2 id="demo-4-grid-properties">Grid Properties</h2> <p> <code>grid-template-columns</code> controls the number and size of columns </p> <p> <code>grid-template-rows</code> controls the number and size of rows </p> <p> <code>gap</code> controls the amount of space between columns and rows </p> <p> <code>grid-column</code> controls the columns a particular element spans </p> <p> <code>grid-row</code> controls the rows a particular element spans </p> <p> <code>grid-area</code> controls the position of a particular element within defined grid areas </p> </section> </main> <aside> <div className={styles.sticky}> <h3>Other Layout Modes</h3> <ol> <li>Flow</li> <li>Positioned</li> <li>Flexbox</li> <li>Float</li> <li>Multi-Column</li> <li>Flow</li> <li>Positioned</li> <li>Flexbox</li> <li>Float</li> <li>Multi-Column</li> <li>Flow</li> <li>Positioned</li> <li>Flexbox</li> <li>Float</li> <li>Multi-Column</li> <li>Flow</li> <li>Positioned</li> <li>Flexbox</li> <li>Float</li> <li>Multi-Column</li> <li>Flow</li> <li>Positioned</li> <li>Flexbox</li> <li>Float</li> <li>Multi-Column</li> <li>Flow</li> <li>Positioned</li> <li>Flexbox</li> <li>Float</li> <li>Multi-Column</li> <li>Flow</li> <li>Positioned</li> <li>Flexbox</li> <li>Float</li> <li>Multi-Column</li> <li>Flow</li> <li>Positioned</li> <li>Flexbox</li> <li>Float</li> <li>Multi-Column</li> <li>Flow</li> <li>Positioned</li> <li>Flexbox</li> <li>Float</li> <li>Multi-Column</li> </ol> </div> </aside> <footer> <p>Copyright ©</p> </footer> </div> ); }
Instead of putting the properties related to sticky positioning directly on the aside
element, I put them on a new div
wrapping our sticky content and put position: relative
on the aside
to create a containing block.
Play around with the demo and watch how the aside content overflows its assigned grid row if you instead put the sticky properties directly on the aside
. I went ahead and commented out the exact properties I experimented with so you can see what I mean.
Try adjusting the min-height
of the main element to see how that affects when the aside begins to scroll. The taller the main element is, the longer the aside can remain stuck.
If you change the height of the aside that will also affect when it scrolls. The taller the sticky element is, the sooner it needs to start scrolling until a point where it has no choice but to scroll immediately. At that point it's probably time to think about using a different layout!
CSS Grid may not be the right tool for every job as it locks all children into a defined set of columns and rows, which may work for or against you, or sometimes both! I found a way for it to work for me on this very blog and it was a joy to get more experience with this powerful layout mode.
I hope this post encourages more developers to experiment and make more, wild grid recipes. For further reading, check out Josh W. Comeau's interactive guide to CSS Grid which provides a more comprehensive guide on how it all works.
March 13, 2024