Menu

Achieving Vertical Alignment

September 30th, 2020

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:

Two groups of two bright pink buttons set on a dark background.

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:

The same two groups of two pink buttons, with one group of buttons stacked on top of the other, showing the buttons are uneven in width.

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;
  }
}
the same two groups of pink buttons but where the buttons all have the same consistent width and line up evenly.

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:

  1. the space available
  2. their content

and!

  1. 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:

Three browsers side-by-side. The first shows three black rectangles in a single row, the second shows three black rectangles stacked vertically, and the third shows three black rectangles set to the right hand of the screen.

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.

Achieving Vertical Alignment

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:

Showing three columns of cards where each card has a headers with a dark blue background and white text, content with a white background, and a footer with a dark blue background. The cards are uneven because some of them have more content that others.
Credit: Rachel Andrew

Subgrid allows the cards to use the rows defined in the parent grid, meaning they can react to content in surrounding cards.

The same three columns of cards, but with each card perfectly aligned with the others.
Credit: Rachel Andrew

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;
}
Showing the two groups of bright pink buttons with both groups on the same row and all the buttons have a consistent width.

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.

The two groups of buttons vertically stacked and the buttons are all consistently aligned with the same width.

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.

Achieving Vertical Alignment
Achieving Vertical Alignment

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 the browser supports the feature at that version and up.

Desktop

Chrome Firefox IE Edge Safari
No 71 No No No

Mobile / Tablet

Android Chrome Android Firefox Android iOS Safari
No 79 No No

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

The original post Achieving Vertical Alignment (Thanks, Subgrid!).