{"id":6323,"date":"2025-06-25T18:04:06","date_gmt":"2025-06-25T23:04:06","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=6323"},"modified":"2025-06-25T18:04:07","modified_gmt":"2025-06-25T23:04:07","slug":"quantity-query-carousel","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/quantity-query-carousel\/","title":{"rendered":"Quantity Query Carousel"},"content":{"rendered":"\n<p>The concept of a <strong>quantity query<\/strong> is really neat. <a href=\"https:\/\/alistapart.com\/article\/quantity-queries-for-css\/\">Coined by Heydon<\/a> back in 2015, the idea is that you apply different styles depending on how many siblings there are. They was a<a href=\"https:\/\/quantityqueries.com\/\"> way to do it back then<\/a>, but it&#8217;s gotten much <a href=\"https:\/\/frontendmasters.com\/blog\/quantity-queries-are-very-easy-with-css-has\/\">easier thanks to <code>:has()<\/code><\/a>, which not only makes the detection easier but gives us access to the parent element where we likely want it.<\/p>\n\n\n\n<p>For instance:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.grid<\/span> {\n  <span class=\"hljs-attribute\">display<\/span>: grid;\n\n  &amp;:has(:nth-child(2)) {\n    <span class=\"hljs-comment\">\/* Has at least 2 elements *\/<\/span>\n    <span class=\"hljs-attribute\">grid-template-columns<\/span>: <span class=\"hljs-number\">1<\/span>fr <span class=\"hljs-number\">1<\/span>fr;\n  }\n \n  <span class=\"hljs-comment\">\/* Use a :not() to do reverse logic *\/<\/span>\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>What if we kept going with the idea where we&#8230;<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>If there is 1 element, let it be full-width<\/li>\n\n\n\n<li>If there are 2 elements, set them side-by-side<\/li>\n\n\n\n<li>If there are 3 elements, the first two are side-by-side, then the last is full-width<\/li>\n\n\n\n<li>If there are 4 elements, then it&#8217;s a 2\u00d72 grid<\/li>\n<\/ul>\n\n\n\n<p>Then&#8230;<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>If there are 5+ elements, <em>woah there<\/em>, let&#8217;s just make it a carousel.<\/li>\n<\/ul>\n\n\n\n<p>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 &#8220;carousel&#8221; just being &#8220;horizontal overflow with some scroll snapping&#8221; in Firefox\/Safari for now. Of course you&#8217;d be free to make your own fallback as needed.<\/p>\n\n\n\n<p>Here&#8217;s the whole gang:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_qEdJqdZ\" src=\"\/\/codepen.io\/editor\/anon\/embed\/qEdJqdZ?height=1000&amp;theme-id=1&amp;slug-hash=qEdJqdZ&amp;default-tab=result\" height=\"1000\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed qEdJqdZ\" title=\"CodePen Embed qEdJqdZ\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Setup &amp; One<\/h2>\n\n\n\n<p>The default setup can be something like:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.grid<\/span> {\n  <span class=\"hljs-attribute\">display<\/span>: grid;\n  <span class=\"hljs-attribute\">gap<\/span>: <span class=\"hljs-number\">1rem<\/span>;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Honestly we don&#8217;t even really need to make it a grid for one item, but it doesn&#8217;t really hurt and now we&#8217;re set up for the rest of them. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Two<\/h2>\n\n\n\n<p>Does it have two? Yeah? Let&#8217;s do this.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.grid<\/span> {\n  ...\n\n  &amp;:has(:nth-child(2)) {\n    <span class=\"hljs-attribute\">grid-template-columns<\/span>: <span class=\"hljs-number\">1<\/span>fr <span class=\"hljs-number\">1<\/span>fr;\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Note that if our grid has <em>three<\/em> or more elements, this will also match. So if want to do something different with columns, we&#8217;ll need to override this or otherwise change things.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Three<\/h2>\n\n\n\n<p>To illustrate the point, let&#8217;s match where there are <em>only<\/em> three items.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.grid<\/span> {\n  ...\n\n  &amp;:has(&gt; :nth-child(3)):not(:has(&gt; :nth-child(4))) {\n    &gt; :nth-child(3) {\n      <span class=\"hljs-attribute\">grid-column<\/span>: span <span class=\"hljs-number\">2<\/span>;\n    }\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>So we&#8217;re not going to change the 2-column grid, we&#8217;ll leave that alone from <strong>two.<\/strong><em> <\/em>And now we&#8217;re not selecting the grid itself, but just grabbing that third item and stretching it across both columns of the grid. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Four<\/h2>\n\n\n\n<p>We can&#8230; do nothing. It&#8217;s already a two-column grid from <strong>two.<\/strong> So let&#8217;s let it be. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Five+<\/h2>\n\n\n\n<p>This is the fun part. We already know how to test for X+ children, so we do that:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.grid<\/span> {\n  ...\n\n  &amp;:has(:nth-child(5)) {\n    <span class=\"hljs-attribute\">grid-template-columns<\/span>: unset;\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>But now we&#8217;re <code>unset<\/code>ing those columns, as we don&#8217;t need them anymore. Instead we&#8217;re going with automatic column creation in the column direction. We could use flexbox here too essentially but we&#8217;re already in a grid and grid can do it with easy sturdy columns so might as well. Then we&#8217;ll slap smooth scrolling and scroll snapping on there, which will essentially be the fallback behavior (only Chrome supports the <code>::scroll-button<\/code> stuff that makes it carousel-like for now). <\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.grid<\/span> {\n  ...\n\n  &amp;:has(:nth-child(5)) {\n    <span class=\"hljs-attribute\">grid-template-columns<\/span>: unset;\n\n    <span class=\"hljs-attribute\">grid-auto-flow<\/span>: column;\n    <span class=\"hljs-attribute\">grid-auto-columns<\/span>: <span class=\"hljs-number\">200px<\/span>;\n\n    <span class=\"hljs-attribute\">overflow-x<\/span>: auto;\n    <span class=\"hljs-attribute\">overscroll-behavior-x<\/span>: contain;\n    <span class=\"hljs-attribute\">scroll-snap-type<\/span>: x mandatory;\n    <span class=\"hljs-attribute\">scroll-behavior<\/span>: smooth;\n\n    &gt; div {\n      <span class=\"hljs-attribute\">scroll-snap-align<\/span>: center;\n    }\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\">Actually Carouselling<\/h2>\n\n\n\n<p>We&#8217;re all set up for it, we just need those back\/forward buttons to make it really be a carousel. That&#8217;s a CSS thing now, at least in Chrome &#8216;n&#8217; friends, so we can progressively enhance into it:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.grid<\/span> {\n  ...\n\n  &amp;:has(:nth-child(5)) {\n    ...\n\n    <span class=\"hljs-attribute\">anchor-name<\/span>: --\u2693\ufe0f-carousel;\n\n    &amp;::scroll-button(*) {\n      <span class=\"hljs-attribute\">position<\/span>: absolute;\n      <span class=\"hljs-attribute\">top<\/span>: <span class=\"hljs-number\">0<\/span>;\n      <span class=\"hljs-attribute\">left<\/span>: <span class=\"hljs-number\">0<\/span>;\n      <span class=\"hljs-attribute\">position-anchor<\/span>: --\u2693\ufe0f-carousel;\n      <span class=\"hljs-attribute\">background<\/span>: none;\n      <span class=\"hljs-attribute\">border<\/span>: <span class=\"hljs-number\">0<\/span>;\n      <span class=\"hljs-attribute\">padding<\/span>: <span class=\"hljs-number\">0<\/span>;\n      <span class=\"hljs-attribute\">font-size<\/span>: <span class=\"hljs-number\">32px<\/span>;\n    }\n\n    &amp;<span class=\"hljs-selector-pseudo\">::scroll-button(right)<\/span> {\n      <span class=\"hljs-attribute\">position-area<\/span>: center inline-end;\n      <span class=\"hljs-attribute\">translate<\/span>: -<span class=\"hljs-number\">3rem<\/span> -<span class=\"hljs-number\">0.5rem<\/span>;\n      <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-string\">\"\u27a1\ufe0f\"<\/span> \/ <span class=\"hljs-string\">\"Next\"<\/span>;\n    }\n\n    &amp;<span class=\"hljs-selector-pseudo\">::scroll-button(left)<\/span> {\n      <span class=\"hljs-attribute\">position-area<\/span>: inline-start center;\n      <span class=\"hljs-attribute\">translate<\/span>: <span class=\"hljs-number\">3rem<\/span> -<span class=\"hljs-number\">0.5rem<\/span>;\n      <span class=\"hljs-attribute\">content<\/span>: <span class=\"hljs-string\">\"\u2b05\ufe0f\"<\/span> \/ <span class=\"hljs-string\">\"Previous\"<\/span>;\n    }\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>That&#8217;ll do it! <a href=\"https:\/\/codepen.io\/editor\/chriscoyier\/pen\/qEdJqdZ\">Here&#8217;s the demo<\/a> and I&#8217;ll video it in case you&#8217;re not in Chrome. <\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player aligncenter wp-block-jetpack-videopress--has-max-width\" style=\"max-width: 428px;\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='714' src='https:\/\/videopress.com\/embed\/WnVQsCOi?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1739540970'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t","protected":false},"excerpt":{"rendered":"<p>:has() makes quantities queries both easier and more powerful. We can alter how a grid is laid out and where the children go. Or, we can just blast it into a carousel. <\/p>\n","protected":false},"author":1,"featured_media":6333,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[341,123,41],"class_list":["post-6323","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-carousels","tag-grid","tag-quantity-queries"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/06\/grids.jpg?fit=2000%2C1386&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6323","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=6323"}],"version-history":[{"count":8,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6323\/revisions"}],"predecessor-version":[{"id":6336,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6323\/revisions\/6336"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/6333"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=6323"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=6323"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=6323"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}