10 Free Resources for Web Designers, January 2025
Looking for a tool to make web designing easier? This list is packed with freebies, from tools to themes to fonts, you’ll find something you can use here.
Looking for a tool to make web designing easier? This list is packed with freebies, from tools to themes to fonts, you’ll find something you can use here.
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.
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.
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.
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;
}
}
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.
Users’ feedback is shaped by biases, emotions, and familiarity with the status quo. Great UX design requires balancing empathy with vision, using user insights as a guide—not the rulebook.
Adobe has introduced live co-editing in Photoshop, now available in private beta, allowing multiple users to collaborate on the same document in real time. This innovative feature streamlines workflows, eliminates versioning issues, and fosters seamless creative teamwork across the globe.
What you can incorporate in a website design can have a definite impact on its performance and by extension on your business. What you are actually able to incorporate into that same website design depends heavily on the WordPress theme you choose.
When you are in the process of selecting a WordPress theme you quite naturally become familiar with all the design aids, tools, and other features a given theme has to offer. The more features a theme has, the more it should be able to do for you. But will that be enough?
At some point, you need to address two key questions. First, does the theme allow you to create a website that makes a strong first impression? Second, will it ensure your website is future-ready? For the latter, it’s essential to choose a theme that is continuously improved and updated.
Our experts have selected these 9 incredible WordPress themes for 2025 for your consideration. Each one is fully capable of helping you build a website that not only attracts visitors but is also future-ready.
Short Description: A true powerhouse for WordPress.
Click the video to explore UiCore Pro’s most downloaded template.
UiCore Pro invites you to discover its expanding collection of ready-made websites, perfect for launching your next design project. Any ready-made website can be accessed in one-click, plus you can mix-and-match sections, and adapt the results to your branding in no time.
Slate is an outstanding example of a UiCore Pro ready-made website. Slate provides the ideal foundation for setting up a starter company’s one-page informative website. Slate ranks among UiCore Pro’s top 10 most downloaded demos in 2024.
Other UiCore Pro features you’ll fall in love with include:
Key UiCore Pro clients include Startups, Online Shop owners, SaaS providers, Agencies, and Architects.
Updates & New Content Releases: New demos and pre-built websites are added every month to the 60+ pre-built website library.
Product Rating: 4.6/5
Readily Accessible support materials: YouTube videos, Support manual. Our response time for user assistance requests is only a few hours.
Short Description: With its hundreds of WordPress pre-built sites and handful of builders Betheme gives you everything you need to design lightning-fast, feature-rich websites, and shops with ease.
Click the video to explore Betheme’s most downloaded template.
A Betheme prebuilt website allows you to have a fully loaded website up and running in less than a minute. With just a few hours of customization, your website will be ready to launch. That’s even true for more complex websites, as long as you have your content readily available.
A long-standing favorite among users is Betheme’s library of 700+ pre-built websites, all accessible with just a single click. Betheme’s Be Business pre-built website ranked among the top 10 downloaded demos of 2024 for a good reason. It offers an excellent design approach for advertising a new business, particularly a service-oriented one.
Additional features include a wealth of super-fast builders like:
Include multiple customizable layouts for your portfolio, blog, and shop pages and you are still at the tip of the iceberg.
New pre-built websites and demos are released monthly.
Readily Accessible support materials: Support documentation along with an extensive library of Video Tutorials.
Short Description: Blocksy is responsive, WooCommerce ready, SEO optimized and has many customization options.
Click the video to explore Blocksy’s most downloaded template.
Blocksy’s remarkable feature is a Header Builder together with accompanying header elements you can use to design a header designed precisely to your specifications.
Additional standout features include:
Have you ever experienced “designer’s block” trying to start a new project? The e-Bike Starter Shop template might be just what you need to get started, whether your business involves e-bikes or something totally different. There are many other popular sites worth exploring, and new pre-built websites are added every month.
Readily Accessible support materials: The average support ticket resolution time is under 24 hours.
Product Rating: 4.97/5 on WordPress.org
User Review: I’m using Blocksy with the premium extension since December 2020 and so far the experience has been really positive. Found some bugs, but they were successfully squashed by the extremely helpful support team. How they keep adding helpful features and extensions is great and definitely not standard in the WordPress world.
Short Description: Litho WordPress theme is an all-around versatile and robust choice for those who want to create a professional website.
Click the video to explore Litho’s most downloaded template.
Litho is a creative multi-purpose Elementor WordPress theme you can use to create any type of business niche website, blog, plus portfolio, and eCommerce sites.
Litho features 300+ single-click importable templates you can customize to fit your specific needs. For example, Litho’s Journey to The Self through Meditation template features a straightforward approach with some clever special effects. In short, as a basis for website design it opens up all sorts of possibilities.
Additional features of Litho include:
Readily Accessible support materials: The average response time for support tickets is less than 24 hours. Immediate assistance is available through online documentation, a quick start guide, and comprehensive installation and update guidelines.
User Review: Very good response time and help in resolving situations. We are very satisfied. Good customer support.
Short Description: Uncode is designed with pixel-perfect attention to all details.
Click the video to explore Uncode’s most downloaded template.
Uncode is one of the top-selling creative themes of all time. People keep returning to use Uncode for more projects because of the results they can achieve.
A host of other design aids are at the ready, but it’s the demos that serve as the foundation and inspiration, ensuring projects get off to a successful start. Uncode’s Shop Bold demo, featuring clever hover effects, ranks among the 10 most downloaded demos for 2024.
Check out several of these demos to get an even better picture of the ideas and inspirations that await you. New demos and pre-built websites are released every 3 to 6 months.
Uncode’s other standout features include –
Free and reliable updates are consistently released to meet customer needs and preferences.
Product Rating: 4.89/5
Readily Accessible support materials: Narrated video tutorials are available, along with a dedicated support group on Facebook. Uncode also offers customer support with ticket resolutions typically under 24 hours.
User Review: I don’t know what is better. The theme or the support. The theme is very nice and the support is rapid fast! Well done!
Short Description: Avada is a versatile and powerful website builder for WordPress and WooCommerce that works for beginners, marketers, and professionals.
Click the video to explore Avada’s most downloaded template.
Avada is fully responsive, SEO and speed-optimized, WooCommerce-ready, and an excellent choice for both first-time web designers and seasoned developers managing multiple clients. It has been called the Swiss Army Knife of WordPress themes.
Whether you’re creating a simple one-page business website or a bustling online marketplace, Avada provides a wealth of powerful tools and plugins for building websites that include –
With over 750,000 satisfied clients, Avada has earned its place as an all-time best-seller among WordPress themes.
Updates & New Content Releases: Free lifetime updates.
Product Rating: 4.77 /5
Readily Accessible support materials: Comprehensive documentation, an extensive video library, and exceptional assistance from Avada’s Customer Support team.
User Review: Has many options to adjust post information! I like it a lot!
Short Description: Total is a WordPress theme optimized for WPBakery, offering exclusive features, unique elements, and enhancements you won’t find anywhere else.
Click the video to explore Total’s most downloaded template.
With its unmatched flexibility and efficient, time-saving features, Total can handle any website design project you throw at it.
For example:
Total’s demos provide an excellent starting point for your projects, offering a quick launch option while also serving as valuable sources of inspiration. Total’s Bryce demo provides a strong foundation for a one-page introductory site to a startup business.
Product Rating: 4.86/5
User Review: The Total Theme is really great. Very flexible to use, and the author, AJ, has gone to great pains to integrate his other plugins to work seamlessly with Total and also to open additional features when used together. It is a really great ecosystem that is being built. The support for this theme is, in a word, unbelievably-incredibly-awesome.
Short Description: Begin with one of WoodMart’s pre-built websites and customize it to your needs using the powerful Elementor and WPBakery plugins.
Click the video to explore Woodmart’s most downloaded template.
Whether you’re building a small store, a high-volume shop, or a multivendor marketplace, size doesn’t matter – WoodMart provides everything you need for success.
Woodmart demos can be used to design websites for any type or niche, but Woodmart offers much more than just shops, product sales, and furniture. Woodmart’s Pottery demo for a pottery workshop or class can be customized to fit a variety of uses. But that is what you can expect from Woodmart, even though you need to provide your own content!
Updates & New Content Releases: New pre-built websites and demos are released monthly.
Short Description: Pro Theme with its powerful Cornerstone feature gives you website building at its very best.
Click the video to explore Pro Theme’s most downloaded template.
Pro Theme’s key feature is its continuous stream of updates and new features, offering more than you’ll find with any other WordPress theme. In 2024 alone, Pro Theme released 27 updates, including –
When it comes to custom-designed websites, are you planning to open or advertise a Spa? Perhaps not, but this short but sweet Pro Theme demo gives you a taste of how easy it might be to customize it to perfectly fit your own planned business niche.
Updates & New Content Releases: New updates are released every two weeks.
Readily Accessible support materials: a forum, a support manual, and collection of YouTube tutorial videos.
********
It’s almost guaranteed that any of these 9 incredible WordPress themes has all the tools you need to create a stunning, future-proof website. However, we recommend taking the time to explore each option to find the one that feels most intuitive to use or helps you achieve your goals with greater ease.
Rather than listing every possible feature you might need, we’ve concentrated on highlighting the key points in the theme summaries. When you explore specific themes, you’ll find it simple to gain a clear understanding of how they can meet your needs.
WordPress Theme | Quick Overview |
UiCore PRO | Intuitive interface with premium design packs |
Betheme | Integrated tools for SEO optimization |
Blocksy | Best for visually stunning WooCommerce sites |
Uncode | Dynamic theme with creative features |
Avada | Comprehensive tools for website customization |
Litho | Tailored for photographers and designers |
Woodmart | Offers multi-language support out of the box |
Pro Theme + Cornerstone Builder | Enhanced for marketing-focused designs |
Total Theme | Flexible layouts with drag-and-drop editor |
(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:
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:
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.
Google’s Willow quantum computer presents both revolutionary opportunities and significant risks, particularly by threatening to disrupt systems that secure modern digital infrastructure. Designers must urgently rethink how to build future-proof systems, balancing innovation with the challenges of quantum-level security threats.
Walmart has introduced a modernized visual identity that balances its rich heritage with innovative updates, including a refined logo, accessible designs, and sustainable practices.
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!
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 ofcalc()
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, 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, 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!
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.
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.
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.
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…
2
, we can select 3
elements.3
, we can select 8
elements.5
, we can select 24
elements.10
, we can select 99
elements.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.
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.
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.
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:
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:
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.
counter()
inside calc()
#1026sibling-count()
and sibling-index()
#4559sibling-index()
and sibling-count()
with a selector argument #9572children-count()
function #11068descendant-count()
function #11069How 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.
Welcome to our first tools round up of 2025. It’s the first Monday of a new year, and many of you are back at work after a much needed break. Let us ease you back in with our pick of the latest tools to help you get stuff done faster, smarter, and with a little […]