DEV Community

Cover image for The Impossible Filter Gallery Transition in CSS Only
Prahalad S
Prahalad S

Posted on Edited on

The Impossible Filter Gallery Transition in CSS Only

The Impossible Filter Gallery Transition in CSS Only YouTube Video Explanation

*Look at the gif image above. Your eyes are lying to you. You expected JavaScript. But CSS took it personally. *

Please subscribe to my youTube channel for more info.
subscribe @css-info YouTube
Please follow @onlypurecss Instagram

Most filter galleries use JavaScript to read selected options and update the visible items. For this experiment, I wanted to see how far CSS selectors could take the same idea. Back in 2024 I discovered the hardcoded-variable trick, still stuck on it today trying to achieve this without hardcoding but couldn't due to CSS limitation. So I wanted to post this anyway.

Let's jump into the code. Add cards with different categories and 4 input radio buttons. The trick is all in the custom variables.

custom variables:
--l, --t for default card placements with position absolute.
--x, --y for click events - To move the cards in new positions with transitions.

responsive custom variables:
--l1, --t1 for default card placements with position absolute.
--x1, --y1 for click events - To move the cards in new positions with transitions.

create HTML

<div class="gallery">
    <h1> The <span>Impossible Filter</span><br>Transition in CSS Only</h1>
        <div>
                <input type="radio" name="slideLeft" id="all" checked><label for="all">All</label>
                <input type="radio" name="slideLeft" id="nature"><label for="nature">Nature</label>
                <input type="radio" name="slideLeft" id="people"><label for="people">People</label>
                <input type="radio" name="slideLeft" id="animals"><label for="animals">Animals</label>
        </div>

        <ul class="filter">
                <li class="category nature" style="--l:0;--t:0;--x:0;--y:0; --l1:0;--t1:0; --x1:0;--y1:0;"><img  src="https://images.unsplash.com/vector-1777222917158-18d7de997d83?w=500&auto=format&fit=crop&q=60" alt="Landscape" tabindex="0"><span>Lush green hills</span></li>
                <li class="category nature" style="--l:1;--t:0;--x:1;--y:0; --l1:1;--t1:0; --x1:1;--y1:0;"><img src="https://plus.unsplash.com/premium_vector-1697729804286-7dd6c1a04597?w=500&auto=format&fit=crop&q=60" alt="" tabindex="0"><span>Trees in wanderlust</span></li>
                <li class="category animals" style="--l:2;--t:0;--x:0;--y:0; --l1:2;--t1:0; --x1:0;--y1:0;"><img src="https://images.unsplash.com/vector-1749806268611-72e774c6f235?w=500&auto=format&fit=crop&q=60" tabindex="0"><span>Fox relaxing</span></li>
                <li class="category animals" style="--l:3;--t:0;--x:1;--y:0; --l1:3;--t1:0; --x1:1;--y1:0;"><img src="https://images.unsplash.com/vector-1777378571488-fdaa0332fcac?w=500&auto=format&fit=crop&q=60" alt="" tabindex="0"><span>Panda on bench</span></li>
                <li class="category people" style="--l:4;--t:0;--x:0;--y:0; --l1:0; --t1:1; --x1:0;--y1:0;"><img src="https://plus.unsplash.com/premium_vector-1729529625915-52ce7e62d369?w=500&auto=format&fit=crop&q=60" alt="" tabindex="0"><span>Children playing</span></li>
                <li class="category nature" style="--l:0;--t:1;--x:2;--y:0; --l1:1; --t1:1; --x1:2;--y1:0;"><img src="https://plus.unsplash.com/premium_vector-1697729816538-9c0753bc28f0?w=500&auto=format&fit=crop&q=60" alt="" tabindex="0"><span>River in wanderlust</span></li>
                <li class="category animals" style="--l:1;--t:1;--x:2;--y:0; --l1:2; --t1:1; --x1:2;--y1:0;"><img src="https://images.unsplash.com/vector-1777382626483-ba6ae9c57aae?w=500&auto=format&fit=crop&q=60" alt="" tabindex="0"><span>Cougar in a forest</span></li>
                <li class="category nature" style="--l:2;--t:1;--x:3;--y:0; --l1:3; --t1:1; --x1:3;--y1:0; "><img src="https://plus.unsplash.com/premium_vector-1702650077705-23394d0a0b52?w=500&auto=format&fit=crop&q=60" alt="" tabindex="0"><span>Boat floating </span></li>
                <li class="category people" style="--l:3;--t:1;--x:1;--y:0; --l1:0; --t1:2;  --x1:1;--y1:0;"><img src="https://plus.unsplash.com/premium_vector-1744241910489-eb8d2ebeb46a?w=500&auto=format&fit=crop&q=60" alt="" tabindex="0"><span>Parents with kid</span></li>
                <li class="category people" style="--l:4;--t:1;--x:2;--y:0; --l1:1; --t1:2; --x1:2;--y1:0;"><img src="https://plus.unsplash.com/premium_vector-1682270612979-075136091c39?w=500&auto=format&fit=crop&q=60" alt="" tabindex="0"><span>Mother and daughter</span></li>
                <li class="category nature" style="--l:0;--t:2;--x:4;--y:0; --l1:2; --t1:2;  --x1:0;--y1:1;"><img src="https://plus.unsplash.com/premium_vector-1721983446943-a8dbff1df8e5?w=500&auto=format&fit=crop&q=60" alt="" tabindex="0"><span>Picture of a mountain</span></li>
                <li class="category animals" style="--l:1;--t:2;--x:3;--y:0; --l1:3; --t1:2; --x1:3;--y1:0;"><img src="https://plus.unsplash.com/premium_vector-1746328421184-ccad64644869?w=500&auto=format&fit=crop&q=60" alt="" tabindex="0"><span>Rhino in a field</span></li>
                <li class="category nature" style="--l:2;--t:2;--x:0;--y:1; --l1:0;--t1:3; --x1:1;--y1:1; "><img src="https://images.unsplash.com/vector-1761633623751-c8c6d5340eb0?w=500&auto=format&fit=crop&q=60" alt="" tabindex="0"><span>Palm trees</span></li>
                <li class="category animals" style="--l:3;--t:2;--x:4;--y:0; --l1:1; --t1:3; --x1:0;--y1:1;"><img src="https://plus.unsplash.com/premium_vector-1731085539175-ad326aa520c1?w=500&auto=format&fit=crop&q=60" alt="" tabindex="0"><span>Lion in a forest</span></li>
                <li class="category nature" style="--l:4;--t:2;--x:1;--y:1; --l1:2; --t1:3; --x1:2;--y1:1;"><img src="https://images.unsplash.com/vector-1777302627684-0ea81b1af39c?w=500&auto=format&fit=crop&q=60" alt="" tabindex="0"><span>Green rolling hills</span></li>
                <li class="category animals" style="--l:0;--t:3;--x:0;--y:1; --l1:3; --t1:3; --x1:1;--y1:1;"><img src="https://images.unsplash.com/vector-1777345028029-6b1238808fa7?w=500&auto=format&fit=crop&q=60" alt="" tabindex="0"><span>Lepoard resting</span></li>
                <li class="category animals" style="--l:1;--t:3;--x:1;--y:1; --l1:0; --t1:4; --x1:2;--y1:1;"><img src="https://images.unsplash.com/vector-1777382626693-bfcb77affc2c?w=500&auto=format&fit=crop&q=60" alt="" tabindex="0"><span>Tiger in a forest</span>
                <li class="category people" style="--l:2;--t:3;--x:3;--y:0; --l1:1; --t1:4; --x1:3;--y1:0;"><img src="https://plus.unsplash.com/premium_vector-1728283906027-78d55b56f6cb?w=500&auto=format&fit=crop&q=60" alt="" tabindex="0"><span>Kids jumping</span></li>
                <li class="category nature" style="--l:3;--t:3;--x:2;--y:1; --l1:2; --t1:4; --x1:3;--y1:1;"><img src="https://plus.unsplash.com/premium_vector-1721494020721-45d7295df5e0?w=500&auto=format&fit=crop&q=60" alt="" tabindex="0"><span> Painting of a sunset</span></li>
                <li class="category people" style="--l:4;--t:3;--x:4;--y:0; --l1:3; --t1:4; --x1:0;--y1:1;"><img src="https://images.unsplash.com/vector-1748298828860-16eff573e619?w=500&auto=format&fit=crop&q=60" alt="" tabindex="0"><span>Kid enjoying</span></li>
        </ul>
 </div>
Enter fullscreen mode Exit fullscreen mode

Logic for initial placement of cards using custom variables
left and top positions:

we have to multiply with our width variable(--w) x custom variable(--l) for default placements of all cards for left and top positions.

- left: calc(var(--l) * var(--w) * 1px);
- top: calc(var(--t) * var(--h) * 1px);
Enter fullscreen mode Exit fullscreen mode

This will create in order for left positions:
(--l:0, --l:1, --l:2.... x --w: 150) = 0, 150px, 300px, 350...

For top: height variable(--h) x custom variable(--t) for default placements of all cards for top positions.

  • cards on first row: top = (--t:0 x --h: 100) = 0x100 = 0px;
  • cards on second row: top = (--t:1 x --h: 100) = 1x100 = 100px;
  • repeat same for next row = 300px....

Same trick for click events:
Again assigning left and top positions for all categories separately.

- left: calc(var(--x) * var(--w) * 1px);
- top: calc(var(--y) * var(--h) * 1px);
Enter fullscreen mode Exit fullscreen mode

create CSS

@import url(https://fonts.bunny.net/css?family=jetbrains-mono:200);

:root {
  --w: 150; /* card width */
  --h: 100; /* card height */
  --text: #f5f7fb;
}

body {
  margin: 0;
  padding: 0;
  background: #070a10;
  font-family: 'JetBrains Mono', monospace;
  overflow-x: hidden;
}

.gallery {
  width: 740px;
  margin: 10px auto;
}

.gallery>div {
  padding-bottom: 10px;
}

h1 {
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-size: clamp(38px, 6vw, 76px);
  color: var(--text);
  line-height: .95;
  letter-spacing: -.055em;
  margin-bottom: 50px;
}

h1 span {
  background: linear-gradient(90deg, #818cf8, #c084fc, #f472b6);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

input[type="radio"] {
  display: none;
}

input[type="radio"]+label {
  position: relative;
  width: auto;
  cursor: pointer;
  text-align: center;
  color: var(--text);
  background: crimson;
  box-shadow: 0 5px 18px rgba(124, 92, 255, .25);
  margin: 0;
  font-family: monospace;
  text-transform: uppercase;
  padding: 5px 10px;
  display: inline-block;
  font-size: 1rem;
  font-weight: 600;
  border-radius: 2px;
}

input[type="radio"]:checked+label {
  background: white;
  color: black;
}

ul.filter {
  padding: 0;
  list-style-type: none;
  position: relative;
}

ul.filter li {
  box-sizing: border-box;
  -moz-box-sizing: border-box;
  -webkit-box-sizing: border-box;
  /* -10px for gaps between cards; */
  width: calc(var(--w) * 1px - 10px);
  height: calc(var(--h) * 1px - 10px);
  position: absolute;
  margin: 0;
  padding: 0;
  left: calc(var(--l) * var(--w) * 1px);
  top: calc(var(--t) * var(--h) * 1px);
  cursor: pointer;
  transition: 0.5s all ease;
}

/* Logic for initial placement of cards using custom variables */
body:has(#all:checked) li {
  left: calc(var(--l) * var(--w) * 1px);
  top: calc(var(--t) * var(--h) * 1px);
}

/* Logic for changing placements of cards when clicked */
body:has(#nature:checked) .nature,
body:has(#animals:checked) .animals,
body:has(#people:checked) .people {
  left: calc(var(--x) * var(--w) * 1px);
  top: calc(var(--y) * var(--h) * 1px);
}

/* Filter cards(Using :has() pseudo-class) to show only the selected category and hide other categories */
body:has(#nature:checked, #people:checked) .animals,
body:has(#animals:checked, #people:checked) .nature,
body:has(#nature:checked, #animals:checked) .people {
  opacity: 0;
  scale: 0;
  width: 0;
  height: 0;
}

@media (max-width: 760px) {
  .gallery {
    width: 600px;
  }

/* separate variables for breakpoints (for initial placement of cards --l1:0; --t1:0; ) and (for changing placement of cards when clicked --x1:0; --y1:0;) */
  ul.filter li {
    left: calc(var(--l1) * var(--w) * 1px);
    top: calc(var(--t1) * var(--h) * 1px);
  }

  body:has(#all:checked) li {
    left: calc(var(--l1) * var(--w) * 1px);
    top: calc(var(--t1) * var(--h) * 1px);
  }

/* Click events - Responsive */
  body:has(#nature:checked) .nature,
  body:has(#animals:checked) .animals,
  body:has(#people:checked) .people {
    left: calc(var(--x1) * var(--w) * 1px);
    top: calc(var(--y1) * var(--h) * 1px);
  }
}

/* Open lightbox view when an image is clicked */
ul.filter img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  cursor: pointer;
  transition: all 0.3s ease;
  position: relative;
  z-index: 1;
  border-radius: 8px;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}

/* Dark backdrop */
ul.filter:has(img:focus)::before {
  content: "";
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.85);
  z-index: 998;
}

/* The focused image enlarges and centers itself */
ul.filter img:focus {
  position: fixed;
  inset: 0;
  opacity: 1;
  margin: auto;
  animation: size 1s ease forwards;
  width: clamp(200px, 55vw, 500px);
  height: clamp(151px, 41.47vw, 279px);
  /* object-fit: cover; */
  z-index: 999;
  outline: none;
  border-radius: 8px;
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}

/* caption hidden by default */
ul.filter img+span {
  display: none;
}

ul.filter img:focus+span {
  display: block;
  position: fixed;
  /* 50% center + half of the image height + 10px gap */
  top: calc(50% + (clamp(200px, 50vw, 500px) / 2) + 10px);
  left: 0;
  opacity: 1;
  width: 100%;
  text-align: center;
  color: white;
  font-size: 1.1rem;
  font-weight: 500;
  z-index: 998;
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
  animation: showCaption 1s ease;
  pointer-events: none;
}

/* Enlarge image when lightbox opens */
@keyframes size {
  0% {
    opacity: 0;
    width: calc(var(--w) * 1px - 10px);
    height: calc(var(--h) * 1px - 10px);
  }

  to {
    opacity: 1;
    width: clamp(200px, 55vw, 500px);
    height: clamp(151px, 41.47vw, 279px);
  }
}

/* Show image caption when lightbox opens */
@keyframes showCaption {
  0% {
    opacity: 0;
    top: 50%;
  }

  to {
    opacity: 1;
    top: calc(50% + (clamp(200px, 50vw, 500px) / 2) + 10px);
  }
}
Enter fullscreen mode Exit fullscreen mode

Lightbox is also included in this code. I Added separate variables for breakpoints (For placement of cards --l1:0; --t1:0; ) and (For changing placement of cards when clicked --x1:0; --y1:0;)

If needed more breakpoints add more custom variables.
For better understanding use as many as custom variables you can for breakpoints like bootstrap(default placements --lg, --md, --sm, --xs) and (when clicked --lg_x, --lg_y), (--md_x, --md_y), (--sm_x, --sm_y), (--xs_x, --xs_y).

I’ll post another video on YouTube and a tutorial on DEV soon. This next CSS trick is simpler and uses much less hardcoded code than this. Stay tuned!

Below is the full explanation in youtube video
YouTube Video Explanation
Codepen Demo

Please subscribe to my youTube channel for more info.
subscribe @css-info YouTube
Please follow @onlypurecss Instagram

Top comments (0)