Quantity Query Carousel
Quantity Query Carousel ę´ë ¨
The concept of a quantity query is really neat. Coined by Heydon back in 2015, the idea is that you apply different styles depending on how many siblings there are. They was a way to do it back then, but itâs gotten much easier thanks to :has()
, which not only makes the detection easier but gives us access to the parent element where we likely want it.
For instance:
.grid {
display: grid;
&:has(:nth-child(2)) {
/* Has at least 2 elements */
grid-template-columns: 1fr 1fr;
}
/* Use a :not() to do reverse logic */
}
What if we kept going with the idea where weâŚ
- If there is 1 element, let it be full-width
- If there are 2 elements, set them side-by-side
- If there are 3 elements, the first two are side-by-side, then the last is full-width
- If there are 4 elements, then itâs a 2Ă2 grid
ThenâŚ
- If there are 5+ elements, woah there, letâs just make it a carousel.
I heard Ahmad Shadeed mention this idea on stage at CSS Day and I had to try it myself. Good news is that it works, particularly if you can stomach the idea of a âcarouselâ just being âhorizontal overflow with some scroll snappingâ in Firefox/Safari for now. Of course youâd be free to make your own fallback as needed.
Hereâs the whole gang:
Setup & One
The default setup can be something like:
.grid {
display: grid;
gap: 1rem;
}
Honestly we donât even really need to make it a grid for one item, but it doesnât really hurt and now weâre set up for the rest of them.
Two
Does it have two? Yeah? Letâs do this.
.grid {
...
&:has(:nth-child(2)) {
grid-template-columns: 1fr 1fr;
}
}
Note that if our grid has three or more elements, this will also match. So if want to do something different with columns, weâll need to override this or otherwise change things.
Three
To illustrate the point, letâs match where there are only three items.
.grid {
...
&:has(> :nth-child(3)):not(:has(> :nth-child(4))) {
> :nth-child(3) {
grid-column: span 2;
}
}
}
So weâre not going to change the 2-column grid, weâll leave that alone from two. And now weâre not selecting the grid itself, but just grabbing that third item and stretching it across both columns of the grid.
Four
We can⌠do nothing. Itâs already a two-column grid from two. So letâs let it be.
Five+
This is the fun part. We already know how to test for X+ children, so we do that:
.grid {
...
&:has(:nth-child(5)) {
grid-template-columns: unset;
}
}
But now weâre unset
ing those columns, as we donât need them anymore. Instead weâre going with automatic column creation in the column direction. We could use flexbox here too essentially but weâre already in a grid and grid can do it with easy sturdy columns so might as well. Then weâll slap smooth scrolling and scroll snapping on there, which will essentially be the fallback behavior (only Chrome supports the ::scroll-button
stuff that makes it carousel-like for now).
.grid {
...
&:has(:nth-child(5)) {
grid-template-columns: unset;
grid-auto-flow: column;
grid-auto-columns: 200px;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
> div {
scroll-snap-align: center;
}
}
}
Actually Carouselling
Weâre all set up for it, we just need those back/forward buttons to make it really be a carousel. Thatâs a CSS thing now, at least in Chrome ânâ friends, so we can progressively enhance into it:
.grid {
...
&:has(:nth-child(5)) {
...
anchor-name: --âď¸-carousel;
&::scroll-button(*) {
position: absolute;
top: 0;
left: 0;
position-anchor: --âď¸-carousel;
background: none;
border: 0;
padding: 0;
font-size: 32px;
}
&::scroll-button(right) {
position-area: center inline-end;
translate: -3rem -0.5rem;
content: "âĄď¸" / "Next";
}
&::scroll-button(left) {
position-area: inline-start center;
translate: 3rem -0.5rem;
content: "âŹ
ď¸" / "Previous";
}
}
}
Thatâll do it! Hereâs the demo (chriscoyier
) and Iâll video it in case youâre not in Chrome.