Tech News
Fancy Menu Navigation Using Anchor Positioning
You have for sure heard about the new CSS Anchor Positioning, right? It’s a feature that allows you to link any element from the page to another one, i.e., the anchor. It’s useful for all the tooltip stuff, but it can also create a lot of other nice effects.
In this article, we will study menu navigation where I rely on anchor positioning to create a nice hover effect on links.
CodePen Embed FallbackCool, right? We have a sliding effect where the blue rectangle adjusts to fit perfectly with the text content over a nice transition. If you are new to anchor positioning, this example is perfect for you because it’s simple and allows you to discover the basics of this new feature. We will also study another example so stay until the end!
Note that only Chromium-based browsers fully support anchor positioning at the time I’m writing this. You’ll want to view the demos in a browser like Chrome or Edge until the feature is more widely supported in other browsers.
The initial configurationLet’s start with the HTML structure which is nothing but a nav element containing an unordered list of links:
<nav> <ul> <li><a href="#">Home</a></li> <li class="active"><a href="#">About</a></li> <li><a href="#">Projects</a></li> <li><a href="#">Blog</a></li> <li><a href="#">Contact</a></li> </ul> </nav>We will not spend too much time explaining this structure because it can be different if your use case is different. Simply ensure the semantic is relevant to what you are trying to do. As for the CSS part, we will start with some basic styling to create a horizontal menu navigation.
ul { padding: 0; margin: 0; list-style: none; display: flex; gap: .5rem; font-size: 2.2rem; } ul li a { color: #000; text-decoration: none; font-weight: 900; line-height: 1.5; padding-inline: .2em; display: block; }Nothing fancy so far. We remove some default styling and use Flexbox to align the elements horizontally.
CodePen Embed Fallback Sliding effectFirst off, let’s understand how the effect works. At first glance, it looks like we have one rectangle that shrinks to a small height, moves to the hovered element, and then grows to full height. That’s the visual effect, but in reality, more than one element is involved!
Here is the first demo where I am using different colors to better see what is happening.
CodePen Embed FallbackEach menu item has its own “element” that shrinks or grows. Then we have a common “element” (the one in red) that slides between the different menu items. The first effect is done using a background animation and the second one is where anchor positioning comes into play!
The background animationWe will animate the height of a CSS gradient for this first part:
/* 1 */ ul li { background: conic-gradient(lightblue 0 0) bottom/100% 0% no-repeat; transition: .2s; } /* 2 */ ul li:is(:hover,.active) { background-size: 100% 100%; transition: .2s .2s; } /* 3 */ ul:has(li:hover) li.active:not(:hover) { background-size: 100% 0%; transition: .2s; }We’ve defined a gradient with a 100% width and 0% height, placed at the bottom. The gradient syntax may look strange, but it’s the shortest one that allows me to have a single-color gradient.
Related: “How to correctly define a one-color gradient”
Then, if the menu item is hovered or has the .active class, we make the height equal to 100%. Note the use of the delay here to make sure the growing happens after the shrinking.
Finally, we need to handle a special case with the .active item. If we hover any item (that is not the active one), then the .active item gets the shirking effect (the gradient height is equal to 0%). That’s the purpose of the third selector in the code.
CodePen Embed FallbackOur first animation is done! Notice how the growing begins after the shrinking completes because of the delay we defined in the second selector.
The anchor positioning animationThe first animation was quite easy because each item had its own background animation, meaning we didn’t have to care about the text content since the background automatically fills the whole space.
We will use one element for the second animation that slides between all the menu items while adapting its width to fit the text of each item. This is where anchor positioning can help us.
Let’s start with the following code:
ul:before { content:""; position: absolute; position-anchor: --li; background: red; transition: .2s; } ul li:is(:hover, .active) { anchor-name: --li; } ul:has(li:hover) li.active:not(:hover) { anchor-name: none; }To avoid adding an extra element, I will prefer using a pseudo-element on the ul. It should be absolutely-positioned and we will rely on two properties to activate the anchor positioning.
We define the anchor with the anchor-name property. When a menu item is hovered or has the .active class, it becomes the anchor element. We also have to remove the anchor from the .active item if another item is in a hovered state (hence, the last selector in the code). In other words, only one anchor is defined at a time.
Then we use the position-anchor property to link the pseudo-element to the anchor. Notice how both use the same notation --li. It’s similar to how, for example, we define @keyframes with a specific name and later use it inside an animation property. Keep in mind that you have to use the <dashed-indent> syntax, meaning the name must always start with two dashes (--).
CodePen Embed FallbackThe pseudo-element is correctly placed but nothing is visible because we didn’t define any dimension! Let’s add the following code:
ul:before { bottom: anchor(bottom); left: anchor(left); right: anchor(right); height: .2em; }The height property is trivial but the anchor() is a newcomer. Here’s how Juan Diego describes it in the Almanac:
The CSS anchor() function takes an anchor element’s side and resolves to the <length> where it is positioned. It can only be used in inset properties (e.g. top, bottom, bottom, left, right, etc.), normally to place an absolute-positioned element relative to an anchor.
Let’s check the MDN page as well:
The anchor() CSS function can be used within an anchor-positioned element’s inset property values, returning a length value relative to the position of the edges of its associated anchor element.
Usually, we use left: 0 to place an absolute element at the left edge of its containing block (i.e., the nearest ancestor having position: relative). The left: anchor(left) will do the same but instead of the containing block, it will consider the associated anchor element.
That’s all — we are done! Hover the menu items in the below demo and see how the pseudo-element slides between them.
CodePen Embed FallbackEach time you hover over a menu item it becomes the new anchor for the pseudo-element (the ul:before). This also means that the anchor(...) values will change creating the sliding effect! Let’s not forget the use of the transition which is important otherwise, we will have an abrupt change.
We can also write the code differently like this:
ul:before { content:""; position: absolute; inset: auto anchor(right, --li) anchor(bottom, --li) anchor(left, --li); height: .2em; background: red; transition: .2s; }In other words, we can rely on the inset shorthand instead of using physical properties like left, right, and bottom, and instead of defining position-anchor, we can include the anchor’s name inside the anchor() function. We are repeating the same name three times which is probably not optimal here but in some situations, you may want your element to consider multiple anchors, and in such cases, this syntax will make sense.
Combining both effectsNow, we combine both effects and, tada, the illusion is perfect!
CodePen Embed FallbackPay attention to the transition values where the delay is important:
ul:before { transition: .2s .2s; } ul li { transition: .2s; } ul li:is(:hover,.active) { transition: .2s .4s; } ul:has(li:hover) li.active:not(:hover) { transition: .2s; }We have a sequence of three animations — shrink the height of the gradient, slide the pseudo-element, and grow the height of the gradient — so we need to have delays between them to pull everything together. That’s why for the sliding of the pseudo-element we have a delay equal to the duration of one animation (transition: .2 .2s) and for the growing part the delay is equal to twice the duration (transition: .2s .4s).
Bouncy effect? Why not?!Let’s try another fancy animation in which the highlight rectangle morphs into a small circle, jumps to the next item, and transforms back into a rectangle again!
CodePen Embed FallbackI won’t explain too much for this example as it’s your homework to dissect the code! I’ll offer a few hints so you can unpack what’s happening.
Like the previous effect, we have a combination of two animations. For the first one, I will use the pseudo-element of each menu item where I will adjust the dimension and the border-radius to simulate the morphing. For the second animation, I will use the ul pseudo-element to create a small circle that I move between the menu items.
Here is another version of the demo with different coloration and a slower transition to better visualize each animation:
CodePen Embed FallbackThe tricky part is the jumping effect where I am using a strange cubic-bezier() but I have a detailed article where I explain the technique in my CSS-Tricks article “Advanced CSS Animation Using cubic-bezier()”.
ConclusionI hope you enjoyed this little experimentation using the anchor positioning feature. We only looked at three properties/values but it’s enough to prepare you for this new feature. The anchor-name and position-anchor properties are the mandatory pieces for linking one element (often called a “target” element in this context) to another element (what we call an “anchor” element in this context). From there, you have the anchor() function to control the position.
Related: CSS Anchor Positioning Guide
Fancy Menu Navigation Using Anchor Positioning originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Web-Slinger.css: Across the Swiper-Verse
My previous article warned that horizontal motion on Tinder has irreversible consequences. I’ll save venting on that topic for a different blog, but at first glance, swipe-based navigation seems like it could be a job for Web-Slinger.css, your friendly neighborhood experimental pure CSS Wow.js replacement for one-way scroll-triggered animations. I haven’t managed to fit that description into a theme song yet, but I’m working on it.
In the meantime, can Web-Slinger.css swing a pure CSS Tinder-style swiping interaction to indicate liking or disliking an element? More importantly, will this experiment give me an excuse to use an image of Spider Pig, in response to popular demand in the bustling comments section of my previous article? Behold the Spider Pig swiper, which I propose as a replacement for captchas because every human with a pulse loves Spider Pig. With that unbiased statement in mind, swipe left or right below (only Chrome and Edge for now) to reveal a counter showing how many people share your stance on Spider Pig.
CodePen Embed Fallback Broaden your horizonsThe crackpot who invented Web-Slinger.css seems not to have considered horizontal scrolling, but we can patch that maniac’s monstrous creation like so:
[class^="scroll-trigger-"] { view-timeline-axis: x; }This overrides the default behavior for marker elements with class names using the Web-Slinger convention of scroll-trigger-n, which activates one-way, scroll-triggered animations. By setting the timeline axis to x, the scroll triggers only run when they are revealed by scrolling horizontally rather than vertically (which is the default). Otherwise, the triggers would run straightaway because although they are out of view due to the container’s width, they will all be above the fold vertically when we implement our swiper.
My steps in laying the foundation for the above demo were to fork this awesome JavaScript demo of Tinder-style swiping by Nikolay Talanov, strip out the JavaScript and all the cards except for one, then import Web-Slinger.css and introduce the horizontal patch explained above. Next, I changed the card’s container to position: fixed, and introduced three scroll-snapping boxes side-by-side, each the height and width of the viewport. I set the middle slide to scroll-align: center so that the user starts in the middle of the page and has the option to scroll backwards or forwards.
Sidenote: When unconventionally using scroll-driven animations like this, a good mindset is that the scrollable element needn’t be responsible for conventionally scrolling anything visible on the page. This approach is reminiscent of how the first thing you do when using checkbox hacks is hide the checkbox and make the label look like something else. We leverage the CSS-driven behaviors of a scrollable element, but we don’t need the default UI behavior.
I put a div marked with scroll-trigger-1 on the third slide and used it to activate a rejection animation on the card like this:
<div class="demo__card on-scroll-trigger-1 reject"> <!-- HTML for the card --> </div> <main> <div class="slide"> </div> <div id="middle" class="slide"> </div> <div class="slide"> <div class="scroll-trigger-1"></div> </div> </main>It worked the way I expected! I knew this would be easy! (Narrator: it isn’t, you’ll see why next.)
<div class="on-scroll-trigger-2 accept"> <div class="demo__card on-scroll-trigger-2 reject"> <!-- HTML for the card --> </div> </div> <main> <div class="slide"> <div class="scroll-trigger-2"></div> </div> <div id="middle" class="slide"> </div> <div class="slide"> <div class="scroll-trigger-1"></div> </div> </main>After adding this, Spider Pig is automatically ”liked” when the page loads. That would be appropriate for a card that shows a person like myself who everybody automatically likes — after all, a middle-aged guy who spends his days and nights hacking CSS is quite a catch. By contrast, it is possible Spider Pig isn’t everyone’s cup of tea. So, let’s understand why the swipe right implementation would behave differently than the swipe left implementation when we thought we applied the same principles to both implementations.
Take a step backThis bug drove home to me what view-timeline does and doesn’t do. The lunatic creator of Web-Slinger.css relied on tech that wasn’t made for animations which run only when the user scrolls backwards.
This visualizer shows that no matter what options you choose for animation-range, the subject wants to complete its animation after it has crossed the viewport in the scrolling direction — which is exactly what we do not want to happen in this particular case.
Fortunately, our friendly neighborhood Bramus from the Chrome Developer Team has a cool demo showing how to detect scroll direction in CSS. Using the clever --scroll-direction CSS custom property Bramus made, we can ensure Spider Pig animates at the right time rather than on load. The trick is to control the appearance of .scroll-trigger-2 using a style query like this:
:root { animation: adjust-slide-index 3s steps(3, end), adjust-pos 1s; animation-timeline: scroll(root x); } @property --slide-index { syntax: "<number>"; inherits: true; initial-value: 0; } @keyframes adjust-slide-index { to { --slide-index: 3; } } .scroll-trigger-2 { display: none; } @container style(--scroll-direction: -1) and style(--slide-index: 0) { .scroll-trigger-2 { display: block; } }That style query means that the marker with the .scroll-trigger-2 class will not be rendered until we are on the previous slide and reach it by scrolling backward. Notice that we also introduced another variable named --slide-index, which is controlled by a three-second scroll-driven animation with three steps. It counts the slide we are on, and it is used because we want the user to swipe decisively to activate the dislike animation. We don’t want just any slight breeze to trigger a dislike.
When the swipe has been concluded, one more like (I’m superhuman)As mentioned at the outset, measuring how many CSS-Tricks readers dislike Spider Pig versus how many have a soul is important. To capture this crucial stat, I’m using a third-party counter image as a background for the card underneath the Spider Pig card. It is third-party, but hopefully, it will always work because the website looks like it has survived since the dawn of the internet. I shouldn’t complain because the price is right. I chose the least 1990s-looking counter and used it like this:
@container style(--scroll-trigger-1: 1) { .result { background-image: url('https://counter6.optistats.ovh/private/freecounterstat.php?c=qbgw71kxx1stgsf5shmwrb2aflk5wecz'); background-repeat: no-repeat; background-attachment: fixed; background-position: center; } .counter-description::after { content: 'who like spider pig'; } .scroll-trigger-2 { display: none; } } @container style(--scroll-trigger-2: 1) { .result { background-image: url('https://counter6.optistats.ovh/private/freecounterstat.php?c=abtwsn99snah6wq42nhnsmbp6pxbrwtj'); background-repeat: no-repeat; background-attachment: fixed; background-position: center; } .counter-description::after { content: 'who dislike spider pig'; } .scroll-trigger-1 { display: none; } } Scrolls of wisdom: Lessons learnedThis hack turned out more complex than I expected, mostly because of the complexity of using scroll-triggered animations that only run when you meet an element by scrolling backward which goes against assumptions made by the current API. That’s a good thing to know and understand. Still, it’s amazing how much power is hidden in the current spec. We can style things based on extremely specific scrolling behaviors if we believe in ourselves. The current API had to be hacked to unlock that power, but I wish we could do something like:
[class^="scroll-trigger-"] { view-timeline-axis: x; view-timeline-direction: backwards; /* <-- this is speculative. do not use! */ }With an API like that allowing the swipe-right scroll trigger to behave the way I originally imagined, the Spider Pig swiper would not require hacking.
I dream of wider browser support for scroll-driven animations. But I hope to see the spec evolve to give us more flexibility to encourage designers to build nonlinear storytelling into the experiences they create. If not, once animation timelines land in more browsers, it might be time to make Web-Slinger.css more complete and production-ready, to make the more advanced scrolling use cases accessible to the average CSS user.
Web-Slinger.css: Across the Swiper-Verse originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
A Few Ways That Cloudways Makes Running This Site a Little Easier
(This is a sponsored post.)
It’s probably no surprise to you that CSS-Tricks is (proudly) hosted on Cloudways, DigitalOcean’s managed hosting arm. Given both CSS-Tricks and Cloudways are part of DigitalOcean, it was just a matter of time before we’d come together this way. And here we are!
We were previously hosted on Flywheel which was a fairly boutique WordPress hosting provider until WP Engine purchased it years back. And, to be very honest and up-front, Flywheel served us extremely well. There reached a point when it became pretty clear that CSS-Tricks was simply too big for Flywheel to scale along. That might’ve led us to try out WP Engine in the absence of Cloudways… but it’s probably good that never came to fruition considering recent events.
Anyway, moving hosts always means at least a smidge of contest-switching. Different server names with different configurations with different user accounts with different controls.
We’re a pretty low-maintenance operation around here, so being on a fully managed host is a benefit because I see very little of the day-to-day nuance that happens on our server. The Cloudways team took care of all the heavy lifting of migrating us and making sure we were set up with everything we needed, from SFTP accounts and database access to a staging environment and deployment points.
Our development flow used to go something like this:
- Fire up Local (Flywheel’s local development app)
- Futz around with local development
- Push to main
- Let a CI/CD pipeline publish the changes
I know, ridiculously simple. But it was also riddled with errors because we didn’t always want to publish changes on push. There was a real human margin of error in there, especially when handling WordPress updates. We could have (and should have) had some sort of staging environment rather than blindly trusting what was working locally. But again, we’re kinduva a ragtag team despite the big corporate backing.
The flow now looks like this:
- Fire up Local (we still use it!)
- Futz around with local development
- Push to main
- Publish to staging
- Publish to production
This is something we could have set up in Flywheel but was trivial with Cloudways. I gave up some automation for quality assurance’s sake. Switching environments in Cloudways is a single click and I like a little manual friction to feel like I have some control in the process. That might not scale well for large teams on an enterprise project, but that’s not really what Cloudways is all about — that’s why we have DigitalOcean!
See that baseline-status-widget branch in the dropdown? That’s a little feature I’m playing with (and will post about later). I like that GitHub is integrated directly into the Cloudways UI so I can experiment with it in whatever environment I want, even before merging it with either the staging or master branches. It makes testing a whole lot easier and way less error-prone than triggering auto-deployments in every which way.
Here’s another nicety: I get a good snapshot of the differences between my environments through Cloudways monitoring. For example, I was attempting to update our copy of the Gravity Forms plugin just this morning. It worked locally but triggered a fatal in staging. I went in and tried to sniff out what was up with the staging environment, so I headed to the Vulnerability Scanner and saw that staging was running an older version of WordPress compared to what was running locally and in production. (We don’t version control WordPress core, so that was an easy miss.)
I hypothesized that the newer version of Gravity Forms had a conflict with the older version of WordPress, and this made it ridiculously easy to test my assertion. Turns out that was correct and I was confident that pushing to production was safe and sound — which it was.
That little incident inspired me to share a little about what I’ve liked about Cloudways so far. You’ll notice that we don’t push our products too hard around here. Anytime you experience something delightful — whatever it is — is a good time to blog about it and this was clearly one of those times.
I’d be remiss if I didn’t mention that Cloudways is ideal for any size or type of WordPress site. It’s one of the few hosts that will let you BOYO cloud, so to speak, where you can hold your work on a cloud server (like a DigitalOcean droplet, for instance) and let Cloudways manage the hosting, giving you all the freedom to scale when needed on top of the benefits of having a managed host. So, if you need a fully managed, autoscaling hosting solution for WordPress like we do here at CSS-Tricks, Cloudways has you covered.
A Few Ways That Cloudways Makes Running This Site a Little Easier originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
How to Wait for the sibling-count() and sibling-index() Functions
New features don’t just pop up in CSS (but I wish they did). Rather, they go through an extensive process of discussions and considerations, defining, writing, prototyping, testing, shipping handling support, and many more verbs that I can’t even begin to imagine. That process is long, and despite how much I want to get my hands on a new feature, as an everyday developer, I can only wait.
I can, however, control how I wait: do I avoid all possible interfaces or demos that are possible with that one feature? Or do I push the boundaries of CSS and try to do them anyway?
As ambitious and curious developers, many of us choose the latter option. CSS would grow stagnant without that mentality. That’s why, today, I want to look at two upcoming functions: sibling-count() and sibling-index(). We’re waiting for them — and have been for several years — so I’m letting my natural curiosity get the best of me so I can get a feel for what to be excited about. Join me!
The tree-counting functionsAt some point, you’ve probably wanted to know the position of an element amongst its siblings or how many children an element has to calculate something in CSS, maybe for some staggering animation in which each element has a longer delay, or perhaps for changing an element’s background-color depending on its number of siblings. This has been a long-awaited deal on my CSS wishlists. Take this CSSWG GitHub Issue from 2017:
Feature request. It would be nice to be able to use the counter() function inside of calc() function. That would enable new possibilities on layouts.
However, counters work using strings, rendering them useless inside a calc() function that deals with numbers. We need a set of similar functions that return as integers the index of an element and the count of siblings. This doesn’t seem too much to ask. We can currently query an element by its tree position using the :nth-child() pseudo-selector (and its variants), not to mention query an element based on how many items it has using the :has() pseudo-selector.
Luckily, this year the CSSWG approved implementing the sibling-count() and sibling-index() functions! And we already have something in the spec written down:
The sibling-count() functional notation represents, as an <integer>, the total number of child elements in the parent of the element on which the notation is used.
The sibling-index() functional notation represents, as an <integer>, the index of the element on which the notation is used among the children of its parent. Like :nth-child(), sibling-index() is 1-indexed.
How much time do we have to wait to use them? Earlier this year Adam Argyle said that “a Chromium engineer mentioned wanting to do it, but we don’t have a flag to try it out with yet. I’ll share when we do!” So, while I am hopeful to get more news in 2025, we probably won’t see them shipped soon. In the meantime, let’s get to what we can do right now!
Rubbing two sticks togetherThe closest we can get to tree counting functions in terms of syntax and usage is with custom properties. However, the biggest problem is populating them with the correct index and count. The simplest and longest method is hardcoding each using only CSS: we can use the nth-child() selector to give each element its corresponding index:
li:nth-child(1) { --sibling-index: 1; } li:nth-child(2) { --sibling-index: 2; } li:nth-child(3) { --sibling-index: 3; } /* and so on... */Setting the sibling-count() equivalent has a bit more nuance since we will need to use quantity queries with the :has() selector. A quantity query has the following syntax:
.container:has(> :last-child:nth-child(m)) { }…where m is the number of elements we want to target. It works by checking if the last element of a container is also the nth element we are targeting; thus it has only that number of elements. You can create your custom quantity queries using this tool by Temani Afif. In this case, our quantity queries would look like the following:
ol:has(> :nth-child(1)) { --sibling-count: 1; } ol:has(> :last-child:nth-child(2)) { --sibling-count: 2; } ol:has(> :last-child:nth-child(3)) { --sibling-count: 3; } /* and so on... */This example is intentionally light on the number of elements for brevity, but as the list grows it will become unmanageable. Maybe we could use a preprocessor like Sass to write them for us, but we want to focus on a vanilla CSS solution here. For example, the following demo can support up to 12 elements, and you can already see how ugly it gets in the code.
CodePen Embed FallbackThat’s 24 rules to know the index and count of 12 elements for those of you keeping score. It surely feels like we could get that number down to something more manageable, but if we hardcode each index we are bound increase the amount of code we write. The best we can do is rewrite our CSS so we can nest the --sibling-index and --sibling-count properties together. Instead of writing each property by itself:
li:nth-child(2) { --sibling-index: 2; } ol:has(> :last-child:nth-child(2)) { --sibling-count: 2; }We could instead nest the --sibling-count rule inside the --sibling-index rule.
li:nth-child(2) { --sibling-index: 2; ol:has(> &:last-child) { --sibling-count: 2; } }While it may seem wacky to nest a parent inside its children, the following CSS code is completely valid; we are selecting the second li element, and inside, we are selecting an ol element if its second li element is also the last, so the list only has two elements. Which syntax is easier to manage? It’s up to you.
CodePen Embed FallbackBut that’s just a slight improvement. If we had, say, 100 elements we would still need to hardcode the --sibling-index and --sibling-count properties 100 times. Luckily, the following method will increase rules in a logarithmic way, specifically base-2. So instead of writing 100 rules for 100 elements, we will be writing closer to 10 rules for around 100 elements.
Flint and steelThis method was first described by Roman Komarov in October last year, in which he prototypes both tree counting functions and the future random() function. It’s an amazing post, so I strongly encourage you to read it.
This method also uses custom properties, but instead of hardcoding each one, we will be using two custom properties that will build up the --sibling-index property for each element. Just to be consistent with Roman’s post, we will call them --si1 and --si2, both starting at 0:
li { --si1: 0; --si2: 0; }The real --sibling-index will be constructed using both properties and a factor (F) that represents an integer greater or equal to 2 that tells us how many elements we can select according to the formula sqrt(F) - 1. So…
- For a factor of 2, we can select 3 elements.
- For a factor of 3, we can select 8 elements.
- For a factor of 5, we can select 24 elements.
- For a factor of 10, we can select 99 elements.
- For a factor of 25, we can select 624 elements.
As you can see, increasing the factor by one will give us exponential gains on how many elements we can select. But how does all this translate to CSS?
The first thing to know is that the formula for calculating the --sibling-index property is calc(F * var(--si2) + var(--si1)). If we take a factor of 3, it would look like the following:
li { --si1: 0; --si2: 0; /* factor of 3; it's a harcoded number */ --sibling-index: calc(3 * var(--si2) + var(--si1)); }The following selectors may be random but stay with me here. For the --si1 property, we will write rules selecting elements that are multiples of the factor and offset them by one 1 until we reach F - 1, then set --si1 to the offset. This translates to the following CSS:
li:nth-child(Fn + 1) { --si1: 1; } li:nth-child(Fn + 2) { --si1: 2; } /* ... */ li:nth-child(Fn+(F-1)) { --si1: (F-1) }So if our factor is 3, we will write the following rules until we reach F-1, so 2 rules:
li:nth-child(3n + 1) { --si1: 1; } li:nth-child(3n + 2) { --si1: 2; }For the --si2 property, we will write rules selecting elements in batches of the factor (so if our factor is 3, we will select 3 elements per rule), going from the last possible index (in this case 8) backward until we simply are unable to select more elements in batches. This is a little more convoluted to write in CSS:
li:nth-child(n + F*1):nth-child(-n + F*1-1){--si2: 1;} li:nth-child(n + F*2):nth-child(-n + F*2-1){--si2: 2;} /* ... */ li:nth-child(n+(F*(F-1))):nth-child(-n+(F*F-1)) { --si2: (F-1) }Again, if our factor is 3, we will write the following two rules:
li:nth-child(n + 3):nth-child(-n + 5) { --si2: 1; } li:nth-child(n + 6):nth-child(-n + 8) { --si2: 2; }And that’s it! By only setting those two values for --si1 and --si2 we can count up to 8 total elements. The math behind how it works seems wacky at first, but once you visually get it, it all clicks. I made this interactive demo in which you can see how all elements can be reached using this formula. Hover over the code snippets to see which elements can be selected, and click on each snippet to combine them into a possible index.
CodePen Embed FallbackIf you crank the elements and factor to the max, you can see that we can select 48 elements using only 14 snippets!
Wait, one thing is missing: the sibling-count() function. Luckily, we will be reusing all we have learned from prototyping --sibling-index. We will start with two custom properties: --sc1 and --sc1 at the container, both starting at 0 as well. The formula for calculating --sibling-count is the same.
ol { --sc1: 0; --sc2: 0; /* factor of 3; also a harcoded number */ --sibling-count: calc(3 * var(--sc2) + var(--sc1)); }Roman’s post also explains how to write selectors for the --sibling-count property by themselves, but we will use the :has() selection method from our first technique so we don’t have to write extra selectors. We can cram those --sc1 and --sc2 properties into the rules where we defined the sibling-index() properties:
/* --si1 and --sc1 */ li:nth-child(3n + 1) { --si1: 1; ol:has(> &:last-child) { --sc1: 1; } } li:nth-child(3n + 2) { --si1: 2; ol:has(> &:last-child) { --sc1: 2; } } /* --si2 and --sc2 */ li:nth-child(n + 3):nth-child(-n + 5) { --si2: 1; ol:has(> &:last-child) { --sc2: 1; } } li:nth-child(n + 6):nth-child(-n + 8) { --si2: 2; ol:has(> &:last-child) { --sc2: 2; } }This is using a factor of 3, so we can count up to eight elements with only four rules. The following example has a factor of 7, so we can count up to 48 elements with only 14 rules.
CodePen Embed FallbackThis method is great, but may not be the best fit for everyone due to the almost magical way of how it works, or simply because you don’t find it aesthetically pleasing. While for avid hands lighting a fire with flint and steel is a breeze, many won’t get their fire started.
Using a flamethrowerFor this method, we will use once again custom properties to mimic the tree counting functions, and what’s best, we will write less than 20 lines of code to count up to infinity—or I guess to 1.7976931348623157e+308, which is the double precision floating point limit!
We will be using the Mutation Observer API, so of course it takes JavaScript. I know that’s like admitting defeat for many, but I disagree. If the JavaScript method is simpler (which it is, by far, in this case), then it’s the most appropriate choice. Just as a side note, if performance is your main worry, stick to hard-coding each index in CSS or HTML.
First, we will grab our container from the DOM:
const elements = document.querySelector("ol");Then we’ll create a function that sets the --sibling-index property in each element and the --sibling-count in the container (it will be available to its children due to the cascade). For the --sibling-index, we have to loop through the elements.children, and we can get the --sibling-count from elements.children.length.
const updateCustomProperties = () => { let index = 1; for (element of elements.children) { element.style.setProperty("--sibling-index", index); index++; } elements.style.setProperty("--sibling-count", elements.children.length); };Once we have our function, remember to call it once so we have our initial tree counting properties:
updateCustomProperties();Lastly, the Mutation Observer. We need to initiate a new observer using the MutationObserver constructor. It takes a callback that gets invoked each time the elements change, so we write our updateCustomProperties function. With the resulting observer object, we can call its observe() method which takes two parameters:
- the element we want to observe, and
- a config object that defines what we want to observe through three boolean properties: attributes, childList, and subtree. In this case, we just want to check for changes in the child list, so we set that one to true:
That would be all we need! Using this method we can count many elements, in the following demo I set the max to 100, but it can easily reach tenfold:
CodePen Embed FallbackSo yeah, that’s our flamethrower right there. It definitely gets the fire started, but it’s plenty overkill for the vast majority of use cases. But that’s what we have while we wait for the perfect lighter.
More information and tutorials- Possible Future CSS: Tree-Counting Functions and Random Values (Roman Komarov)
- View Transitions Staggering (Chris Coyier)
- Element Indexes (Chris Coyier)
- Enable the use of counter() inside calc() #1026
- Proposal: add sibling-count() and sibling-index() #4559
- Extend sibling-index() and sibling-count() with a selector argument #9572
- Proposal: children-count() function #11068
- Proposal: descendant-count() function #11069
How to Wait for the sibling-count() and sibling-index() Functions originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Tight Mode: Why Browsers Produce Different Performance Results
I wrote a post for Smashing Magazine that was published today about this thing that Chrome and Safari have called “Tight Mode” and how it impacts page performance. I’d never heard the term until DebugBear’s Matt Zeunert mentioned it in a passing conversation, but it’s a not-so-new deal and yet there’s precious little documentation about it anywhere.
So, Matt shared a couple of resources with me and I used those to put some notes together that wound up becoming the article that was published. In short:
Tight Mode discriminates resources, taking anything and everything marked as High and Medium priority. Everything else is constrained and left on the outside, looking in until the body is firmly attached to the document, signaling that blocking scripts have been executed. It’s at that point that resources marked with Low priority are allowed in the door during the second phase of loading.
The implications are huge, as it means resources are not treated equally at face value. And yet the way Chrome and Safari approach it is wildly different, meaning the implications are wildly different depending on which browser is being evaluated. Firefox doesn’t enforce it, so we’re effectively looking at three distinct flavors of how resources are fetched and rendered on the page.
It’s no wonder web performance is a hard discipline when we have these moving targets. Sure, it’s great that we now have a consistent set of metrics for evaluating, diagnosing, and discussing performance in the form of Core Web Vitals — but those metrics will never be consistent from browser to browser when the way resources are accessed and prioritized varies.
Tight Mode: Why Browsers Produce Different Performance Results originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Publishing in the Generative AI Age
Hey Luke, why aren't you publishing new content? I am... but it's different in the age of generative AI. You don't see most of what I'm publishing these days and here's why.
The Ask Luke feature on this site uses the writings, videos, audio, and presentations I've published over the past 28 years to answer people's questions about digital product design. But since there's an endless amount of questions people could ask on this topic, I might not always have an answer. When this happens, the Ask Luke system basically tells people: "sorry I haven't written about this but here's some things I have written about." That's far from an ideal experience.
But just because I haven't taken the time to write an article or create a talk about a topic doesn't mean I don't have experiences or insights on it. Enter "saved questions". For any question Ask Luke wasn't able to answer, I can add information to answer it in the future in the form of a saved question. This admin feature allows the corpus of information Ask Luke uses to expand but it's invisible to people. Think of it as behind-the-scenes publishing.
Since launching the Ask Luke feature in April 2023, I've added close to 500 saved questions to my content corpus. That's a lot of publishing that doesn't show up as blog posts or articles but can be used to generate answers when needed.
Each of these new bits of content can also be weighted more or less. With more weight, answers to similar questions will lean more on that specific answer.
Without the extra weighting, saved questions are just another piece of content that can be used (or not) to answer similar questions. You can see the difference weighting makes by comparing these two replies to the same question. The first is weighted more heavily toward the saved question I added.
Using this process triggered a bunch of thoughts. Should I publish these saved questions as new articles on my blog or keep them behind the scenes? What level of polish do these types of content additions need? On one hand, I can simply talk fluidly, record it, and let the AI figure what to use. Even if it's messy, the machines will use what they deem relevant, so why bother? On the other hand, I can write, edit, and polish the answers so the overall content corpus is quality is consistently high. Currently I lean more toward the later. But should I?
Zooming up a level, any content someone publishes is out of date the moment it goes live. But generated content, like Ask Luke answers, are only produced when a specific person has a specific question. So the overall content corpus is more like a fully malleable singular entity vs. a bunch of discrete articles or files. Different parts of this corpus can be used when needed, or not at all. That's a different way of thinking about publishing (overall corpus vs. individual artifacts) with more implications than I touched on here.
The Importance of Investing in Soft Skills in the Age of AI
I’ll set out my stall and let you know I am still an AI skeptic. Heck, I still wrap “AI” in quotes a lot of the time I talk about it. I am, however, skeptical of the present, rather than the future. I wouldn’t say I’m positive or even excited about where AI is going, but there’s an inevitability that in development circles, it will be further engrained in our work.
We joke in the industry that the suggestions that AI gives us are more often than not, terrible, but that will only improve in time. A good basis for that theory is how fast generative AI has improved with image and video generation. Sure, generated images still have that “shrink-wrapped” look about them, and generated images of people have extra… um… limbs, but consider how much generated AI images have improved, even in the last 12 months.
There’s also the case that VC money is seemingly exclusively being invested in AI, industry-wide. Pair that with a continuously turbulent tech recruitment situation, with endless major layoffs and even a skeptic like myself can see the writing on the wall with how our jobs as developers are going to be affected.
The biggest risk factor I can foresee is that if your sole responsibility is to write code, your job is almost certainly at risk. I don’t think this is an imminent risk in a lot of cases, but as generative AI improves its code output — just like it has for images and video — it’s only a matter of time before it becomes a redundancy risk for actual human developers.
Do I think this is right? Absolutely not. Do I think it’s time to panic? Not yet, but I do see a lot of value in evolving your skillset beyond writing code. I especially see the value in improving your soft skills.
What are soft skills?A good way to think of soft skills is that they are life skills. Soft skills include:
- communicating with others,
- organizing yourself and others,
- making decisions, and
- adapting to difficult situations.
I believe so much in soft skills that I call them core skills and for the rest of this article, I’ll refer to them as core skills, to underline their importance.
The path to becoming a truly great developer is down to more than just coding. It comes down to how you approach everything else, like communication, giving and receiving feedback, finding a pragmatic solution, planning — and even thinking like a web developer.
I’ve been working with CSS for over 15 years at this point and a lot has changed in its capabilities. What hasn’t changed though, is the core skills — often called “soft skills” — that are required to push you to the next level. I’ve spent a large chunk of those 15 years as a consultant, helping organizations — both global corporations and small startups — write better CSS. In almost every single case, an improvement of the organization’s core skills was the overarching difference.
The main reason for this is a lot of the time, the organizations I worked with coded themselves into a corner. They’d done that because they just plowed through — Jira ticket after Jira ticket — rather than step back and question, “is our approach actually working?” By focusing on their team’s core skills, we were often — and very quickly — able to identify problem areas and come up with pragmatic solutions that were almost never development solutions. These solutions were instead:
- Improving communication and collaboration between design and development teams
- Reducing design “hand-off” and instead, making the web-based output the source of truth
- Moving slowly and methodically to move fast
- Putting a sharp focus on planning and collaboration between developers and designers, way in advance of production work being started
- Changing the mindset of “plow on” to taking a step back, thoroughly evaluating the problem, and then developing a collaborative and by proxy, much simpler solution
One thing AI cannot do — and (hopefully) never will be able to do — is be human. Core skills — especially communication skills — are very difficult for AI to recreate well because the way we communicate is uniquely human.
I’ve been doing this job a long time and something that’s certainly propelled my career is the fact I’ve always been versatile. Having a multifaceted skillset — like in my case, learning CSS and HTML to improve my design work — will only benefit you. It opens up other opportunities for you too, which is especially important with the way the tech industry currently is.
If you’re wondering how to get started on improving your core skills, I’ve got you. I produced a course called Complete CSS this year but it’s a slight rug-pull because it’s actually a core skills course that uses CSS as a context. You get to learn some iron-clad CSS skills alongside those core skills too, as a bonus. It’s definitely worth checking out if you are interested in developing your core skills, especially so if you receive a training budget from your employer.
Wrapping upThe main message I want to get across is developing your core skills is as important — if not more important — than keeping up to date with the latest CSS or JavaScript thing. It might be uncomfortable for you to do that, but trust me, being able to stand yourself out over AI is only going to be a good thing, and improving your core skills is a sure-fire way to do exactly that.
The Importance of Investing in Soft Skills in the Age of AI originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
10 Must-have Typefaces for 2025
Read the book, Typographic Firsts
Ten quality curated must-have font families for 2025.
The post 10 Must-have Typefaces for 2025 appeared first on I Love Typography.
Thank You (2024 Edition)
I’ll be honest: writing this post feels like a chore some years. Rounding up and reflecting on what’s happened throughout the year is somewhat obligatory for a site like this, especially when it’s a tradition that goes back as far as 2007. “Hey, look at all the cool things we did!”
This year is different. Much different. I’m more thankful this time around because, last year, I didn’t even get to write this post. At this time last year, I was a full-time student bent on earning a master’s degree while doing part-time contract work.
But now that I’m back, writing this feels so, so, so good. There’s a lot more gusto going into my writing when I say: thank you so very much! It’s because of you and your support for this site that I’m back at my regular job. I’d be remiss if I didn’t say that, so please accept my sincerest gratitude and appreciation. Thank you!
Let’s tie a bow on this year and round up what happened around here in 2024.
Overall trafficIs it worth saying anything about traffic? This site’s pageviews had been trending down since 2020 as it has for just about any blog about front-end dev, but it absolutely cratered when the site was on pause for over a year. Things began moving again in late May, but it was probably closer to mid-June when the engine fully turned over and we resumed regular publishing.
And, yes. With regular publishing came a fresh influx of pageviews. Funny how much difference it makes just turning on the lights.
All said and done, we had 26 million unique pageviews in 2024. That’s exactly what we had in 2023 as traffic went into a tailspin, so I call it a win that we stopped the bleeding and broke even this year.
PublishingA little bit of history when it comes to how many articles we publish each 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)
Going from 0 articles to 153 (including this one) in six months was no small task. I was the only writer on the team until about October. There are only three of us right now; even then, we’re all extremely part-time workers. Between us and 19 guest authors, I’d say that we outperformed expectations as far as quantity goes — but I’m even more proud of the effort and quality that goes into each one. It’s easy to imagine publishing upwards of 400 articles in 2025 if we maintain the momentum.
Case in point: we published a whopping three guides in six months:
That might not sound like a lot, so I’ll put it in context. We published just one guide in 2022 and our goal was to write three in all of 2021. We got three this year alone, and they’re all just plain great. I visit Juan’s Anchor Positioning guide as much as — if not more than — I do the ol’ Flexbox and Grid guides.
On top of that, we garnered 34 new additions to the CSS-Tricks Almanac! That includes all of the features for Anchor Positioning and View Transitions, as well as other new features like @starting-style. And the reason spent so much time in the Almanac is because we made some significant…
Site updatesThis is where the bulk of the year was spent, so let’s break things out into digestible chunks.
AlmanacWe refreshed the entire thing! It used to be just selectors and properties, but now we can write about everything from at-rules and functions to pseudos and everything in between. We still need a lot of help in there, so maybe consider guesting writing with us. 😉
Table of ContentsWe’ve been embedding anchor links to section headings in articles for several years, but it required using a WordPress block and it was fairly limiting as far as placement and customization. Now we generate those links automatically and include a conditional that allows us to toggle it on and off for specific articles. I’m working on an article about how it came together that we’ll publish after the holiday break.
NotesThere’s a new section where we take notes on what other people are writing about and share our takeaways with you. The motivation was to lower the barrier to writing more freely. Technical writing takes a lot of care and planning that’s at odds with openly learning and sharing. This way, we have a central spot where you can see what we’re learning and join us along the way — such as this set of notes I took from Bramus’ amazing free course on scroll-driven animations.
LinksThis is another area of the site that got a fresh coat of paint. Well, more than paint. It used to be that links were in the same stream as the rest of the articles, tutorials, and guides we publish. Links are meant to be snappy, sharable bits — conversation starters if you will. Breaking them out of the main feed into their own distinguished section helps reduce the noise on this site while giving links a brighter spotlight with a quicker path to get to the original article. Like when there’s a new resource for learning Anchor Positioning, we can shoot that out a lot more easily.
Quick HitsWe introduced another new piece of content in the form of brief one-liners that you might typically find us posting on Mastodon or Bluesky. We still post to those platforms but now we can write them here on the site and push them out when needed. There’s a lot more flexibility there, even if we haven’t given it a great deal of love just yet.
PicksThere’s a new feed of the articles we’re reading. It might seem a lot like Links, but the idea is that we can simply “star” something from our RSS reader and it’ll show up in the feed. They’re simply interesting articles that catch our attention that we want to spotlight and share, even if we don’t have any commentary to contribute. This was Chris’ brainchild a few years ago and it feels so good to bring it to fruition. I’ll write something up about it after the break, but you can already head over there.
Baseline StatusOoo, this one’s fun! I saw that the Chrome team put out a new web component for embedding web platform browser support information on a page so I set out to make it into a WordPress block we can use throughout the Almanac, which we’re already starting to roll out as content is published or refreshed (such as here in the anchor-name property). I’m still working on a write-up about it, but it’s I’ve already made it available in the WordPress Plugin Directory if you want to grab it for your WordPress site.
Or, here… I can simply drop it in and show you.
Post SliderThis was one of the first things I made when re-joining the team. We wanted to surface a greater number of articles on the homepage so that it’s easier to find specific types of content, whether it’s the latest five articles, the 10 most recently updated Almanac items or guides, classic CSS tricks from ages ago… that sort of thing. So, we got away from merely showing the 10 most recent articles and developed a series of post sliders that pull from different areas of the site. Converting our existing post slider component into a WordPress block made it more portable and a heckuva lot easier to update the homepage — and any other page or post where we might need a post slider. In fact, that’s another one I can demo for you right here…
Classic TricksTimeless CSS gems
Article on Oct 6, 2021 Scroll Animation Chris Coyier Article on Oct 6, 2021 Yellow Flash Chris Coyier Article on Oct 6, 2021 Self-Drawing Shapes Chris Coyier Article on Oct 6, 2021 Scroll Shadows Chris Coyier Article on May 20, 2020 Editable Style Blocks Chris Coyier Article on Oct 6, 2021 Scroll Indicator Chris Coyier Article on Mar 15, 2020 Border Triangles Chris Coyier Article on Oct 3, 2021 Pin Scrolling to Bottom Chris Coyier Article on Jul 5, 2021 Infinite Scrolling Background Image Chris CoyierSo, yeah. This year was heavier on development than many past years. But everything was done with the mindset of making content easier to find, publish, and share. I hope that this is like a little punch on the gas pedal that accelerates our ability to get fresh content out to you.
2025 GoalsI’m quite reluctant to articulate new goals when there are so many things still in flux, but the planner in me can’t help myself. If I can imagine a day at the end of next year when I’m reflecting on things exactly like this, I’d be happy, nay stoked, if I was able to say we did these things:
- Publish 1-2 new guides. We already have two in the works! That said, the bar for quality is set very high on these, so it’s still a journey to get from planning to publishing two stellar and chunky guides.
- Fill in the Almanac. My oh my, there is SO much work to do in this little corner of the site. 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.
- Restart the newsletter. This is something I’ve been itching to do. I know I miss reading the newsletter (especially when Robin was writing it) and this community feels so much smaller and quieter without it. The last issue went out in December 2022 and it’s high time we get it going again. The nuts and bolts are still in place. All we need is a little extra resourcing and the will to do it, and we’ve got at least half of that covered.
- More guest authors. I mentioned earlier that we’ve worked with 19 guest authors since June of this year. That’s great but also not nearly enough given that this site thrives on bringing in outside voices that we can all learn from. We were clearly busy with development and all kinds of other site updates but I’d like to re-emphasize our writing program this year, with the highest priority going into making it as smooth as possible to submit ideas, receive timely feedback on them, and get paid for what gets published. There’s a lot of invisible work that goes into that but it’s worth everyone’s while because it’s a win-win-win-win (authors win, readers win, CSS-Tricks wins, and DigitalOcean wins).
Thank you. That’s the most important thing I want to say. And special thanks to Juan Diego Rodriguez and Ryan Trimble. You may not know it, but they joined the team this Fall and have been so gosh-dang incredibly helpful. I wish every team had a Juan and Ryan just like I do — we’d all be better for it, that’s for sure. I know I learn a heckuva lot from them and I’m sure you will (or are!) as well.
Juan Diego Rodriguez
Ryan Trimble
Give them high-fives when you see them because they deserve it. ✋
Thank You (2024 Edition) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
A CSS Wishlist for 2025
2024 has been one of the greatest years for CSS: cross-document view transitions, scroll-driven animations, anchor positioning, animate to height: auto, and many others. It seems out of touch to ask, but what else do we want from CSS? Well, many things!
We put our heads together and came up with a few ideas… including several of yours.
Geoff’s wishlistI’m of the mind that we already have a BUNCH of wonderful CSS goodies these days. We have so many wonderful — and new! — things that I’m still wrapping my head around many of them.
But! There’s always room for one more good thing, right? Or maybe room for four new things. If I could ask for any new CSS features, these are the ones I’d go for.
1. A conditional if() statementIt’s coming! Or it’s already here if you consider that the CSS Working Group (CSSWG) resolved to add an if() conditional to the CSS Values Module Level 5 specification. That’s a big step forward, even if it takes a year or two (or more?!) to get a formal definition and make its way into browsers.
My understanding about if() is that it’s a key component for achieving Container Style Queries, which is what I ultimately want from this. Being able to apply styles conditionally based on the styles of another element is the white whale of CSS, so to speak. We can already style an element based on what other elements it :has() so this would expand that magic to include conditional styles as well.
2. CSS mixinsThis is more of a “nice-to-have” feature because I feel its squarely in CSS Preprocessor Territory and believe it’s nice to have some tooling for light abstractions, such as writing functions or mixins in CSS. But I certainly wouldn’t say “no” to having mixins baked right into CSS if someone was offering it to me. That might be the straw that breaks the CSS preprocessor back and allows me to write plain CSS 100% of the time because right now I tend to reach for Sass when I need a mixin or function.
I wrote up a bunch of notes about the mixins proposal and its initial draft in the specifications to give you an idea of why I’d want this feature.
3. // inline commentsYes, please! It’s a minor developer convenience that brings CSS up to par with writing comments in other languages. I’m pretty sure that writing JavaScript comments in my CSS should be in my list of dumbest CSS mistakes (even if I didn’t put it in there).
4. font-size: fitI just hate doing math, alright?! Sometimes I just want a word or short heading sized to the container it’s in. We can use things like clamp() for fluid typesetting, but again, that’s math I can’t be bothered with. You might think there’s a possible solution with Container Queries and using container query units for the font-size but that doesn’t work any better than viewport units.
Ryan’s wishlistI’m just a simple, small-town CSS developer, and I’m quite satisfied with all the new features coming to browsers over the past few years, what more could I ask for?
5. Anchor positioning in more browsers!I don’t need anymore convincing on CSS anchor positioning, I’m sold! After spending much of the month of November learning how it works, I went into December knowing I won’t really get to use it for a while.
As we close out 2024, only Chromium-based browsers have support, and fallbacks and progressive enhancements are not easy, unfortunately. There is a polyfill available (which is awesome), however, that does mean adding another chunk of JavaScript, contrasting what anchor positioning solves.
I’m patient though, I waited a long time for :has to come to browsers, which has been “newly available” in Baseline for a year now (can you believe it?).
6. Promoting elements to the #top-layer without popover?I like anchor positioning, I like popovers, and they go really well together!
The neat thing with popovers is how they appear in the #top-layer, so you get to avoid stacking issues related to z-index. This is probably all most would need with it, but having some other way to move an element there would be interesting. Also, now that I know that the #top-layer exists, I want to do more with it — I want to know what’s up there. What’s really going on?
Well, I probably should have started at the spec. As it turns out, the CSS Position Layout Module Level 4 draft talks about the #top-layer, what it’s useful for, and ways to approach styling elements contained within it. Interestingly, the #top-layer is controlled by the user agent and seems to be a byproduct of the Fullscreen API.
Dialogs and popovers are the way to go for now but, optimistically speaking, these features existing might mean it’s possible to promote elements to the #top-layer in future ways. This very well may be a coyote/roadrunner-type situation, as I’m not quite sure what I’d do with it once I get it.
7. Adding a layer attribute to <link> tagsPersonally speaking, Cascade Layers have changed how I write CSS. One thing I think would be ace is if we could include a layer attribute on a <link> tag. Imagine being able to include a CSS reset in your project like:
<link rel="stylesheet" href="https://cdn.com/some/reset.css" layer="reset">Or, depending on the page visited, dynamically add parts of CSS, blended into your cascade layers:
<!-- Global styles with layers defined, such as: @layer reset, typography, components, utilities; --> <link rel="stylesheet" href="/styles/main.css"> <!-- Add only to pages using card components --> <link rel="stylesheet" href="/components/card.css" layer="components">This feature was proposed over on the CSSWG’s repo, and like most things in life: it’s complicated.
Browsers are especially finicky with attributes they don’t know, plus definite concerns around handling fallbacks. The topic was also brought over to the W3C Technical Architecture Group (TAG) for discussion as well, so there’s still hope!
Juandi’s WishlistI must admit this, I wasn’t around when the web was wild and people had hit counters. In fact, I think I am pretty young compared to your average web connoisseur. While I do know how to make a layout using float (the first web course I picked up was pretty outdated), I didn’t have to suffer long before using things like Flexbox or CSS Grid and never grinded my teeth against IE and browser support.
So, the following wishes may seem like petty requests compared to the really necessary features the web needed in the past — or even some in the present. Regardless, here are my three petty requests I would wish to see in 2025:
8. Get the children count and index as an integerThis is one of those things that you swear it should already be possible with just CSS. The situation is the following: I find myself wanting to know the index of an element between its siblings or the total number of children. I can’t use the counter() function since sometimes I need an integer instead of a string. The current approach is either hardcoding an index on the HTML:
<ul> <li style="--index: 0">Milk</li> <li style="--index: 1">Eggs</li> <li style="--index: 2">Cheese</li> </ul>Or alternatively, write each index in CSS:
li:nth-child(1) { --index: 0; } li:nth-child(2) { --index: 1; } li:nth-child(3) { --index: 2; }Either way, I always leave with the feeling that it should be easier to reference this number; the browser already has this info, it’s just a matter of exposing it to authors. It would make prettier and cleaner code for staggering animations, or simply changing the styles based on the total count.
Luckily, there is a already proposal in Working Draft for sibling-count() and sibling-index() functions. While the syntax may change, I do hope to hear more about them in 2025.
ul > li { background-color: hsl(sibling-count() 50% 50%); } ul > li { transition-delay: calc(sibling-index() * 500ms); } 9. A way to balance flex-wrapI’m stealing this one from Adam Argyle, but I do wish for a better way to balance flex-wrap layouts. When elements wrap one by one as their container shrinks, they either are left alone with empty space (which I don’t dislike) or grow to fill it (which hurts my soul):
I wish for a more native way of balancing wrapping elements:
It’s definitely annoying.
10. An easier way to read/research CSSWG discussionsI am a big fan of the CSSWG and everything they do, so I spent a lot of time reading their working drafts, GitHub issues, or notes about their meetings. However, as much as I love jumping from link to link in their GitHub, it can be hard to find all the related issues to a specific discussion.
I think this raises the barrier of entry to giving your opinion on some topics. If you want to participate in an issue, you should have the big picture of all the discussion (what has been said, why some things don’t work, others to consider, etc) but it’s usually scattered across several issues or meetings. While issues can be lengthy, that isn’t the problem (I love reading them), but rather not knowing part of a discussion existed somewhere in the first place.
So, while it isn’t directly a CSS wish, I wish there was an easier way to get the full picture of the discussion before jumping in.
What’s on your wishlist?We asked! You answered! Here are a few choice selections from the crowd:
- Rotate direct background-images, like background-rotate: 180deg
- CSS random(), with params for range, spread, and type
- A CSS anchor position mode that allows targeting the mouse cursor, pointer, or touch point positions
- A string selector to query a certain word in a block of text and apply styling every time that word occurs
- A native .visually-hidden class.
- position: sticky with a :stuck pseudo
CSS-Tricks trajectory hasn’t been the most smooth these last years, so our biggest wish for 2025 is to keep writing and sparking discussions about the web. Happy 2025!
A CSS Wishlist for 2025 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
The Little Triangle in the Tooltip
Tooltips are like homemade food: everyone uses them and everyone has their own recipe to make them. If you don’t remember a particular recipe, you will search for one, follow it, and go on with your day. This “many ways to do the same thing” concept is general to web development and programming (and life!), but it’s something that especially rings true with tooltips. There isn’t a specialized way to make them — and at this point, it isn’t needed — so people come up with different ways to fill those gaps.
Today, I want to focus on just one step of the recipe, which due to lack of a better name, I’ll just call the little triangle in the tooltip. It’s one of those things that receives minimal attention (admittedly, I didn’t know much before writing this) but it amazes you how many ways there are to make them. Let’s start with the simplest and make our way up to the not-so-simple.
Ideally, the tooltip is just one element. We want to avoid polluting our markup just for that little triangle:
<span class="tooltip">I am a tooltip</span> Clever borderBefore running, we have to learn to walk. And before connecting that little triangle we have to learn to make a triangle. Maybe the most widespread recipe for a triangle is the border trick, one that can be found in Stack Overflow issues from 2010 or even here by Chris in 2016.
In a nutshell, borders meet each other at 45° angles, so if an element has a border but no width and height, the borders will make four perfect triangles. What’s left is to set three border colors to transparent and only one triangle will show! You can find an animated version on this CodePen by Chris Coyier
CodePen Embed FallbackUsually, our little triangle will be a pseudo-element of the tooltip, so we need to set its dimensions to 0px (which is something ::before and ::after already do) and only set one of the borders to a solid color. We can control the size of the triangle base by making the other borders wider, and the height by making the visible border larger.
.tooltip { &::before { content: ""; border-width: var(--triangle-base); border-style: solid; border-color: transparent; border-top: var(--triangle-height) solid red; } }Attaching the triangle to its tooltip is an art in itself, so I am going with the basics and setting the little triangle’s position to absolute and the .tooltip to relative, then playing with its inset properties to place it where we want. The only thing to notice is that we will have to translate the little triangle to account for its width, -50% if we are setting its position with the left property, and 50% if we are using right.
.tooltip { position: relative; &::before { /* ... */ position: absolute; top: var(--triangle-top); left: var(--triangle-left); transform: translateX(-50%); } }However, we could even use the new Anchor Positioning properties for the task. Whichever method you choose, we should now have that little triangle attached to the tooltip:
CodePen Embed Fallback Rotated squareOne drawback from that last example is that we are blocking the border property so that if we need it for something else, we are out of luck. However, there is another old-school method to make that little triangle: we rotate a square by 45° degrees and hide half of it behind the tooltip’s body. This way, only the corner shows in the shape of a triangle. We can make the square out of a pseudo-element:
.tooltip { &::before { content: ""; display: block; height: var(--triangle-size); width: var(--triangle-size); background-color: red; } }Then, position it behind the tooltip’s body. In this case, such that only one-half shows. Since the square is rotated, the transformation will be on both axes.
.tooltip { position: relative; &::before { /* ... */ position: absolute; top: 75%; left: 50%; z-index: -1; /* So it's behind the tooltip's body */ transform: translateX(-50%); transform: rotate(45deg) translateY(25%) translateX(-50%); } } CodePen Embed FallbackI also found that this method works better with Anchor Positioning since we don’t have to change the little triangle’s styles whenever we move it around. Unlike the border method, in which the visible border changes depending on the direction.
CodePen Embed Fallback Trimming the square with clip-pathAlthough I didn’t mention it before, you may have noticed some problems with that last approach. First off, it isn’t exactly a triangle, so it isn’t the most bulletproof take; if the tooltip is too short, the square could sneak out on the top, and moving the false triangle to the sides reveals its true square nature. We can solve both issues using the clip-path property.
The clip-path property allows us to select a region of an element to display while clipping the rest. It works by providing the path we want to trim through, and since we want a triangle out of a square, we can use the polygon() function. It takes points in the element and trims through them in straight lines. The points can be written as percentages from the origin (i.e., top-left corner), and in this case, we want to trim through three points 0% 0% (top-left corner), 100% 0% (top-right corner) and 50% 100% (bottom-center point).
So, the clip-path value would be the polygon() function with those three points in a comma-separated list:
.tooltip { &::before { content: ""; width: var(--triangle-base); height: var(--triangle-height); clip-path: polygon(0% 0%, 100% 0%, 50% 100%); transform: translate(-50%); background-color: red; } }This time, we will set the top and left properties using CSS variables, which will come in handy later.
.tooltip { position: relative; &::before { /* ... */ position: absolute; top: var(--triangle-top); /* 100% */ left: var(--triangle-left); /* 50% */ transform: translate(-50%); } }And now we should have a true little triangle attached to the tooltip:
CodePen Embed FallbackHowever, if we take the little triangle to the far end of any side, we can still see how it slips out of the tooltip’s body. Luckily, the clip-path property gives us better control of the triangle’s shape. In this case, we can change the points the trim goes through depending on the horizontal position of the little triangle. For the top-left corner, we want its horizontal value to approach 50% when the tooltip’s position approaches 0%, while the top-right corner should approach 50% when the tooltip position approaches 100%.
The following min() + max() combo does exactly that:
.tooltip { clip-path: polygon( max(50% - var(--triangle-left), 0%) 0, min(150% - var(--triangle-left), 100%) 0%, 50% 100% ); }The calc() function isn’t necessary inside math functions like min() and max().
Try to move the tooltip around and see how its shape changes depending on where it is on the horizontal axis:
CodePen Embed Fallback Using the border-image propertyIt may look like our last little triangle is the ultimate triangle. However, imagine a situation where you have already used both pseudo-elements and can’t spare one for the little triangle, or simply put, you want a more elegant way of doing it without any pseudo-elements. The task may seem impossible, but we can use two properties for the job: the already-seen clip-path and the border-image property.
Using the clip-path property, we could trim the shape of a tooltip — with the little triangle included! — directly out of the element. The problem is that the element’s background isn’t big enough to account for the little triangle. However, we can use the border-image property to make an overgrown background. The syntax is a bit complex, so I recommend reading this full dive into border-image by Temani Afif. In short, it allows us to use an image or CSS gradient as the border of an element. In this case, we are making a border as wide as the triangle height and with a solid color.
.tooltip { border-image: fill 0 // var(--triangle-height) conic-gradient(red 0 0);; }The trim this time will be a little more complex, since we will also trim the little triangle, so more points are needed. Exactly, the following seven points:
This translates to the following clip-path value:
.tooltip { /* ... */ clip-path: polygon( 0% 100%, 0% 0%, 100% 0%, 100% 100%, calc(50% + var(--triangle-base) / 2) 100%, 50% calc(100% + var(--triangle-height)), calc(50% - var(--triangle-base) / 2) 100% ); }We can turn it smart by also capping the little triangle bottom point whenever it gets past any side of the tooltip:
.tooltip { /* ... */ clip-path: polygon( 0% 100%, 0% 0%, 100% 0%, 100% 100%, min(var(--triangle-left) + var(--triangle-base) / 2, 100%) 100%, var(--triangle-left) calc(100% + var(--triangle-height)), max(var(--triangle-left) - var(--triangle-base) / 2, 0%) 100% ; }And now we have our final little triangle of the tooltip, one that is part of the main body and only uses one element!
CodePen Embed Fallback More information- The Complex But Awesome CSS border-image Property (Temani Afif)
- Transforming Borders into Cool CSS Triangles (Optimistic Web)
The Little Triangle in the Tooltip originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
What ELSE is on your CSS wishlist?
What else do we want or need CSS to do? It’s like being out late at night someplace you shouldn’t be and a stranger in a trenchcoat walks up and whispers in your ear.
“Psst. You wanna buy some async @imports? I’ve got the specificity you want.”
You know you shouldn’t entertain the idea but you do it anyway. All your friends doing Cascade Layers. What are you, a square?
I keep thinking of how amazing it is to write CSS today. There was an email exchange just this morning where I was discussing a bunch of ideas for a persistent set of controls in the UI that would have sounded bonkers even one year ago if it wasn’t for new features, like anchor positioning, scroll timelines, auto-height transitions, and popovers. We’re still in the early days of all these things — among many, many more — and have yet to see all the awesome possibilities come to fruition. Exciting times!
Chris kept a CSS wishlist, going back as far as 2013 and following up on it in 2019. We all have things we’d like to see CSS do and we always will no matter how many sparkly new features we get. Let’s revisit the ones from 2013:
- ✅ “I’d like to be able to select an element based on if it contains another particular selector.” Hello, :has()!
- ❌ “I’d like to be able to select an element based on the content it contains.”
- ❌ “I’d like multiple pseudo-elements.”
- ✅ “I’d like to be able to animate/transition something to height: auto;” Yep, we got that!
- 🟠 “I’d like things from Sass, like @extend, @mixin, and nesting.” We got the nesting part down with some progress on mixins.
- ❌ “I’d like ::nth-letter, ::nth-word, etc.”
- ✅ “I’d like all the major browsers to auto-update.” This one was already fulfilled.
So, about a score of 3.5 out of 7. It could very well be that some of these things fell out of favor at some point (haven’t heard any crying for a new pseudo-element since the first wishlist). Chris re-articulated the list this way:
- Parent queries. As in, selecting an element any-which-way, then selecting the parent of that element. We have some proof it’s possible with :focus-within.
- Container queries. Select a particular element when the element itself is under certain conditions.
- Standardized styling of form elements.
- Has/Contains Selectors.
- Transitions to auto dimensions.
- Fixed up handling of viewport units.
And we’ve got the vast majority of those under wraps! We have ways to query parents and containers. We’re exploring stylable selects and field-sizing. We know about :has() and we’re still going gaga over transitions to intrinsic sizes. We’ve openly opined whether there’s too much CSS (there isn’t).
But what else is on your CSS wishlist? Ironically enough, Adam Argyle went through this exercise just this morning and I love the way he’s broken things down into a user-facing wishlist and a developer-facing wishlist. I mean, geez, a CSS carousel? Yes, please! I love his list and all lists like it.
We’ll round things up and put a list together — so let us know!
What ELSE is on your CSS wishlist? originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Fluid Superscripts and Subscripts
Superscripts and subscripts are essential elements in academic and scientific content — from citation references to chemical formulas and mathematical expressions. Yet browsers handle these elements with a static approach that can create significant problems: elements become either too small on mobile devices or disproportionately large on desktop displays.
After years of wrestling with superscript and subscript scaling in CSS, I’m proposing a modern solution using fluid calculations. In this article, I’ll show you why the static approach falls short and how we can provide better typography across all viewports while maintaining accessibility. Best of all, this solution requires nothing but clean, pure CSS.
The problem with static scalingThe scaling issue is particularly evident when comparing professional typography with browser defaults. Take this example (adapted from Wikipedia), where the first “2” is professionally designed and included in the glyph set, while the second uses <sub> (top) and <sup> (bottom) elements:
Browsers have historically used font-size: smaller for <sup> and <sub> elements, which translates to roughly 0.83x scaling. While this made sense in the early days of CSS for simple documents, it can create problems in modern responsive designs where font sizes can vary dramatically. This is especially true when using fluid typography, where text sizes can scale smoothly between extremes.
Fluid scaling: A better solutionI’ve developed a solution that scales more naturally across different sizes by combining fixed and proportional units. This approach ensures legibility at small sizes while maintaining proper proportions at larger sizes, eliminating the need for context-specific adjustments.
CodePen Embed FallbackHere’s how it works:
sup, sub { font-size: calc(0.5em + 4px); vertical-align: baseline; position: relative; top: calc(-0.5 * 0.83 * 2 * (1em - 4px)); /* Simplified top: calc(-0.83em + 3.32px) */ } sub { top: calc(0.25 * 0.83 * 2 * (1em - 4px)); /* Simplified top: calc(0.42em - 1.66px) */ }- Natural scaling: The degressive formula ensures that superscripts and subscripts remain proportional at all sizes
- Baseline alignment: By using vertical-align: baseline and relative positioning, we prevent the elements from affecting line height and it gives us better control over the offset to match your specific needs. You’re probably also wondering where the heck these values come from — I’ll explain in the following.
Let’s look at how this works, piece by piece:
Calculating the font size (px)At small sizes, the fixed 4px component has more impact. At large sizes, the 0.5em proportion becomes dominant. The result is more natural scaling across all sizes.
sup, sub { font-size: calc(0.5em + 4px); /* ... */ } sub { /* ... */ } Calculating the parent font size (em)Within the <sup> and <sub> elements, we can calculate the parent’s font-size:
sup, sub { font-size: calc(0.5em + 4px); top: calc(2 * (1em - 4px)); } sub { top: calc(2 * (1em + 4px)); }The fluid font size is defined as calc(0.5em + 4px). To compensate for the 0.5em, we first need to solve 0.5em * x = 1em which gives us x = 2. The 1em here represents the font size of the <sup> and <sub> elements themselves. We subtract the 4px fixed component from our current em value before multiplying.
The vertical offsetFor the vertical offset, we start with default CSS positioning values and adjust them to work with our fluid scaling:
sup, sub { font-size: calc(0.5em + 4px); top: calc(-0.5 * 0.83 * 2 * (1em - 4px)); } sub { top: calc(0.25 * 0.83 * 2 * (1em - 4px)); }The formula is carefully calibrated to match standard browser positioning:
- 0.5em (super) and 0.25em (sub) are the default vertical offset values (e.g. used in frameworks like Tailwind CSS and Bootstrap).
- We multiply by 0.83 to account for the browser’s font-size: smaller scaling factor, which is used per default for superscript and subscript.
This approach ensures that our superscripts and subscripts maintain familiar vertical positions while benefiting from improved fluid scaling. The result matches what users expect from traditional browser rendering but scales more naturally across different font sizes.
Helpful tipsThe exact scaling factor font-size: (0.5em + 4px) is based on my analysis of superscript Unicode characters in common fonts. Feel free to adjust these values to match your specific design needs. Here are a few ways how you might want to customize this approach:
For larger scaling:
sup, sub { font-size: calc(0.6em + 3px); /* adjust offset calculations accordingly */ }For smaller scaling:
sup, sub { font-size: calc(0.4em + 5px); /* adjust offset calculations accordingly */ }For backward compatibility, you might want to wrap all of it in a @supports block:
@supports (font-size: calc(1em + 1px)) { sup, sub { ... } } Final demoI built this small interactive demo to show different fluid scaling options, compare them to the browser’s static scaling, and fine-tune the vertical positioning to see what works best for your use case:
CodePen Embed Fallback Open Live DemoGive it a try in your next project and happy to hear your thoughts!
Fluid Superscripts and Subscripts originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
CSSWG Minutes Telecon (2024-12-04): Just Use Grid vs. Display: Masonry
The CSS Working Group (CSSWG) meets weekly (or close to it) to discuss and quickly resolve issues from their GitHub that would otherwise be lost in the back-and-forth of forum conversation. While each meeting brings interesting conversation, this past Wednesday (December 4th) was special. The CSSWG met to try and finally squash a debate that has been going on for five years: whether Masonry should be a part of Grid or a separate system.
I’ll try to summarize the current state of the debate, but if you are looking for the long version, I recommend reading CSS Masonry & CSS Grid by Geoff and Choosing a Masonry Syntax in CSS by Miriam Suzanne.
In 2017, it was frequently asked whether Grid could handle masonry layouts; layouts where the columns (or the rows) could hold unevenly sized items without gaps in between. While this is just one of several possibilities with masonry, you can think about the layout popularized by Pinterest:
In 2020, Firefox released a prototype in which masonry was integrated into the CSS Grid layout module. The main voice against it was Rachel Andrew, arguing that it should be its own, separate thing. Since then, the debate has escalated with two proposals from Apple and Google, arguing for and against a grid-integrated syntax, respectively.
There were some technical worries against a grid-masonry implementation that were since resolved. What you have to know is this: right now, it’s a matter of syntax. To be specific, which syntax is
a. is easier to learn for authors and
b. how might this decision impact possible future developments in one or both models (or CSS in general).
In the middle, the W3C Technical Architecture Group (TAG) was asked for input on the issue which has prompted an effort to unify the two proposals. Both sides have brought strong arguments to the table over a series of posts, and in the following meeting, they were asked to lay those arguments once again in a presentation, with the hope of reaching a consensus.
Remember that you can subscribe and read the full minutes on W3C.org
The Battle of PowerPointsAlison Maher representing Microsoft and an advocate of implementing Masonry as a new display value, opened the meeting with a presentation. The main points were:
- Several properties behave differently between masonry and grid.
- Better defaults when setting display: masonry, something that Rachel Andrew recently argued for.
- There was an argument against display: masonry since fallbacks would be more lengthy to implement, whereas in a grid-integrated the fallback to grid is already there. Alison Maher refutes this since “needing one is a temporary problem, so [we] should focus on the future,” and that “authors should make explicit fallback, to avoid surprises.”
- “Positioning in masonry is simpler than grid, it’s only placed in 1 axis instead of 2.”
- Shorthands are also better: “Grid shorthand is complicated, hard to use. Masonry shorthand is easier because don’t need to remember the order.”
- “Placement works differently in grid vs masonry” and “alignment is also very different”
- There will be “other changes for submasonry/subgrid that will lead to divergences.”
- “Integrating masonry into grid will lead to spec bloat, will be harder to teach, and lead to developer confusion.”
alisonmaher: “Conclusion: masonry should be a separate display type”
Jen Simmons, representing the WebKit team and advocate of the “Just Use Grid” approach followed with another presentation. On this side, the main points were:
- Author learning could be skewed since “a new layout type creates a separate tool with separate syntax that’s similar but not the same as what exists […]. They’re familiar but not quite the same”
- The Chrome proposal would add around 10 new properties. “We don’t believe there’s a compelling argument to add so many new properties to CSS.”
- “Chromium argues that their new syntax is more understandable. We disagree, just use grid-auto-flow“
- “When you layout rows in grid, template syntax is a bit different — you stack the template names to physically diagram the names for the rows. Just Use Grid re-uses this syntax exactly; but new masonry layout uses the column syntax for rows”
- “Other difference is the auto-flow — grid’s indicates the primary fill direction, Chrome believes this doesn’t make sense and changed it to match the orientation of lines”
- “Chrome argues that new display type allows better defaults — but the defaults propose aren’t good […] it doesn’t quite work as easily as claimed [see article] requires deep understanding of autosizing”
- “Easier to switch, e.g. at breakpoints or progressive enhancement”
- “Follows CSS design principles to re-use what already exists”
After two presentations with compelling arguments, Lea Verou (also a member of the TAG) followed with their input.
lea: We did a TAG review on this. My opinion is fully reflected there. I think the arguments WebKit team makes are compelling. We thought not only should masonry be part of grid, but should go further. A lot of arguments for integrating is that “grid is too hard”. In that case we should make grid things easier. Complex things are possible, but simple things are not so easy.
Big part of Google’s argument is defaults, but we could just have smarter defaults — there is precedent for this in CSS if we decided that would help ergonomics We agree that switching between grid vs. masonry is common. Grid might be a slightly better fallback than nothing, but minor argument because people can use @supports. Introducing all these new properties increasing the API surfaces that authors need to learn. Less they can port over. Even if we say we will be disciplined, experience shows that we won’t. Even if not intentional, accidental. DRY – don’t have multiple sources of truth
One of arguments against masonry in grid is that grids are 2D, but actually in graphic design grids were often 1D. I agree that most masonry use cases need simpler grids than general grid use cases, but that means we should make those grids easier to define for both grid and masonry. The more we looked into this, we realize there are 3 different layout modes that give you 2D arrangement of children. We recommended not just make masonry part of grid, but find ways of integrating what we already have better could we come up with a shorthand that sets grid-auto-flow and flex-direction, and promote that for layout direction in general? Then authors only need to learn one control for it.
The debateAll was laid out onto the table, it was only left what other members had to say.
oriol: Problem with Jen Simmons’s reasoning. She said the proposed masonry-direction property would be new syntax that doesn’t match grid-auto-flow property, but this property matches flex-direction property so instead of trying to be close to grid, tries to be close to flexbox. Closer to grid is a choice, could be consistent with different things.
astearns: One question I asked is, has anyone changed their mind on which proposal they support? I personally have. I thought that separate display property made a lot more sense, in terms of designing the feature and I was very daunted by the idea that we’d have to consider both grid and masonry for any new development in either seemed sticky to me but the TAG argument convinced me that we should do the work of integrating these things.
TabAtkins: Thanks for setting that up for me, because I’m going to refute the TAG argument! I think they’re wrong in this case. You can draw a lot of surface-level connections between Grid and Masonry, and Flexbox, and other hypothetical layouts but when you actually look at details of how they work, behaviors each one is capable of, they’re pretty distinct if you try to combine together, it would be an unholy mess of conflicting constraints — e.g. flexing in items of masonry or grid or you’d have a weird mish-mash of, “the 2D layout.
But if you call it a flex you get access to these properties, call it grid, access to these other properties concrete example, “pillar” example mentioned in webKit blog post, that wasn’t compatible with the base concepts in masonry and flex because it wants a shared block formatting context grid etc have different formatting contexts, can’t use floats.
lea: actually, the TAG argument was that layout seems to actually be a continuum, and syntax should accommodate that rather than forcing one of two extremes (current flex vs current grid).
The debate kept back and forth until there was an attempt to set a general north star to follow.
jyasskin: Wanted to emphasize a couple aspects of TAG review. It seems really nice to keep the property from Chrome proposal that you don’t have to learn both, can just learn to do masonry without learning all of Grid even if that’s in a unified system perhaps still define masonry shorthand, and have it set grid properties
jensimmons: To create a simple masonry-style layout in Grid, you just need 3 lines of code (4 with a gap). It’s quite simple.
jyasskin: Most consensus part of TAG feedback was to share properties whenever possible. Not necessary to share the same ‘display’ values; could define different ‘display’ values but share the properties. One thing we didn’t like about unified proposal was grid-auto-flow in the unified proposal, where some values were ignored. Yeah, this is the usability point I’m pounding on
Another Split DecisionDespite all, it looked like nobody was giving away, and the debate seemed stuck once again:
astearns: I’m not hearing a way forward yet. At some point, one of the camps is going to have to concede in order to move this forward.
lea: What if we do a straw poll. Not to decide, but to figure out how far are we from consensus?
The votes were cast and the results were… split.
florian: though we could still not reach consensus, I want to thank both sides for presenting clear arguments, densely packed, well delivered. I will go back to the presentations, and revisit some points, it really was informative to present the way it was.
That’s all folks, a split decision! There isn’t a preference for either of the two proposals and implementing something with such mixed opinions is something nobody would approve. After a little over five years of debate, I think this meeting is yet another good sign that a new proposal addressing the concerns of both sides should be considered, but that’s just a personal opinion. To me, masonry (or whatever name it may be) is an important step in CSS layout that may shape future layouts, it shouldn’t be rushed so until then, I am more than happy to wait for a proposal that satisfies both sides.
Further Reading- Help us choose the final syntax for Masonry in CSS (Jen Simmons and Elika Etemad, with Brandon Stewart)
- Feedback needed: How should we define CSS masonry? (Rachel Andrew, Ian Kilpatrick and Tab Atkins-Bittner)
- Weighing in on CSS Masonry (Keith J. Grant)
- Masonry and good defaults (Rachel Andrew)
- Should masonry be part of CSS grid? (Ahmad Shadeed)
- Pinterest/Masonry style layout support #945
- Designer/developer feedback on masonry layout #10233
- Alternative masonry path forward #9041
- CSS Masonry Layout #1003
- Masonry Syntax Debate #11243
CSSWG Minutes Telecon (2024-12-04): Just Use Grid vs. Display: Masonry originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Smashing Meets Product Design
I had the pleasure of hosting a recent Smashing Magazine workshop on product design, subbing for Vitaly Friedman who usually runs these things.
What? A front-ender interviewing really smart people about their processes for user research, documenting requirements, and scaling teams around usability? I was a product designer once upon a time and even though it’s been a long time since I’ve flexed that muscle, it was a hoot learning from the guests, which included: Chris Kolb, Kevin Hawkins, and Vicky Carmichael.
The videos are barred from embedding, so I’ll simply link ’em up directly to YouTube:
- Chris Kolb: All Users Are Stupid
- Kevin Hawkins: Scaling International User Research
- Vicky Carmichael: Design By Doing
I also moderated a follow-up discussion with Chris and Kevin following the presentations.
A few of my choice takeaways:
- Small teams have the luxury of being in greater, more intimate contact with customers. Vicky explained how their relatively small size (~11 employees) means that everyone interfaces with customers and that customer issues and requests are handled more immediately.
- Large teams have to be mindful of teams forming into individual silos. A silo mentality typically happens when teams scale up in size, resulting in less frequent communication and collaboration. Team dashboards help, as do artifacts from meetings in multiple formats, such as AI-flavored summaries, video recordings, and documented decisions.
- Customers may appear to be dumb, but what looks like dumbness is often what happens when humans are faced with a lack of time and context. Solving “dumb” user problems often means coming at the problem in the same bewildered context rather than simply assuming the customer “just doesn’t get it.”
Smashing Meets Product Design originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Yet Another Anchor Positioning Quirk
I strongly believe Anchor Positioning will go down as one of the greatest additions to CSS. It may not be as game-changing as Flexbox or Grid, but it does fill a positioning gap that has been missing for decades. As awesome as I think it is, CSS Anchor Positioning has a lot of quirks, some of which are the product of its novelty and others due to its unique way of working. Today, I want to bring you yet another Anchor Positioning quirk that has bugged me since I first saw it.
The inceptionIt all started a month ago when I was reading about what other people have made using Anchor Positioning, specifically this post by Temani Afif about “Anchor Positioning & Scroll-Driven Animations.” I strongly encourage you to read it and find out what caught my eye there. Combining Anchor Positioning and Scroll-Driven Animation, he makes a range slider that changes colors while it progresses.
CodePen Embed FallbackAmazing by itself, but it’s interesting that he is using two target elements with the same anchor name, each attached to its corresponding anchor, just like magic. If this doesn’t seem as interesting as it looks, we should then briefly recap how Anchor Positioning works.
CSS Anchor Positioning and the anchor-scope propertySee our complete CSS Anchor Positioning Guide for a comprehensive deep dive.
Anchor Positioning brings two new concepts to CSS, an anchor element and a target element. The anchor is the element used as a reference for positioning other elements, hence the anchor name. While the target is an absolutely-positioned element placed relative to one or more anchors.
An anchor and a target can be almost every element, so you can think of them as just two div sitting next to each other:
<div class="anchor">anchor</div> <div class="target">target</div>To start, we first have to register the anchor element in CSS using the anchor-name property:
.anchor { anchor-name: --my-anchor; }And the position-anchor property on an absolutely-positioned element attaches it to an anchor of the same name. However, to move the target around the anchor we need the position-area property.
.target { position: absolute; position-anchor: --my-anchor; position-area: top right; } CodePen Embed FallbackThis works great, but things get complicated if we change our markup to include more anchors and targets:
<ul> <li> <div class="anchor">anchor 1</div> <div class="target">target 1</div> </li> <li> <div class="anchor">anchor 2</div> <div class="target">target 2</div> </li> <li> <div class="anchor">anchor 3</div> <div class="target">target 3</div> </li> </ul>Instead of each target attaching to its closest anchor, they all pile up at the last registered anchor in the DOM.
CodePen Embed FallbackThe anchor-scope property was introduced in Chrome 131 as an answer to this issue. It limits the scope of anchors to a subtree so that each target attaches correctly. However, I don’t want to focus on this property, because what initially caught my attention was that Temani didn’t use it. For some reason, they all attached correctly, again, like magic.
What’s happening?Targets usually attach to the last anchor on the DOM instead of their closest anchor, but in our first example, we saw two anchors with the same anchor-name and their corresponding targets attached. All this without the anchor-scope property. What’s happening?
Two words: Containing Block.
Something to know about Anchor Positioning is that it relies a lot on how an element’s containing block is built. This isn’t something inherently from Anchor Positioning but from absolute positioning. Absolute elements are positioned relative to their containing block, and inset properties like top: 0px, left: 30px or inset: 1rem are just moving an element around its containing block boundaries, creating what’s called the inset-modified containing block.
A target attached to an anchor isn’t any different, and what the position-area property does under the table is change the target’s inset-modified containing block so it is right next to the anchor.
Usually, the containing block of an absolutely-positioned element is the whole viewport, but it can be changed by any ancestor with a position other than static (usually relative). Temani takes advantage of this fact and creates a new containing block for each slider, so they can only be attached to their corresponding anchors. If you snoop around the code, you can find it at the beginning:
label { position: relative; /* No, It's not useless so don't remove it (or remove it and see what happens) */ }If we use this tactic on our previous examples, suddenly they are all correctly attached!
CodePen Embed Fallback Yet another quirkWe didn’t need to use the anchor-scope property to attach each anchor to its respective target, but instead took advantage of how the containing block of absolute elements is computed. However, there is yet another approach, one that doesn’t need any extra bits of code.
This occurred to me when I was also experimenting with Scroll-Driven Animations and Anchor Positioning and trying to attach text-bubble footnotes on the side of a post, like the following:
Logically, each footnote would be a target, but the choice of an anchor is a little more tricky. I initially thought that each paragraph would work as an anchor, but that would mean having more than one anchor with the same anchor-name. The result: all the targets would pile up at the last anchor:
CodePen Embed FallbackThis could be solved using our prior approach of creating a new containing block for each note. However, there is another route we can take, what I call the reductionist method. The problem comes when there is more than one anchor with the same anchor-name, so we will reduce the number of anchors to one, using an element that could work as the common anchor for all targets.
In this case, we just want to position each target on the sides of the post so we can use the entire body of the post as an anchor, and since each target is naturally aligned on the vertical axis, what’s left is to move them along the horizontal axis:
CodePen Embed FallbackYou can better check how it was done on the original post!
ConclusionThe anchor-scope may be the most recent CSS property to be shipped to a browser (so far, just in Chrome 131+), so we can’t expect its support to be something out of this world. And while I would love to use it every now and there, it will remain bound to short demos for a while. This isn’t a reason to limit the use of other Anchor Positioning properties, which are supported in Chrome 125 onwards (and let’s hope in other browsers in the near future), so I hope these little quirks can help you to keep using Anchor Positioning without any fear.
Yet Another Anchor Positioning Quirk originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
CSS Wrapped 2024
Join the Chrome DevRel team and a skateboarding Chrome Dino on a journey through the latest CSS launched for Chrome and the web platform in 2024, highlighting 17 new features
That breaks down (approximately) as:
Components Interactions Developer experiencePlus:
CSS Wrapped 2024 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Knowing CSS is Mastery to Frontend Development
Anselm Hannemann on the intersection between frameworks and learning the basics:
Nowadays people can write great React and TypeScript code. Most of the time a component library like MUI, Tailwind and others are used for styling. However, nearly no one is able to judge whether the CSS in the codebase is good or far from optimal. It is magically applied by our toolchain into the HTML and we struggle to understand why the website is getting slower and slower.
Related, from Alex Russell:
Many need help orienting themselves as to which end of the telescope is better for examining frontend problems. Frameworkism is now the dominant creed of frontend discourse. It insists that all user problems will be solved if teams just framework hard enough. This is non-sequitur, if not entirely backwards. In practice, the only thing that makes web experiences good is caring about the user experience — specifically, the experience of folks at the margins. Technologies come and go, but what always makes the difference is giving a toss about the user.
Knowing CSS is Mastery to Frontend Development originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
Steven Heller’s Font of the Month: Gigafly
Read the book, Typographic Firsts
This month, Steven Heller takes a closer look at the Gigafly font family.
The post Steven Heller’s Font of the Month: Gigafly appeared first on I Love Typography.
The Law of Diminishing Returns
Some animation can make things feel natural. Too many animations becomes distracting.
Some line spacing can help legibility. Too much hurts it.
Some alt text is contextual. Too much alt text is noise.
Some padding feels comfy. Too much padding feels exposed.
Some specificity is manageable. Too much specificity is untenable.
Some technical debt is healthy. Too much of it becomes a burden.
Some corner rounding is classy. Too much is just a circle.
Some breakpoints are fluid. Too many of them becomes adaptive.
Some margin adds breathing room. Too much margin collapses things.
Some images add context. Too many images takes a long time to download (and impacts the environment).
Some JavaScript enhances interactions. Too much becomes a bottleneck.
A font pairing creates a typographic system. Too many pairings creates a visual distraction.
Some utility classes come in handy. Too many eliminates a separation of concerns.
Some data helps make decisions. Too much data kills the vibe.
Some AI can help write the boring parts of code. Too much puts downward pressure on code quality.
Some SEO improves search ranking. Too much mutes the human voice.
Some testing provides good coverage. Too much testing requires its own maintenance.
A few colors establish a visual hierarchy. Too many establish a cognitive dissonance.
Some planning helps productivity. Too much planning creates delays.
Striking the right balance can be tough. We don’t want cool mama bear’s porridge or hot papa’s bear porridge, but something right in the middle, like baby bear’s porridge.
The Law of Diminishing Returns originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.