I started 2024 with a super in-depth Reality Check article (more of those coming this year) so I thought I’d start simple this year because I’m still a bit tired from, y’know, writing a whole CSS course 😅
What is masonry?permalink
I’m not going to presume that everyone knows what a masonry layout is, but you’ve almost certainly seen one websites like Pinterest, Unsplash and countless others.
A masonry layout is a grid-like layout that has a set column system with a flexible, content-driven row system. What this setup does is allow content to be displayed in a brick wall-like layout — hence the name, masonry. Content is packed into space, rather than adhering to a strict, set row system.
The masonry layout was popularised in the late 2010s with tools such as David Desandro’s legendary masonry library which used JavaScript to calculate the layout for you. It was very cool at the time, trust me.
People were — even back then — not overly comfortable with using JavaScript to calculate layout though, especially as responsive design started growing in popularity. Those calculations were very expensive on low powered devices, so naturally, people started looking towards CSS.
Masonry in CSSpermalink
You can right now, apply masonry with CSS grid like so:
- Code language
- css
.masonry { display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: masonry; }
That’s the end of the article then, right? Not quite yet because there’s a few problems.
Firstly, the browser support is still very low because it’s an experimental feature. Only Firefox and Safari Technology preview currently support that syntax, which is a very low share of the overall browser market. I’m all about progressive enhancement though, which is why I applied it to Set Studio’s Build Excellent Websites site in 2022.
The second problem is browser makers are still debating how to build masonry into CSS. Webkit have one opinion and Google have another, so don’t expect it to be resolved any time soon.
A simple, composable layoutpermalink
Here’s what we’re building.
We had a client need for a masonry-like layout which we dutifully over-engineered with a media queries, grid and way too much markup, so I’m not going to show you that to protect our modesty. Ever since we completed that work, I’ve been obsessing about how to achieve a masonry-like layout in a composable, flexible manner. Last time I got like this, Heydon and I published Every Layout as we were both obsessing about layout at the same time 😅
Every Layout is the answer to this problem though, and even as the co-author, it took me way too long for the penny to drop. The answer is right on the homepage:
If you find yourself wrestling with CSS layout, it’s likely you’re making decisions for browsers they should be making themselves.
Through a series of simple, composable layouts, Every Layout will teach you how to better harness the built-in algorithms that power browsers and CSS.
The answer to our problem is composable layouts! The two layouts we’re going to compose together are the switcher and the stack (which I call flow).
Let’s look at some code together and I’ll explain as we go.
HTML first, alwayspermalink
How do we fake packed rows with a set(ish) column system? The markup is going to have to be built in columns which then let the child content flow naturally.
- Code language
- html
<div class="wrapper switcher"> <!-- Column 1 --> <div class="flow"> <!-- Images redacted for brevity --> </div> <!-- Column 2 --> <div class="flow"> <!-- Images redacted for brevity --> </div> <!-- Column 3 --> <div class="flow"> <!-- Images redacted for brevity --> </div> </div>
I’ve just used <div>
elements here, but this could comfortably work with lists and other elements too. In each column, there’s a collection of <img>
elements which I’ve redacted for brevity. You can see the full markup in the CodePen demo though.
One thing you absolutely need to make sure of here is — just like with the current CSS masonry — keyboard tabbing order will not be predictably in the reading direction (left-to-right for English). This layout is better than that linked article, only because keyboard focus will be top-to-bottom in each column, but that still isn’t good enough. My advice: only use this layout for non-focusable elements.
Let’s add some CSSpermalink
What I really like about composable layouts is they’re configurable. We cover this a lot in Complete CSS. The idea is properties look for a custom property value, then lean on a fallback if the custom property is not defined. Let’s use .flow
as an example, which is explained here.
- Code language
- css
.flow > * + * { margin-block-start: var(--flow-space, 1em); }
If --flow-space
is not defined, margin-block-start
will be 1em
. Handy, right? We’re using that methodology with our masonry-like layout too. In fact, the masonry
class is only configuring layouts:
- Code language
- css
.masonry { --gutter: 0.25em; --flow-space: var(--gutter); --switcher-target-container-width: 25rem; }
We’re setting a nice small spacing value for --gutter
(used by the switcher and wrapping container). With that set, we tell --flow-space
to use --gutter
too, levelling horizontal and vertical space.
To explain --switcher-target-container-width
, allow me to cite Every Layout:
The switcher element (based on the bizarrely named Flexbox Holy Albatross) switches a flexbox context between a horizontal and a vertical layout at a given, container-based breakpoint. That is, if the breakpoint is
30rem
, the layout will switch to a vertical configuration when the parent element is less than30rem
wide.
Rather than determining a viewport-based media query to snap our masonry-like layout into a stacked layout, we allow the container to calculate that for us by configuring the bounds. We set those bounds (--switcher-target-container-width
) to 25rem
, meaning if the masonry
container is anything less than that, it will be stacked.
Before we move on, let’s make sure images always fill their container. For the layout to look neat, we need to make sure that happens or small images will make the layout look broken.
- Code language
- css
.masonry img { width: 100%; }
With that configuration set, we can introduce our layouts. Let’s add the switcher first:
- Code language
- css
.switcher { display: flex; flex-wrap: wrap; gap: var(--gutter, 1em); align-items: var(--switcher-vertical-alignment, flex-start); } .switcher > * { flex-grow: 1; flex-basis: calc( (var(--switcher-target-container-width, 40rem) - 100%) * 999 ); }
Let’s drill down on that flex-basis
property. Using calc()
, we’re generating either a very large positive value if there’s enough space in the container or a very large negative value if there’s not enough space. A negative flex-basis
value is invalid, and dropped. Thanks to CSS’s resilient error handling this means just the flex-basis
line is ignored, and the rest of the CSS is still applied. Because our items are instructed to grow with flex-grow
, a stacked layout takes over.
Let’s move on to our flow layout next:
- Code language
- css
.flow > * + * { margin-block-start: var(--flow-space, 1em); }
Yep, that’s the same snippet from earlier. I’ll let you read the whole explainer to keep this article as brief as possible, but in short, a flow
element’s child items will have margin-block-start
applied, aside from the first child, which gives us that nice vertical flow.
- Code language
- css
.wrapper { max-width: 80rem; margin-inline: auto; padding-inline: var(--gutter); }
Lastly, here’s a little wrapping container which pops the content in the center of the viewport with a touch of padding, configured by --gutter
.
Wrapping uppermalink
With that, we are done. Sure, this is isn’t “proper” masonry, but it’s close enough. If you get smart with your aspect ratios, you can really get close to “proper” masonry, like we did for a client.
I’d say this is a solid approach to carry you until browsers finally make their mind up on the how and then actually implement this stuff for real.