2026 is almost upon us. I know we’re all itching to see the clock strike midnight (cue The Final Countdown by Europe), but not without recapping the best CSS-related things that happened over the last two weeks!
Annotating design system components for accessibility
Geri Reid carefully explained how to annotate design system components for accessibility, giving solid HTML-based examples. I especially love the part about defining elements, ARIA, markup, keyboard navigation, zoom, and user preferences as accessibility tokens.
Firefox to become a “modern AI browser” to the delight of absolutely no one
Firefox has been a bit of a rollercoaster lately. It started with an interview on The Verge, where new CEO Anthony Enzor-DeMeo said that Firefox “will evolve into a modern AI browser.” With only 2.3% of the browser market share, I suppose it was inevitable that Firefox would try to shake things up, but users don’t appear to want this at all. To stem the bleeding though, they’ve announced an AI kill switch.
And that’s how I met your Mo- …Waterfox, which is a no-AI fork of Firefox.
That’s right, 2025! Adam Argyle wrote about the CSS features that you should learn in 2025 at the beginning of the year. Well, it’s almost 2026 now, so how did you do? I’ll be honest — I barely even touched the surface of @property.
Great CSS posts on Bluesky
Let’s be honest, social media isn’t a fun place to hang out anymore, but I’ll make the exception for Bluesky. I rarely see stuff that’s not relevant to my interests, I actually talk to people on there, there’s a large dev community on there, and of course CSS-Tricks is on Bluesky too, as is the CSS-Tricks team and many of our authors.
So since browser updates typically ship at the beginning of the month, here are some cool posts by the Bluesky CSS community instead:
If you're going to write semantic CSS classes in 2025, don't call them .btn or .card.Use a ui-* prefix: ui-button, ui-input, ui-badge.Here's why naming matters more than you think…
Similar to Andy Clarke’s ‘toon text explorations, Ana Tudor’s displacement map text effects are rather astonishing as well, although you’ll most likely need Chrome for cutting-edge stuff like this:
I personally love displacement maps.When they work, they can do really cool stuff, not just shrinking/ expansion, but also rotation (codepen.io/thebabydino/…), 3D effects (codepen.io/thebabydino/…), the bubbling lens effect (codepen.io/thebabydino/…).None of these duplicate/ split text.
Writing CSS for my site that uses modern features only available in Chrome or Safari, but not both. Eventual consistency — eventually it’ll look right in one browser… right?
Actually, yes! Most likely. For those that don’t know, the Interop Project is a collaboration between Chrome, Firefox, and Safari where they make certain features work consistently across their browsers. You can even vote on the features! Keen to learn more? Read what Chrome and WebKit have said about Interop 2026.
Writing CSS for my site that uses modern features only available in Chrome or Safari, but not both.Eventual consistency — eventually it’ll look right in one browser… right?
Thank you to all who work very hard to expand and push the boundaries of what we can do with CSS. You’ve made my work and the work of millions of others so much easier. You’re not unnoticed, and I do hope you’re having an amazing day.
Thank you to all who work very hard to expand and push the boundaries of what we can do with CSS. You've made my work and the work of millions of others so much easier.You're not unnoticed, and I do hope you're having an amazing day.💛
If I had to whittle down my favorite thing about CSS-Tricks to one thing — and like Tom Petty hits, there’s a lot of ’em — it’s that we take moments like this to hit Pause and simply write like humans. We’re a super technical site that gets into super technical web dev jargon and yet we’re just a set of people trying to learn together with other people and be better at what we do, whether that’s design, development, accessibility, performance, or any specialty flavor from the wide array of front-end responsibilities.
This is my tenth year here at CSS-Tricks. I remember when CSS-Tricks turned 10. It felt really old (in the best possible way) back then. So imagine how I feel about my age today.
What I’m ultimately trying to say is Thank You. Because of real people like you and the small team of folks who contribute here, I get to make this my day job. It’s the best job I’ve had in my life and it’s only possible because you keep showing up each day to read, learn, share, and discuss all-things-front-end (and a little CSS, of course) with us.
Thank you, thank you… thank you!
Allow me a chance to share some highlights from 2025 — a year with a bunch of milestones and (let’s say) interesting twists.
Overall Site Traffic
Jumping straight to it: there were 20 million unique views in 2025. This is a huge drop-off from last year’s 26 million… and you can literally see the cliff in July when Google added AI summaries to the top of search results. So, yeah, we’re down an alarming 23% for the year, but the real month-over-month impact is more like 30%. Ouch.
It hurts — and I’m always quick to blame myself — but is also consistent with other sites I work with and what I’ve been hearing from other publishers in this space as we’ve compared battle notes.
I hate saying “it is what it is” but we really are in the midst of a new reality in digital publishing. I’m still trying to wrap my head around it because, obviously, eyeballs pay the bills around here and we’re going to have to adapt. There’s still plenty of reasons to make websites today. Making them sustainable, though? That’s getting harder, even if new CSS features are making the development way more fun.
Which brings me full circle to the start of this post: Thank you for showing up. It means the world to us and to me personally. The first and best way you can support CSS-Tricks is to continue showing up.
Publishing By The Numbers
Looking at a little history of how many articles we’ve published by year:
2020: 1,183 articles
2021: 890 articles (site acquired by DigitalOcean)
2022: 390 articles
2023: 0 articles (site paused)
2024: 153 articles (site resumed in late June)
This year? We’re looking at 255 articles. Considering there are 261 weekdays in 2025, that’s nearly an article per day… and we’re not done yet with the year. In fact, if we count this post and what’s left on the publishing calendar, we’re looking at exactly 261 articles for the year. It’s sorta like we’re your weekday companion at work!
The CSS-Tricks publishing schedule is always cookin’.
That includes the largest infusion to the Almanac in a year, perhaps ever? We added 101 Almanac entries as of today (one for each Dalmatian) and will add three more by the end of the year. The Almanac is my favorite part of the site. Sure, you can get great CSS documentation from somewhere like MDN, but I think the time and effort we put into explaining CSS features like one developer talking to another sets us apart. Where else are you going to enjoy learning about a trigonometry function like atan2(), right?
Let’s Keep Talking About the Almanac
…because that’s the area we invested most of our time and energy. You may remember that the Almanac has historically been a spot to learn about CSS properties and pseudos. Last year, though, we stuffed the Almanac’s mouth with a fistful of steroids and gave it new sections so that we’re covering all the CSS features we possibly can, including functions, selectors, and at-rules.
If you can believe it, I’m pretty sure we’ve added every single at-rule this year. And we started the year with a big ol’ zero CSS functions and are ending with a whopping 64 of them.
We also spent time making the Almanac a little easier to navigate. For example, now you can get high-level information about any feature without having to click through to the full page:
And we label experimental and shorthand properties:
And you can dig into the constituent properties for each shorthand:
Things wouldn’t be complete without a little dash of modern CSS. We sprinkled in a little scroll-driven animation action for good measure:
The Team
I introduced y’all to Juan Diego Rodriguez and Ryan Trimble last year as regular contributors. They play a big role and I don’t know what I’d do without them. They’re my second and third pair of eyes anytime I’m unsure of something and, let’s face it, that’s often.
But now we have a fourth pair of eyes! Danny Schwarz has been a long-time contributor and he’s stepping up to help us stay on top of timely things. CSS is moving faster than I can remember and it’s easy for things to slip under the radar even when your day job is tracking this stuff. Danny has the eyes of a hawk and has started reporting what he finds in what we’re calling What’s !Important. The first edition went out just the other day and we’ll keep that up on a bi-weekly basis for now.
In between editions, Danny publishes a feed of Quick Hits that you can follow for even more web platform news and happenings.
It takes a village, my friends!
Goal Review
Time to check in on the things we set out to accomplish last year and did… or didn’t.
✅ Publish 1-2 new guides. Yes! We released CSS Color Functions and CSS Counters guides this past year. We actually nailed the goal back in June. I thought we might actually exceed the goal, but things settled down with all the work we put into the next goal.
✅ Fill in the Almanac. No need to rehash all the work. Last year at this time, I said, “We’ve only got a few pages in the at-rules and functions sections that we recently created and could use all the help we can get.” I never imagined we’d end the year with 104 new pages, including all of the at-rules. I can’t thank the likes of Juan Diego, Danny, Sunkanmi Fafowora, John Rhea, and Gabriel Shoyombo enough for going above and beyond to make this one happen. Again, I’m incredibly proud of this treasure trove of CSS documentation and believe it’s a core part of what CSS-Tricks is.
🚫 Restart the newsletter. Nope on this one, and not for lack of effort. The truth is there are administrative hurdles behind the scenes preventing it from happening. We’ll get there though! It’s a piece of CSS-Tricks that I miss so dang much. Perhaps that’s the next evolution of What’s !Important.
RECOVER ALL THE LOST TRAFFIC! Just kidding, that’s only mildly in our control. We’ll continue the daily mission of serving you fresh, fun, and educational front-end goodness. While I’d like to tackle traffic woes head-on, the best plan will always be showing up and delivering the goods as best we can. In fact, we’ll carry over this year’s goals into the brand-new year. No need to shake up the recipe.
If I was given a magic lamp with one wish for the next year, I’d wish for full courses to be added to the site. I run a beginning level HTML/CSS online course separately and love how students interact with the lessons differently than the average reader interacts with a standalone tutorial or article. It’d be a moonshot to get something like that into CSS-Tricks in the next year, but I wouldn’t turn my nose up if the opportunity came up.
Again, Thank You!
Special thanks to DigitalOcean! It’s their backing that keeps this engine running, from hosting to finances and even helpful encouragement along the way. They have every right to do anything they want with this site and yet they choose to operate it like an independent publication. They’re not jamming their products and services down anyone’s throat, dictating the editorial direction of things, or constantly breathing down our necks. They’re good stewards and deserve a big collective high five!
A special shout out to Roxie Elliott for being the go-between on just about everything you can imagine. Her behind-the-scenes help has been incredibly valuable.
Like I said, it’s been an interesting year. Some ups, some downs, but plenty to be thankful for heading into what will be this site’s 19th birthday come July 2026. Nineteen years. Blockbuster still had a site then!
The text-decoration-inset CSS property solves a problem that we’ve had since the beginning of the web, which is that text decorations such as underlines extend beyond the first and last characters (to the edges of the content box, to be specific), resulting in vertical misalignment.
I say it’s a problem “we’ve” had rather sheepishly because maybe you, like some users, don’t actually care. But if you’re a funny bunny like me (I think “designer” is the technical term) then it most likely drives you crazy.
That being said, it’s not a problem that I’ve tried to fix because the juice just isn’t worth the squeeze. The best fix is probably text-decoration: none and ::after with a custom background, but this can be a bit finicky and I’d rather use all of the features that come with native text decorations, such as text-decoration-thickness, text-underline-position (which enables us to change the position of the underline relative to the font’s internal metrics; the baseline, for example), and text-underline-offset (which determines the offset from that position).
So, how does text-decoration-inset work? Well, if I trim an underline just enough for it to vertically align with the text, I wind up with this instead (this only works in Firefox 146, by the way):
However, you can actually trim the decorations as much as you want, which enables us to create some really cool ones and even transition or animate them. Let’s take a quick look, shall we?
text-decoration-inset basic usage
text-decoration-inset, formerly text-decoration-trim, enables us to clip from the ends of the underline or whatever text-decoration-line is computed. This is the syntax:
Yes, this means that we can set different inset values for the left and right sides.
These values must be <length>s, but we can use relative lengths such as em units, which are relative to the computed font-size. So, if the font-size changes, the insets scale with it. For example, in the demo above, 0.076em (which is what I’ve set as the left inset) means 7.6% of the computed font-size, and that’s the value that makes the left inset align with the left stem of the letter “N” and other left stems. This value was determined by trial and error, but it only needs to be determined once for each font.
If that first letter was, say, W? Yeah, then the inset wouldn’t align, so it’s not a perfect solution. I’d say that it’s suitable for when you know what the content will be.
Maybe the W3C will come up with a solution for vertically aligning text decorations as well as multiple lines of text both accurately and automatically. Until then, this is still a cool solution that enables us to create perfectly aligned effects like this (this demo uses an overline and an underline, and a whole ‘lotta text-decoration-thickness of course):
Animating text-decoration-inset
text-decoration-inset is more interesting when we start to think about transitions/animations. We often animate underlines, or should I say faux ::after underlines, but with text-decoration-inset we can do it natively. In the example below I multiply the insets by ten on :hover. Nothing too crazy, but remember that we can only use <length> values, so try to use em units, or at least test the text with different font sizes.
Getting a bit more ambitious now, this next demo leverages a CSS @keyframes animation to create that shooting star underline effect. How it works is that we push the left inset all the way to the other side — but <length>s only, remember? We can’t use 100% here, so instead I’ve determined that the width of the element is 4.5em and used that as the value instead (the more precise we are, the better the animation or transition). Check the code comments for a full explanation:
a {
/*
The value at the start and end of the
animation, as well as the default value
*/
text-decoration-inset: 0.046em 0.009em;
&:hover {
animation: 1s next-level;
}
}
@keyframes next-level {
/* By half-way through the animation... */
50% {
/*
...the left inset has shifted 4.5em,
which is the full width of the element
*/
text-decoration-inset: 4.5em 0.009em;
/* It’s faded out as well */
text-decoration-color: transparent;
}
/* Immediately after that... */
50.999% {
/* ...both insets are set to the left */
text-decoration-inset: 0.046em 4.5em;
}
/* Then it animates back to the default value */
}
Overall, text-decoration-inset is a nice feature. It isn’t without its flaws, but no feature ever is. Personally, anything that helps me to refine a detail natively is very much welcome, and with text-decoration-inset we’re able to refine two — the text decoration alignment (relative to the text) and the text decoration transition or animation.
Here’s what you do need to know: it’s going to be display: grid-lanes.
The earliest talks concerning masonry began in 2017 when Rachel Andrew expressed some concerns about how to make a Pinterest masonry layout. Rachel said it felt like the right approach should be a flexbox kind of thing, but this wasn’t fully achievable with flexbox, as items would rather flow from top-to-bottom rather than across each row:
Currently the closest you can get with CSS to this type of layout is to use multi-col however, the items then flow top to bottom rather than across the rows.
This feels more like a behaviour of flexbox than grid, as the solution is very much based on the size of the items. Opening this in order to record the feature request/use case for future discussion.
And so, the talks surrounding masonry layout in CSS began within the W3C. In that same GitHub thread, you’ll read that Tab Atkins-Bittner shared a “hacky” way to go about it with CSS Grid.
You can have Grid do Masonry if you know the heights of the items ahead of time. It’s a bit hacky, but you set the grid container to grid: auto-flow dense 1px / <column widths here>;, then set each item to grid-row: span <pixel height as integer>; (for example, if the item is 50px tall, use grid-row: span 50;).
I’m a sucker for clever CSS hacks, but you know what else I’m a sucker for? A working CSS solution that makes it easier for beginners to use. And no, unfortunately, we cannot use grid-template-rows or grid-template-columns, as Nate Green suggested in the same thread:
There’s no “simple” way to adapt Grid into Masonry – anything would involve non-trivial edits to the layout algorithm. Packery, in particular, really needs things to have a defined width.
Okay, that’s fine. I’m sure you’d be thinking as well, “why not create a new display property for it?” Well, Rachel added that:
The thing is we can’t create a brand new value of display for every distinct design pattern.
Mind you, all these discussions were happening as far back as 2017, meaning masonry has been an elusive goal for quite some time.
Just to make sure you’re with me, this is an example image of the kind of layout masonry looks like (thanks to Michael Richins for this):
One thing I would like is that prior to the announcement of masonry display type, there were solutions/hacks discussed, like this one from Andy Barefoot, but nothing truly was decided as the main go-to feature for CSS masonry. Hey, there was even a super cool Masonry library from David DeSandro, and he gave pretty useful advice to W3C and browser vendors when writing the spec and implementing the masonry layout, like on how loading images would work, multi-column spanning items, filling gaps, retaining horizontal order.
Check out the Masonry.js docs for more information about the object options.
This thread helped, but it still didn’t lead to a conclusive statement about what the masonry syntax will look like. In fact, we had code suggestions, like from Dan Tonon:
This introduces two new flexbox properties called flex-block-count and flex-block-flow which allows us to control items similarly (with column-count) and control the flow of items, respectively. However, this wouldn’t fly as Michael Richins pointed out:
This will still not work for children of different height…
I think you have a good point. Currently, float is becoming old and flexbox allow a lot of things to be done, but this masonry layout is lacking in both.
… Sometimes you can have lot of small images in a row, so making it left to right can’t work all the time. Masonry is left to right like always, but in a way it’s easier for the user to access informations.
Personally disappointed that we’re not making more progress. We’ve been having this argument for 5 years.
Five years of back and forth on how exactly the naming and structure would be. Do we use grid? Do we use a brand new property? Do we create separate properties just for masonry? We couldn’t really decide on that.
…it should be possible to use a new unprefixed template-areas property for masonry and grid, regardless of the formatting context..
Fortunately, something good came out of that thread because another meeting was held on January 31st of 2025, and guess what? It was resolved to reuse grid templating and placement properties for masonry layout.
The CSS Working Group just discussed [css-grid-3][masonry] Masonry Syntax Debate, and agreed to the following: RESOLVED: Re-use grid templating and placement properties for masonry layout.
Wait. Why am I announcing about masonry syntax properties? What about the main syntax itself? Isn’t that why we’re here? Awesome, you’re still with me. So let me show you the exact thread where the masonry syntax war ended.
It’s (almost) here!
This brings us to the latest announcement that setting grid-lanes on the display property activates a masonry layout.
.masonry {
display: grid-lanes;
}
It was tough to get here. Deciding on the keyword alone took years. Just look at all of the names that were considered leading up to grid-lanes:
collapsed-grid or grid collapse
grid-stack or stacked-grid
grid-pack or packed-grid or grid pack
compact-grid or compact grid
grid-masonry or masonry-grid
grid-flex or flex-grid
grid single-axis (masonry grid) vs. grid dual-axis (normal grid)
grid stack (masonry grid) vs. grid grid (normal grid)
staggered-grid or grid staggered
uneven-grid
semi-grid
lane-grid or grid-lanes
axial-grid
I wouldn’t have minded staggered-grid since that’s what it is in React Native. In fact, Kevin Powellagreed. That said, I am perfectly okay with grid-lanes. It will take some time for browsers to implement it, as discussions surrounding the shorthand adjustments for masonry are still going on, with nothing much said on the issue just yet.
After many back-and-forths, masonrygrid-lanes is here! But where exactly?
Well, this is the awkward part. The truth is we’ll still have to wait for browsers to implement grid-lanes. How long may it take? I really can’t say, but take into consideration that all major browsers already have implemented some type of masonry layout behind flags or previews, but with different syntaxes:
The Chrome and Edge teams initially implemented Masonry in Chromium 140 as display: masonry but are already working on switching to grid-lanes. Some of their demos already reflect it.
The Mozilla team was actually the first browser to implement masonry layout in 2020 — using display: grid as well. But like the others, they’re already in the process of making the switch to grid-lanes.
So, good news all around! All that’s left is to wait. If you want to track grid-lanes progress, here is a good link pool for each browser from Patrick Brosset. And, if you can’t wait for the official masonry implementation, Zell Liew has an approach for using it today, with minimal JavaScript.
I have to search for articles often on this site. And I’d say searching this site is Pretty OK™ in general. Making content searchable on a site with 18+ years of published content is gonna be challenging no matter what, and the Jetpack Search tool we use is darned good considering it’s a drop-in solution.
At the same time, it’s a bit heavy-handed when all I really need is a quick URL I can copy-paste into some other article I’m working on. What would be cool is a search straight from my local machine which is totally possible by fetching the content from the WordPress REST API that runs under the hood of this site.
That’s exactly what Jelte Lagendijk built for Raycast, a “shortcut-all the-things” app for macOS and Windows. It’s a little extension where you simply type and a get a solid set of real-time results.
Click any item and get the high-level summary with a path to open it up in the browser.
Ready for the second part? If you recall, last time we worked on a responsive list of overlapping avatar images featuring a cut-out between them.
We are still creating a responsive list of avatars, but this time it will be a circular list.
This design is less common than the horizontal list, but it’s still a good exercise to explore new CSS tricks.
Let’s start with a demo. You can resize it and see how the images behave, and also hover them to get a cool reveal effect.
The following demo is currently limited to Chrome and Edge, but will work in other browsers as the sibling-index() and sibling-count() functions gain broader support. You can track Firefox support in Ticket #1953973 and WebKit’s position in Issue #471.
We will rely on the same HTML structure and CSS base as the example we covered in Part 1: a list of images inside a container with mask-ed cutouts. This time, however, the positions will be different.
There are several techniques for placing images around a circle. I will start with my favorite one, which is less known but uses a simple code that relies on the CSS offset property.
The code doesn’t look super intuitive, but its logic is fairly straightforward. The offset property is a shorthand, so let’s write it the longhand way to see how breaks down:
We define a path to be a circle with a radius of 180px. All the images will “follow” that path, but will initially be on top of each other. We need to adjust their distance to change their position along the path (i.e., the circle). That’s where offset-distance comes into play, which we combine with the sibling-index() and sibling-count() functions to create code that works with any number of elements instead of working with exact numbers.
For six elements, the values will be as follows:
100% x 1/6 = 16.67%
100% x 2/6 = 33.33%
100% x 3/6 = 50%
100% x 4/6 = 66,67%
100% x 5/6 = 83.33%
100% x 6/6 = 100%
This will place the elements evenly around the circle. To this, we add a rotation equal to 0deg using offset-rotate to keep the elements straight so they don’t rotate as they follow the circular path. From there, all we have to do is update the circle’s radius with the value we want.
That’s my preferred approach, but there is a second one that uses the transform property to combine two rotations with a translation:
The translation contains the circle radius value and the rotations use generic code that relies on the sibling-* functions the same way we did with offset-distance.
Even though I prefer the first approach, I will rely on the second one because it allows me to reuse the rotation angle in more places.
The Responsive Part
Similar to the horizontal responsive list from the last article, I will rely on container query units to define the radius of the circle and make the component responsive.
Resize the container in the demo below and see how the images behave:
It’s responsive, but when the container gets bigger, the images are too spread out, and I don’t like that. It would be good to keep them as close as possible. In other words, we consider the smallest circle that contains all the images without overlap.
Remember what we did in the first part: we added a maximum boundary to the margin for a similar reason. We will do the same thing here:
--_r: min(50cqw - var(--s)/2, R);
I know you don’t want a boring geometry lesson, so I will skip it and give you the value of R:
For this part, we will be using the same mask that we used in the last article:
mask: radial-gradient(50% 50% at X Y, #0000 calc(100% + var(--g)), #000);
With the horizontal list, the values of X and Y were quite simple. We didn’t have to define Y since its default value did the job, and the X value was either 150% + M or -50% - M, with M being the margin that controls the overlap. Seen differently, X and Y are the coordinates of the center point of the next or previous image in the list.
That’s still the case this time around, but the value is trickier to calculate:
The idea is to start from the center of the current image (50% 50%) and move to the center of the next image (X and Y). I will first follow segment A to reach the center of the big circle and then follow segment B to reach the center of the next image.
This is the formula:
X = 50% - Ax + Bx
Y = 50% - Ay + By
Ax and Ay are the projections of the segment A on the X-axis and the Y-axis. We can use trigonometric functions to get the values.
Ax = r x sin(i);
Ay = r x cos(i);
The r represents the circle’s radius defined by the CSS variable --_r, and i represents the angle of rotation defined by the CSS variable --_i.
Same logic with the B segment:
Bx = r x sin(j);
By = r x cos(j);
The j is similar to i, but for the next image in the sequence, meaning we increment the index by 1. That gives us the following CSS calculations for each variable:
Cool, right? You might notice two different implementations for the cut-out. The formula I used previously considered the next image, but if we consider the previous image instead, the cut-out goes in another direction. So, rather than incrementing the index, we decrement instead and assign it to a .reverse class that we can use when we want the cut-out to go in the opposite direction:
Similar to what we did in the last article, the goal of this animation is to remove the overlap when an image is hovered to fully reveal it. In the horizontal list, we simply set its margin property to 0, and we adjust the margin of the other images to prevent overflow.
This time, the logic is different. We will rotate all of the images except the hovered one until the hovered image is fully visible. The direction of the rotation will depend on the cut-out direction, of course.
To rotate the image, we need to update the --_i variable, which is used as an argument for the rotate function. Let’s start with an arbitrary value for the rotation, say 20deg.
Now, when an image is hovered, all of images rotate by 20deg. Try it out in the following demo.
Hmm, the images do indeed rotate, but the mask is not following along. Don’t forget that the mask considers the position of the next or previous image defined by --_j and the next/previous image is rotating — hence we need to also update the --_j variable when the hover happens.
Oops, the mask is off again! Do you see the issue?
We want to stop the hovered image from rotating while allowing the rest of the images to rotate. Therefore, the --_j variable of the hovered image needs to update since it’s linked to the next or previous image. So we should remove --_jj: 0deg and keep only --_ii: 0deg.
.container img:hover {
--_ii: 0deg;
}
That’s a little better. We fixed the cut-out effect on the hovered image, but the overall effect is still not perfect. Let’s not forget that the hovered image is either the next or previous image of another image, and since it’s not rotating, another --_j variable needs to remain unchanged.
For the first list, it’s the variable of the previous image that should remain unchanged. For the second list, it’s the variable of the next image:
/* select previous element of hovered */
.container:not(.reverse) img:has(+ :hover),
/* select next element of hovered */
.container.reverse img:hover + * {
--_jj: 0deg;
}
In case you are wondering how I knew to do this, well, I tried both ways and I picked the one that worked. It was either the code above or this:
We are getting closer! All the images behave correctly except for one in each list. Try hovering all of them to identify the culprit.
Can you figure out what we are missing? Think a moment about it.
Our list is circular, but the HTML code is not, so even if the first and last images are visually placed next to each other, in the code, they are not. We cannot link both of them using the adjacent sibling selector (+). We need two more selectors to cover those edge cases:
Oof! We have fixed all the issues, and now our hover effect is great, but it’s still not perfect. Now, instead of using an arbitrary value for the rotation, we need to be accurate. We have to find the smallest value that removes the overlap while keeping the images as close as possible.
We can get the value with some trigonometry. I’ll skip the geometry lesson again (we have enough headaches as it is!) and give you the value:
This one was a bit tough, right? Don’t worry if you got a bit lost with all the complex formulas. They are very specific to this example, so even if you have already forget about them, that’s fine. The goal was to explore some modern features and a few CSS tricks such as offset, mask, sibling-* functions, container query units, min()/max(), and more!