I get dangerously intrigued by problems that seem potentially impossible where CSS is concerned, but completely reasonable from a visual design point of view.
Here's one that cropped up yesterday: a responsive gallery where the pictures alternate dark-themed and light-themed (could also be B&W/color, red-themed/green-themed, photo/drawing or any alternating stylistic property).
In the markup the images alternate. It even looks totally great on its own when there are an odd number of pictures per row. But when there are an even number of itemsPerRow
, we get vertical bands because each row starts with a 'light' picture.
What we want, though, is for the diagonal tiling effect to be maintained across every breakpoint. Something like (3 mb gif alert π¨):
In a nutshell π₯: The last item in row 1 needs to stylistically match the first item in row 2, but only when there are an even number of items per row.
TLDR for the designers: If your front-end dev said this is impossible, they were wrong π±! (if you don't need to support IE π)
When I first ran across the problem I was stumped. Could I achieve a patterned grid with differing numbers of items per row and the same HTML without using javascript? π€
Conceptually what I want here without complicated math, is for the direction of the grid to weave back and forth as I move down the page. Indeed it would be awesome if I could just declare grid-auto-flow: snakes!;
and move on to the next problem. Unfortunately no such property exists.
But we can still make it happen in just CSS with some diligence and 3 somewhat advanced but very useful selectors and grid properties:
I'll explain each of these and then illustrate how to apply them to our situation. Or just jump to the 'solution' and read the code directly.
:nth-child()
The nth-child selector is an important one. It's very powerful, but sometimes a bit difficult to figure out when using it beyond its basic functionality of odd
/even
/4
:nth-child()
also accepts an equation, which will be very useful for manipulating a series of data of an unknown length, which is exactly what we need to do here. If you already know how this one works, feel free to skip this section!
img:nth-child(4n+5) {
/* */
}
That would result in selecting the following children:
-
(4*0)+5
= 5th -
(4*1)+5
= 9th -
(4*2)+5
= 13th - etc
In case your linear algebra is rusty, there's a pretty straightforward way to apply this.
Given our specific problem, we need to select every x
th item, starting with the y
th. More englishy, we want to select "the first item of alternating rows of 4". Diving a little deeper, we want to ignore the first row, and start with the 5th item overall (y
/firstItem
). After that we want to select every 8th item after that (to continue selecting the first column of every other rowβ(x
/itemsPerRow * 2
) ). That looks like:
// This is not real javascript! π
let y=5
let x=8
equation = (x*n)+y
selector = `8n + 5`
// or
let itemsPerRow = 4
let firstItem = itemsPerRow + 1 // 5
equation = (itemsPerRow * 2)n + firstItem
selector = `8n + 5`
That wasn't real javascript, just an attempt to illustrate the maths going on here. That same equation will allow us to modify any grid, as we'll see later.
Grid-column
Grid-column is a property that can be assigned to a specific grid-item in order to move it to a different place than where it normally would fall. So if I have something like grid-template-columns: repeat(4, auto)
(which places my grid items in rows of 4), I can use grid-column
to move a particular item (chosen by nth-child) to a different position.
.grid-item {
/* put the item in the second column */
grid-column: 2;
/* put the item in the last column */
grid-column: -1;
/* make the item span between column 2 and column 4 */
grid-column: 2/4;
}
Cool!
Grid-auto-flow
.grid {
grid-auto-flow: dense
}
This is the one that makes this even possible. Without it all would be lost (at least until grid-auto-flow: π
is supported π).
Normally, when you reposition a grid item, it repositions every following item with it, leaving a gap. If you want to change the order of just the one particular item when using grid-column
, the dense
keyword takes the other items and moves them up to fill the spaces. It's a little like position: absolute
or a float, except the targeted items still take up space. Whoa.
Grid-auto-flow does other stuff, too, but dense
is what we came here for.
Putting it all together
So putting these rules together you get something that looks like this:
.grid {
grid-template-columns: repeat(4, auto);
grid-auto-flow: dense;
}
/* 4 itemsPerRow, every other row(4*2), starting with the 5th: 8n+5 */
img:nth-child(8n+5) {
grid-column: 4;
}
The finished pattern
To apply this in the real world, we have to set up the correct repositioning rules for just the breakpoints where they're necessary. Here's some example codepens that use a data attribute instead of a media query to make playing with the results a little nicer.
Example for alternating pattern
Example for cycle of 3
You can also use the same strategy to make a repeating pattern of 3 style types always look aesthetically pleasing no matter the number of items per row.
It gets a little more complicated, but it also provides an example of needing to reposition 2 items per row (row of 6 and a cycle of 3 styles).
CSS + Media queries
For it to work like we actually want it to, via media queries, you would need something like this (example below is for a cycle of 2 and a grid up to 4 per row:
.photo-grid {
display: grid;
grid-auto-flow: dense;
}
.photo-grid img {
width: 100%;
}
/* I don't like min AND max queries (mobile first!), but it makes perfect sense here because we're doing some specific things to the image that we don't want to apply to the 900px breakpoint. */
@media screen and (min-width: 600px) and (max-width: 899px) {
.photo-grid {
grid-template-columns: repeat(2, auto);
}
.photo-grid img:nth-child(4n + 3) {
grid-column: 2;
}
}
@media screen and (min-width: 900px) {
.photo-grid {
grid-template-columns: repeat(3, auto);
}
}
@media screen and (min-width: 1200px) {
.photo-grid {
grid-template-columns: repeat(4, auto);
}
.photo-grid img:nth-child(8n + 5) {
grid-column: 4;
}
}
Is it accessible?
This is a question you should always be asking when stumbling across a wacky CSS or JS technique on the internet, and I am thrilled if you're looking at all this thinking "While this is theoretically useful in very specific and uncommon situations, I worry that it isn't fully accessible!"
Indeed! It's important for screen-readers to be able to make sense of a site, and messing with the order of elements visually vs in source can be problematic β 2.4.3: focus order and 1.3.2: meaningful sequence.
If you are ordering your content visually, the order probably isn't inherently important to reading the content of the page, so this should not be an issue! But do keep in mind, if source order is important (i.e. you are explicitly sorting content in a specific order (alphabetically, newβold), or intentionally presenting it in a narrative order) then the visual order and the mark-up order need to be maintained.
Conclusion.
I would love to be able to do this in a breakpoint-free way using, say, repeat(auto-fill, minmax(300px, 1fr) )
and a mythical grid-auto-flow: snake/alternating-rows
to essentially eliminate all the weird math. But in the meantime this was the best I could come up with π
!
Let me know in the comments if you have a cleaner approach to this or a similar problem!
Top comments (1)
Beautifully written, Damon!