The Impossible Filter Gallery Transition in CSS Only
DEV Community

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 The Impossible Filter Transition in CSS Only All Nature People Animals Lush green hills Trees in wanderlust Fox relaxing Panda on bench Children playing River in wanderlust Cougar in a forest Boat floating Parents with kid Mother and daughter Picture of a mountain Rhino in a field Palm trees Lion in a forest Green rolling hills Lepoard resting Tiger in a forest Kids jumping Painting of a sunset Kid enjoying 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); 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); 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); } } 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)

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.