Our tools for vertical alignment have gotten a lot better as of late. My early days as a website designer involved laying out 960px wide homepage designs and aligning things horizontally across a page using a 12-column grid. Media queries came along which required a serious mental shift. It solved some big problems, of course, but introduced new ones, like dealing with alignment when elements wrap or are otherwise moved around in the layout.
Let’s take a look at just one particular scenario: a “bar” with some buttons in it. There are two groups of these buttons, each contained within a <fieldset>
with a <legend>
.
On a large screen, we’re all set:
And here’s a very basic CSS method that accomplishes that layout, and also breaks down onto two “rows” at a mobile breakpoint:
.accessibility-tools fieldset {
width: 48%;
float: left;
margin-right: 1%;
}
/* Mobile */
@media only screen and (max-width: 480px) {
.accessibility-tools fieldset {
width: 100%;
}
}
On a small screen, we end up with this:
This is the problem: lack of vertical alignment. Let’s say we want to align those buttons into a more pleasing arrangement where the button edges align with each other nicely.
To begin, we could go for fixed-width, pixel-based CSS solutions to force elements to line up nicely at various breakpoints, using magic numbers like this:
/* Mobile */
@media only screen and (max-width: 480px) {
legend {
width: 160px;
}
button {
width: 130px;
}
}
That does the trick.
But… this is not exactly a flexible solution to the problem. Aside from the magic numbers (fixed-pixel values based on specific content), it also relied on the use of media queries which I am trying to move away from when I can. I discussed this in a post called “Stepping away from Sass” on my blog.
As I moved towards some of the more modern features of CSS the need to target specific screen sizes with unique code was removed.
What I need is each button and label to respond to:
- the space available
- their content
and!
- other elements around them.
Available space
The problem with using media queries is that they don’t take into account the space around the elements that are being realigned — a point perfectly demonstrated in this image from “The Flexbox holy albatross” by Heydon Pickering:
What I really want is for the second <fieldset>
to wrap under the first only when they can no longer fit neatly on one row.
Can we get this done with flexbox?
A key selling point for flexbox is its ability to create elements that respond to the space around them. Components can “flex” to fill additional space and shrink to fit into smaller spaces.
For this situation, the flex-wrap
property is set to wrap
. This means as soon as both <fieldset>
elements no longer fit on one line, they will wrap onto a second line.
.wrapper--accessibility-tools {
display: flex;
flex-wrap: wrap;
}
The flex-wrap
property has three available values. The default value is nowrap
, leaving items on one line. The wrap
value allows elements to flow onto multiple lines. Then there’s wrap-reverse
, which allows items to wrap but — wait for it — in reverse (it is weird to see: when elements wrap, they go above the previous row in left-to-right situations).
Using flexbox stops the layout from being quite as rigid, but a min-width
value is still needed to remove the vertical alignment problem. So: close but no cigar.
Can grid help us?
CSS Grid is the very first CSS module created specifically to solve the ongoing layout problems faced by web designers and developers. It is not a direct replacement for flexbox; rather the two modules usually work pretty well together.
Like flexbox, grid can be used to allow each <fieldset>
to occupy as much or as little space as they need. Getting right to it, we can leverage the auto-fill
and auto-fit
keywords (within a repeat()
function) to allow grid items to flow onto multiple lines without the need for media queries. The difference is a bit subtle, but well-explained in “Auto-Sizing Columns in CSS Grid: auto-fill vs auto-fit” by Sara Soueidan. Let’s use auto-fit
:
.wrapper--accessibility-tools {
display: grid;
grid-template-columns: repeat(auto-fit, 450px);
grid-gap: 10px;
}
Like the flexbox example, I still need to set an absolute value for the width of the label to align the <fieldset>
elements as they stack.
Another approach with grid
CSS Grid also allows elements to respond based on their content using flexible grid tracks. In addition to other length values like percentages, relative units, or pixels, CSS Grid accepts a Fractional Unit (fr
), where 1fr
will take up one part of the available space, 2fr
will take up two parts of the available space, and so on. Let’s set up two equal columns here:
.wrapper--accessibility-tools {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 10px;
}
There’s also a minmax()
function which creates grid tracks that flex to the available space, but also don’t shrink narrower than a specified size.
.wrapper--accessibility-tools {
display: grid;
grid-template-columns: minmax(auto, max-content) minmax(auto, max-content);
grid-gap: 10px;
}
Both of these demos work, and are free from any absolute values or device specific CSS. The results are far from ideal though, each grid now responds at different points. Maybe not a huge problem, but certainly not great.
This happens because when adding display: grid
to a container, only the direct children of that container become grid items. This means the intrinsic sizing units we used only relate to elements in the same grid.
Using subgrid
To really achieve my goal, I need the buttons and labels to react to elements in sibling grid containers. CSS Grid Level 2 includes the subgrid feature. Although we have always been able to nest grids, the elements within each grid container have been independent. With subgrid, we get to set up nested (child) grids that use parent grids tracks.
This makes a number patterns that were previously difficult much easier, in particular the “card” pattern which seems to be the most popular example to show the benefits of subgrid. Without subgrid, each card is defined as an independent grid, meaning track sizing in the first card cannot respond to a change of height in the second. Pulling from an example Rachel Andrew used, here’s a simple group of cards:
Subgrid allows the cards to use the rows defined in the parent grid, meaning they can react to content in surrounding cards.
Each card in this example still spans three row tracks, but those rows are now defined on the parent grid, allowing each card to occupy the same amount of vertical space.
For the example we’ve been working with, we do not need to use rows. Instead, we need to size columns based on content from sibling grids. First, let’s set the parent grid to contain the two <fieldset>
elements. This is similar to the code we previously look at in the auto-fit
demo.
.wrapper--accessibility-tools {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
grid-gap: 10px;
}
Then we position each subgrid onto the parent grid.
.sub-grid {
display: grid;
grid-column: span 3;
grid-template-columns: subgrid;
align-items: center;
}
All of the labels and buttons are now aligned to the tracks of their parent grid, keeping them consistent. They will each have an equal width based on the space that is available. If there is not enough space for each nested grid on one line, the second will wrap onto a new line.
This time, the two nested grid items align perfectly. The grid is also flexible if we introduce a longer title on a one of the buttons, the other elements will respond accordingly.
Browser compatibility
Support for subgrid is not great at the time of writing. It is only supported in Firefox 71+, although there are positive signals from other browsers. CSS feature queries can be used to provide alternative styling to Chrome and Edge.
This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.
Desktop
Chrome | Firefox | IE | Edge | Safari |
---|---|---|---|---|
117 | 71 | No | 117 | 16.0 |
Mobile / Tablet
Android Chrome | Android Firefox | Android | iOS Safari |
---|---|---|---|
126 | 127 | 126 | 16.0 |
Note that I am using an extra wrapper around the fieldsets in these demos. This is to combat a bug with form elements and grid and flexbox.
<fieldset class="accessibility-tools__colour-theme">
<div class="wrapper"></div>
</fieldset>
The layout CSS is applied to the wrapper with the fieldset being set to display: contents
.
.accessibility-tools fieldset {
display: contents;
border: 0;
}
Other writing on the subject
- “Subgrid for better card layouts” by Miriam Suzanne
- “CSS Grid Level 2: Here Comes Subgrid” by Rachel Andrew
- “The flexbox holy albatross” by Heydon Pickering
I’m really excited that sub-grid is now supported by Firefox!
I noticed that your sub-grid example doesn’t quite work below 500px wide. Is there a good way to fix that without using Media Queries?
Or are media queries the best way to fix that issue?
Great article. Will be adding this to a few upcoming projects as progressive enhancements. But it will great once this has more widespread support