On a recent project we finally got to use Flexbox extensively for page layout. In the interests of increasing general knowledge about flexbox (including mine), I’ll explain a number of layouts that use flexbox extensively, organised in 3 sections:
- Fluid product grid and accordion-like summary box
- Product “cards” and “slabs”
[Edit 12 September: See Reddit for a discussion of this post]
This is not a from-basics flexbox tutorial. Here are two good ones. But in short, you create a flexbox, or make a flex container, by giving an element
display: flex. Its immediate children then become flex items, on which you can set the
flex property. They can themselves be flex containers.
Browser support: IE <11 is out, as are old versions of Mobile Safari. For this project that was OK, and we were able to use flexbox properties without vendor prefixes.
Disclaimer: I’m not claiming this is the perfect way to do it, so copy it at your own risk. But it seems to work very well for us. Questions or suggestions are welcome!
1. Main page layout
Everything is surrounded by
.wrapper, which is
position: fixed to the exact size of the viewport. This is set to be our flex container with
display: flex; flex-direction: column; so the flex items go vertically.
flex: 1. This makes it stretch to fill the available space. (It’s shorthand for
flex: 1 1 0, so
flex-grow is 1 (it can grow),
flex-shrink is 1 (it can shrink), and
flex-basis is 0 (it has no minimum height).
B. Each flex item –
.footer-wrapper – is also set to be a flex container. Their flex items are horizontal though, so they’re all
flex-direction: row (which is the default). Note the use of
align-items: center which vertically aligns the header and footer contents regardless of their height. (Comparable to
vertical-align in tables.)
.body-wrapper there’s also one element each with
flex: 1 so that it stretches to fill the available space. In
.footer-wrapper we need to set each item to
flex: 1 1 auto (changing
auto) so that they can protrude into each other’s half and not start wrapping prematurely).
Note how nice it is that we can have any number of left or right panes (
.pane-wrapper), of any size, and
.canvas-wrapper will just adapt. We could also use
order to change the order panes display in if desired.
.canvas-wrapper are each set as flex containers with
flex-direction: column (they have a titlebar and body area each).
.pane-body is a flex-container with
flex-direction: row. Strictly speaking flexbox is unnecessary for
.pane-body as we ended up setting
.canvas to be
position: absolute. This was so they don’t affect the calculation of the outer containers’ heights at all, and each can scroll. It didn’t look like this was possible if
.canvas were flex items.
How would we have done this layout without Flexbox? On a previous project with very similar layout, we placed every pane using
position: fixed. This has the advantage of excellent support in old browsers. But it comes at the cost of fixed positions in the CSS, with classes for
Also, making things vertically aligned within rows would have been impossible without some nasty hacks.
2. Product grid and summary box
Flexbox was ideal for (A) fluid product grids, as well as (B) an accordion-like summary box. This interface is inside a modal popup of fluid width. To maximise screen real estate it resizes smoothly in proportion with the window width and height. (Using Bootstrap modal markup,
.modal-body are all given percentile heights.) Firstly the flex container
.col-wrapper arranges columns horizontally with
flex-direction: row. Note that
align-items: stretch makes each column the same height. And because it is used inside of a container with known height,
height: 100% makes it stretch to the full height available.
flex: 1 so that it stretches to fill the horizontal space available, and both
.col-summary are given a width and prevented from flexing with
flex: 0 0 <width>. Both
overflow: auto so they can scroll.
A. Many parts of the app uses a grid of products, either in “card” or “slab” form (described in more detail next section). These are stretchy, adapting their width to the fluid container width to fill rows exactly. But we also use media queries to control the number of columns we allow at any screen size.
The flex container is
.product-grid. It has
flex-wrap: wrap to allow wrapping, and
align-items: stretch makes every item in a row the same height.
.product is the flex item. Products have 1% left/right margins, equivalent to 2% between each. (The container also has 1% left/right padding.)
.product is sized with percentile values using
flex-basis, with a Less formula based on the number of columns for that screen size:
flex: 0 0 (100 / 3) - 2% (for 3 columns, taking into account 2% margin). You can see some examples of the media queries in the illustration above.
Without flexbox? Grid items would’ve been floated, but there would’ve been no way to equalise heights within a row without very ugly hacks.
Is this the best way to do a fluid grid with flexbox? This method may seem unsophisticated, relying entirely on percentile
flex-basis, and not on
flex-shrink. It may seem not much better than a float-based approach, although we do get the advantage of consistent box heights. The reason is to align items in the last row to the grid. If the last row is not completely filled, most flexbox grids fill the space by allowing the items to grow in width (example), or increasing the space between them. Neither of those had acceptable results in our case. Here is a codepen with our grid in its minimal form. Our approach also gives us a maximum number of columns for every screen size breakpoint.
B. The challenge in the summary box was to have it fill the vertical space, but have a single section
.summary-content that stretches to fill the available space, yet starts scrolling when the space is filled.
.summary-box needs to have its height set to 100% (its parents also having height specified), and
flex-grow: 1 and
flex-shrink: 1 so it can both grow and shrink as needed.
overflow: auto lets it scroll.
Without flexbox, this is pretty much impossible, if the other rows in the box have unpredictable heights (which many of them do).
3. Product cards and slabs
In product grids, products were either represented as large “cards” with image and description, or compact “slabs”. Flexbox was extremely helpful in each case.
.product-card, we have a vertical flexbox with a single element
.product-desc that stretches to fill the available space. If a
max-height is specified,
.product-desc would be truncated. We’re considering putting the image above the title, and it’s to know we could do that using
order without changing the HTML if we wanted to.
Again, without flexbox, this sort of thing is impossible if the other rows have unpredictable heights.
.product-slab we needed the contents aligned to the left and right edges. Without flexbox, this would’ve been a pain, requiring floats and clearfixing, and no way to distribute the horizontal space according to the amount of content. With flexbox, we can.
flex-direction: row arranges the contents of the slab horizontally.
justify-content: space-between pushes the title and content against the opposite edges. And
flex-grow: 1 on
.product-title ensures the horizontal space is filled inside, pushing it against the left-hand edge.
This project was a valuable learning experience. Flexbox can be surprisingly difficult to get your head around, and the best way to learn is to use it in earnest. While most projects can’t yet rely on flexbox to this degree, for browser support reasons, it can still prove useful for many elements, especially with some fallback measures – this presentation by Zoe Gillenwater has many examples. We’ll definitely be using it more in future projects.
To keep this post to a manageable length I tried to be as concise as possible, so if there are any explanations that are unclear, or if there are things you’d have done differently, let me know in the comments.