Complex Layouts With CSS Grid
From the Journal – Posted 04.04.2018
Part 1 of our in-depth look at CSS Grid
It would be no exaggeration to say that CSS Grid (or the CSS Grid Layout Module Level 1, to give it its full title) has changed the face of layout on the web. Indeed, it’s pretty remarkable that up until this specification we’ve had no purpose-built method for layout in CSS, and for many years have been forced to resort to hacks with floats and (later) flexbox - which, while serving the purpose, were never designed to be used this way.
With over 87% of browsers worldwide now supporting Grid, there has never been a better time to start experimenting! In this article I’ll outline some recent examples of layouts we’ve worked on here at Mud that previously wouldn’t have been possible with CSS alone. I’m going to skip over some of the basics, but for a more in-depth overview there are plenty of resources from Rachel Andrew and Jen Simmons, both of whom have done have done fantastic work in getting the spec drafted and approved, and deserve full credit for the fact that we can use it today.
If you’ve experimented with Grid Layout before, you’ll know that the spec allows us to define lines and tracks on a wrapper element and place child items on it at specified coordinates and/or spanning a given number of rows or columns, irrespective of source order. As a layout engine it’s incredibly powerful. Grid tracks can be fixed (e.g. 200px) or fluid – using percentages, viewport units, calc(), auto
or the fr
(or fraction) unit (new to the specification). Additionally, the minmax()
function allows you to (as the name would suggest) specify minimum and maximum track sizes – e.g. minmax(auto, 300px)
specifies a track the size of the content inside it, but only up until 300px, when it will grow no larger. This makes crafting basic, responsive layouts extremely easy.
Nevertheless, building complex layouts still requires careful consideration. For a recent project, the number of different grids in the design and the level of flexibility required made Grid an obvious choice when it came to building the site.
Case Study
Setting out our parameters
The design is based around a 24-column grid. The first thing to do was to assess the design and work out our parameters for building the grid components. Overall there were a total of eight variants of this type of component, with the following parameters:
- Items must be evenly spaced vertically
- Headings must be vertically centred over images
- Images should retain their aspect ratio and not be cropped
- Consistent space should be retained between text and heading so they don’t crash into each other (rules out absolute positioning)
- Where text exceeds available space, push content outwards (while still retaining space between items)
- Client should have control over whether to place text at the top or bottom of the component, and the text should align to the top or bottom of the image respectively.
- Text, image and heading should also be links individually (means we need to take care with positioning / z-index in order to avoid blocking links)
- Items must fit a max-width wrapper consisting of 24 columns, except...
- Some items have images that bleed to edges, but text content must still align to wrapper
- Paragraph text always spans the same number of grid columns, but images could be any number between 11 and 16 columns.
Of course, with any client site, there are some unknowns that we need to consider:
- The amount of text
- Dimensions of image
- Length of heading (e.g. whether this is likely to run onto two lines)
Phew, that’s a lot to consider, and we haven’t even got started on the responsive design!
Assuming there will certainly be some cases where the text length and image dimensions vary, we can see that we’re not necessarily going to be able to achieve 100% of the above requirements at all times.
Defining our grid
The next step was to jump into front-end code. You can find the complete code for this example here: https://codepen.io/michellebarker/pen/bvBVEE if you want a visual reference.
<p data-height="392" data-theme-id="0" data-slug-hash="bvBVEE" data-default-tab="css,result" data-user="michellebarker" data-embed-version="2" data-pen-title="CSS Grid – Flexible alignment" class="codepen">See the Pen <a href="">@michellebarker</a>) on <a href="">CodePen</a>.</p>
<script async src=""></script>
To give us maximum flexibility, each item placed directly on the grid has to be a direct child of our parent grid container, so the markup looks something like this:
<div class="grid">
<div class="grid__heading">...</div>
<div class="grid__media">...</div>
<div class="grid__body">...</div>
</div>
While Grid allows us to re-order our grid items visually, we need to ensure our underlying markup follows a logical source order for accessibility purposes and non-supporting browsers. In this case the mobile design for each item (which won’t require grid, as it’ll be a single-column layout) will follow this order (heading, image, body), so it makes sense to structure our HTML this way.
Next, we need to define our grid container – the wrapper with a class of .grid
– in our CSS. We’ll need to declare the number of columns in our grid, and the width of each column. Right up until we get to our wrapper max-width, this is pretty easy. We can use Grid’s fr
unit and repeat()
function to construct a 24-column, evenly spaced grid. Additionally, we can add a 20px column of either side, which will serve as padding.
.grid {
display: grid;
grid-template-columns: 20px repeat(24, 1fr) 20px;
grid-column-gap: 20px;
}
This works great up until we get to large viewport sizes. At the moment our grid takes up the entire width of the screen, but we want it to fit a max-width wrapper (e.g. 1200px), with the occasional item bleeding out to the edge. To achieve this we have to do a bit of maths to return our column width after the 1200px breakpoint:
$max-col: calc((((1200px - 40px) - (20px * 25)) / 24) * 1px);
The above calculation looks complicated, but it can be broken down as follows:
($wrapperWidth - ($gutterWidth * $numberOfGutters)) / $numberOfColumns
Then we just need to pass that variable into our grid-template-columns property, inside a media query, and add an extra flexible column (using the fr
unit) either side of our main content:
@media (min-width: 1200px) {
grid-template-columns: 20px 1fr repeat(24, $max-col) 1fr 20px;
}
Now each child item will automatically become a grid item and will be placed on the grid, spanning one column.
One of the great things about Grid is that it allows up to place items on the same grid columns as each other – so items can overlap each other without the need for absolute positioning. This will be useful when it comes to placing our headings over the images in our grid components.
To centre the heading within the grid, while maintaining an even space between it and the body text, we can also define our grid rows.
On the face of it, it looks fairly simple – we want each row to respond to fit our content:
.grid {
//...
grid-template-rows: repeat(3, auto);
}
However, this doesn’t have the desired effect at all. The following gives us a much better result, using the fr
unit (the heading row still uses auto
as we want this to respond to the size of the text), which keeps the heading vertically centred in the component. Here I’m also adding a fixed 40px row which will sit either side of our heading, to ensure a space is maintained even when the text is longer and breaks out of alignment with the image. We can also name our grid lines to make it easier to place items, especially as the text can be placed at the top or the bottom:
.grid {
//...
grid-template-columns: [outer-start] 20px [wrapper-start padding-r] repeat(24, 1fr) [wrapper-end padding-l] 20px [outer-end];
grid-template-rows: [outer-start] 1fr [text-top-end] 40px [heading-start] auto [heading-end] 40px [text-bottom-start] 1fr [outer-end];
}
@media (min-width: $wrapper-width) {
.grid {
grid-template-columns: [outer-start] 20px [padding] 1fr [wrapper-start] repeat(24, $max-col) [wrapper-end] 1fr [padding] 20px [outer-end];
}
}
In some cases the grid lines have two names – that’s because after our breakpoint we’re adding an extra two columns. Using the named grid lines means I don’t need to write an extra chunk of code inside the media query for placing our items, as you’ll see below!
Placing the items
Now we just need to place our grid items! Grid offers a few choices when placing child items in the grid container. For this component I’m using a combination of named grid lines, span and column start / end values.
.grid__heading {
grid-column: padding-l / padding-r;
grid-row: heading-start / heading-end;
z-index: 1;
text-align: center;
}
.grid__media {
grid-column: outer-start / 16;
grid-row: outer-start / outer-end;
align-self: center;
}
.grid__body {
grid-column: span 7 / 27;
grid-row: outer-start / text-top-end;
align-self: flex-start;
}
You can see on the body text block we’re using a different syntax. This is because we know that we always want to block to span 7 columns, but it could start and end at different places, depending on the component. So here we’re saying we want it to end at grid line 27 and span 7 columns, without having to explicitly declare where it starts.
We’re also using flexbox alignment properties (which are by and large the same for Grid), to align our content vertically, so that the text will align correctly with the image.
There is so much more we could touch on with Grid, but hopefully this article gives you a flavour of its capabilities and the confidence to try it yourself!