Css Tricks

Syndicate content CSS-Tricks
Tips, Tricks, and Techniques on using Cascading Style Sheets.
Updated: 8 hours 30 min ago

Creating a “Starred” Feed

Tue, 01/21/2025 - 4:21am

Chris wrote about “Likes” pages a long while back. The idea is rather simple: “Like” an item in your RSS reader and display it in a feed of other liked items. The little example Chris made is still really good.

CodePen Embed Fallback

There were two things Chris noted at the time. One was that he used a public CORS proxy that he wouldn’t use in a production environment. Good idea to nix that, security and all. The other was that he’d consider using WordPress transients to fetch and cache the data to work around CORS.

I decided to do that! The result is this WordPress block I can drop right in here. I’ll plop it in a <details> to keep things brief.

Open Starred Feed Link on 1/16/2025Don’t Wrap Figure in a Linkadrianroselli.com

In my post Brief Note on Figure and Figcaption Support I demonstrate how, when encountering a figure with a screen reader, you won’t hear everything announced at once:

No screen reader combo treats the caption as the accessible name nor accessible description, not even for an…Link on 1/15/2025Learning HTML is the best investment I ever didchristianheilmann.com

One of the running jokes and/or discussion I am sick and tired of is people belittling HTML. Yes, HTML is not a programming language. No, HTML should not just be a compilation target. Learning HTML is a solid investment and not hard to do.

I am not…Link on 1/14/2025Open Props UInerdy.dev

Presenting Open Props UI!…Link on 1/12/2025Gotchas in Naming CSS View Transitionsblog.jim-nielsen.com

I’m playing with making cross-document view transitions work on this blog.

Nothing fancy. Mostly copying how Dave Rupert does it on his site where you get a cross-fade animation on the whole page generally, and a little position animation on the page title specifically.

Link on 1/6/2025The :empty pseudo-classhtml-css-tip-of-the-week.netlify.app

We can use the :empty pseudo-class as a way to style elements on your webpage that are empty.

You might wonder why you’d want to style something that’s empty. Let’s say you’re creating a todo list.

You want to put your todo items in a list, but what about when you don’t…Link on 1/8/2025CSS Wish List 2025meyerweb.com

Back in 2023, I belatedly jumped on the bandwagon of people posting their CSS wish lists for the coming year.  This year I’m doing all that again, less belatedly! (I didn’t do it last year because I couldn’t even.  Get it?)

I started this post by looking at what I…Link on 1/9/2025aria-description Does Not Translateadrianroselli.com

It does, actually. In Firefox. Sometimes.

A major risk of using ARIA to define text content is it typically gets overlooked in translation. Automated translation services often do not capture it. Those who pay for localization services frequently miss content in ARIA attributes when sending text strings to localization vendors.

Content buried…Link on 1/8/2025Let’s Standardize Async CSS!scottjehl.com

6 years back I posted the Simplest Way to Load CSS Asynchronously to document a hack we’d been using for at least 6 years prior to that. The use case for this hack is to load CSS files asynchronously, something that HTML itself still does not support, even though…Link on 1/9/2025Tight Mode: Why Browsers Produce Different Performance Resultssmashingmagazine.com

This article is a sponsored by DebugBear

I was chatting with DebugBear’s Matt Zeunert and, in the process, he casually mentioned this thing called Tight Mode when describing how browsers fetch and prioritize resources. I wanted to nod along like I knew what he was talking about…Link on 12/19/2024Why I’m excited about text-box-trim as a designerpiccalil.li

I’ve been excited by the potential of text-box-trim, text-edge and text-box for a while. They’re in draft status at the moment, but when more browser support is available, this capability will open up some exciting possibilities for improving typesetting in the browser, as well as giving us more…

It’s a little different. For one, I’m only fetching 10 items at a time. We could push that to infinity but that comes with a performance tax, not to mention I have no way of organizing the items for them to be grouped and filtered. Maybe that’ll be a future enhancement!

The Chris demo provided the bones and it does most of the heavy lifting. The “tough” parts were square-pegging the thing into a WordPress block architecture and then getting transients going. This is my first time working with transients, so I thought I’d share the relevant code and pick it apart.

function fetch_and_store_data() { $transient_key = 'fetched_data'; $cached_data = get_transient($transient_key); if ($cached_data) { return new WP_REST_Response($cached_data, 200); } $response = wp_remote_get('https://feedbin.com/starred/a22c4101980b055d688e90512b083e8d.xml'); if (is_wp_error($response)) { return new WP_REST_Response('Error fetching data', 500); } $body = wp_remote_retrieve_body($response); $data = simplexml_load_string($body, 'SimpleXMLElement', LIBXML_NOCDATA); $json_data = json_encode($data); $array_data = json_decode($json_data, true); $items = []; foreach ($array_data['channel']['item'] as $item) { $items[] = [ 'title' => $item['title'], 'link' => $item['link'], 'pubDate' => $item['pubDate'], 'description' => $item['description'], ]; } set_transient($transient_key, $items, 12 * HOUR_IN_SECONDS); return new WP_REST_Response($items, 200); } add_action('rest_api_init', function () { register_rest_route('custom/v1', '/fetch-data', [ 'methods' => 'GET', 'callback' => 'fetch_and_store_data', ]); });

Could this be refactored and written more efficiently? All signs point to yes. But here’s how I grokked it:

function fetch_and_store_data() { }

The function’s name can be anything. Naming is hard. The first two variables:

$transient_key = 'fetched_data'; $cached_data = get_transient($transient_key);

The $transient_key is simply a name that identifies the transient when we set it and get it. In fact, the $cached_data is the getter so that part’s done. Check!

I only want the $cached_data if it exists, so there’s a check for that:

if ($cached_data) { return new WP_REST_Response($cached_data, 200); }

This also establishes a new response from the WordPress REST API, which is where the data is cached. Rather than pull the data directly from Feedbin, I’m pulling it and caching it in the REST API. This way, CORS is no longer an issue being that the starred items are now locally stored on my own domain. That’s where the wp_remote_get() function comes in to form that response from Feedbin as the origin:

$response = wp_remote_get('https://feedbin.com/starred/a22c4101980b055d688e90512b083e8d.xml');

Similarly, I decided to throw an error if there’s no $response. That means there’s no freshly $cached_data and that’s something I want to know right away.

if (is_wp_error($response)) { return new WP_REST_Response('Error fetching data', 500); }

The bulk of the work is merely parsing the XML data I get back from Feedbin to JSON. This scours the XML and loops through each item to get its title, link, publish date, and description:

$body = wp_remote_retrieve_body($response); $data = simplexml_load_string($body, 'SimpleXMLElement', LIBXML_NOCDATA); $json_data = json_encode($data); $array_data = json_decode($json_data, true); $items = []; foreach ($array_data['channel']['item'] as $item) { $items[] = [ 'title' => $item['title'], 'link' => $item['link'], 'pubDate' => $item['pubDate'], 'description' => $item['description'], ]; }

“Description” is a loaded term. It could be the full body of a post or an excerpt — we don’t know until we get it! So, I’m splicing and trimming it in the block’s Edit component to stub it at no more than 50 words. There’s a little risk there because I’m rendering the HTML I get back from the API. Security, yes. But there’s also the chance I render an open tag without its closing counterpart, muffing up my layout. I know there are libraries to address that but I’m keeping things simple for now.

Now it’s time to set the transient once things have been fetched and parsed:

set_transient($transient_key, $items, 12 * HOUR_IN_SECONDS);

The WordPress docs are great at explaining the set_transient() function. It takes three arguments, the first being the $transient_key that was named earlier to identify which transient is getting set. The other two:

  • $value: This is the object we’re storing in the named transient. That’s the $items object handling all the parsing.
  • $expiration: How long should this transient last? It wouldn’t be transient if it lingered around forever, so we set an amount of time expressed in seconds. Mine lingers for 12 hours before it expires and then updates the next time a visitor hits the page.

OK, time to return the items from the REST API as a new response:

return new WP_REST_Response($items, 200);

That’s it! Well, at least for setting and getting the transient. The next thing I realized I needed was a custom REST API endpoint to call the data. I really had to lean on the WordPress docs to get this going:

add_action('rest_api_init', function () { register_rest_route('custom/v1', '/fetch-data', [ 'methods' => 'GET', 'callback' => 'fetch_and_store_data', ]); });

That’s where I struggled most and felt like this all took wayyyyy too much time. Well, that and sparring with the block itself. I find it super hard to get the front and back end components to sync up and, honestly, a lot of that code looks super redundant if you were to scope it out. That’s another story altogether.

Enjoy reading what we’re reading! I put a page together that pulls in the 10 most recent items with a link to subscribe to the full feed.

Creating a “Starred” Feed originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Fancy Menu Navigation Using Anchor Positioning

Fri, 01/17/2025 - 4:57am

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 Fallback

Cool, 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 configuration

Let’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 effect

First 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 Fallback

Each 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 animation

We 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 Fallback

Our 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 animation

The 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 Fallback

The 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 Fallback

Each 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 effects

Now, we combine both effects and, tada, the illusion is perfect!

CodePen Embed Fallback

Pay 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 Fallback

I 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 Fallback

The 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()”.

Conclusion

I 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

Wed, 01/15/2025 - 5:03am

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 horizons

The 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 back

This 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 learned

This 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

Tue, 01/14/2025 - 4:49am

(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

Mon, 01/13/2025 - 5:08am

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 functions

At 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 together

The 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 Fallback

That’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 Fallback

But 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 steel

This 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 Fallback

If 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 Fallback

This 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 flamethrower

For 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:

  1. the element we want to observe, and
  2. 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:
const observer = new MutationObserver(updateCustomProperties); const config = {attributes: false, childList: true, subtree: false}; observer.observe(elements, config);

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 Fallback

So 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.

What to do next?

I don’t have a time machine, so I can’t say when the sibling-index() and sibling-count() functions may be released. However, the CSSWG has already written down something in the spec, and browsers (mainly Chromium) intention to ship things has been on fire lately, so I am confident we will be seeing both functions soon!

Luckily, once they are shipped and support is acceptable, all you have to do to change from one of these methods is set those custom properties to their functional homonyms. That is, of course, if you don’t want to go around your CSS changing each custom property:

ol{ --sibling-count: sibling-count(); } li{ --sibling-index: sibling-index(); } More information and tutorials Related Issues

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

Thu, 01/09/2025 - 6:16am

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.

©2003 - Present Akamai Design & Development.