CSS got some handy new functions recently: sibling-index() and sibling-count(). sibling-index() gives us a number based on a child element’s position relative to its siblings, starting at 1. For example, the third child in a list of 10 would have an index of 3. sibling-count() gives us the total number of siblings within a parent element. We can leverage these functions for more brevity in our CSS.
The original code
Our case studies page has an animated marquee of company logos — we derived that code from this tutorial on frontend.fyi. The popular approach to this pattern is to repeat the HTML a few times to create a seamless loop, but we wanted to avoid bloating the content with a leaner CSS-only approach.
Our HTML is fairly straightforward with container that controls the overflow and then a list with list items and an image for each logo.
<div class="horizontally-scrolling-logos">
<ul class="horizontally-scrolling-logos__list">
<li class="horizontally-scrolling-logos__item">
<img
src="images/vimeo.png"
alt="vimeo logo"
class="horizontally-scrolling-logos__image"
loading="lazy"
>
</li>
<li class="horizontally-scrolling-logos__item">
<img
src="images/merck.png"
alt="Merck logo"
class="horizontally-scrolling-logos__image"
loading="lazy"
>
</li>
<li class="horizontally-scrolling-logos__item">
<img
src="images/planned-parentood.png"
alt="Planned Parenthood logo"
class="horizontally-scrolling-logos__image"
loading="lazy"
>
</li>
<li class="horizontally-scrolling-logos__item">
<img
src="images/hbr.png"
alt="Harvard Business Review logo"
class="horizontally-scrolling-logos__image"
loading="lazy"
>
</li>
<li class="horizontally-scrolling-logos__item">
<img
src="images/moma.png"
alt="MOMA logo"
class="horizontally-scrolling-logos__image"
loading="lazy"
>
</li>
</ul>
</div>
The CSS leverages a lot of custom properties to note the animation speed, the number of logos in the list, etc. All of these go in to calculate the track width and position of each logo in the marquee.
.horizontally-scrolling-logos {
--spacing--medium: 1.5rem;
--speed: 25s;
--numItems: 5;
--single-slide-speed: calc(var(--speed) / var(--numItems));
--item-width: 20rem;
--item-gap: var(--spacing--medium);
--item-width-plus-gap: calc(var(--item-width) + var(--item-gap));
--track-width: calc(var(--item-width-plus-gap) * calc(var(--numItems)));
overflow: hidden;
&:hover .horizontally-scrolling-logos__item {
animation-play-state: paused;
}
}
.horizontally-scrolling-logos__list {
align-items: center;
container-type: inline-size;
display: grid;
gap: var(--spacing--medium);
grid-template-columns: var(--track-width) [track] 0 [resting];
width: max-content;
@media screen and (prefers-reduced-motion: reduce) {
display: flex;
flex-wrap: wrap;
justify-content: center;
width: auto;
}
}
.horizontally-scrolling-logos__item {
animation: marquee var(--speed) linear infinite var(--direction, forwards);
animation-delay: calc(
var(--single-slide-speed) * var(--item-position) * -1
);
grid-area: resting;
width: var(--item-width);
&:nth-child(1) {
--item-position: 1;
}
&:nth-child(2) {
--item-position: 2;
}
&:nth-child(3) {
--item-position: 3;
}
&:nth-child(4) {
--item-position: 4;
}
&:nth-child(5) {
--item-position: 5;
}
@media screen and (prefers-reduced-motion: reduce) {
animation: none;
display: flex;
justify-content: center;
}
}
@keyframes marquee {
to {
transform: translateX(calc(-100cqw - 100%));
}
}
And what it looks like all together:
See the Pen Marquee Old by Elaina Natario (@enatario) on CodePen.
Reducing our code with functions
We can reduce the verbosity of this in one spot in particular: where we are defining the position of each child element.
Right now we’re using --item-position to relay the index of the child and set a staggered animation delay on each logo, which creates the marquee effect. We can remove all those nth-child declarations with a sibling-index() function in lieu of --item-position:
.horizontally-scrolling-logos__item {
animation: marquee var(--speed) linear infinite var(--direction, forwards);
animation-delay: calc(
- var(--single-slide-speed) * var(--item-position) * -1
+ var(--single-slide-speed) * sibling-index() * -1
);
grid-area: resting;
width: var(--item-width);
- &:nth-child(1) {
- --item-position: 1;
- }
- &:nth-child(2) {
- --item-position: 2;
- }
- &:nth-child(3) {
- --item-position: 3;
- }
- &:nth-child(4) {
- --item-position: 4;
- }
- &:nth-child(5) {
- --item-position: 5;
- }
@media screen and (prefers-reduced-motion: reduce) {
animation: none;
display: flex;
justify-content: center;
}
}
The --single-slide-speed calculates the overall speed of the animation divided by the number of children (–numItems). We can use sibling-count() here to replace --numItems. And we’ll need to move that calculation to be within the .horizontally-scrolling-logos__item to be able to count the siblings.
.horizontally-scrolling-logos__item {
+ --single-slide-speed: calc(var(--speed) / sibling-count());
animation: marquee var(--speed) linear infinite var(--direction, forwards);
animation-delay: calc(
var(--single-slide-speed) * sibling-index() * -1
);
grid-area: resting;
width: var(--item-width);
@media screen and (prefers-reduced-motion: reduce) {
animation: none;
display: flex;
justify-content: center;
}
}
And that’s it! A small change overall, but an impactful one!
See the Pen Marquee New by Elaina Natario (@enatario) on CodePen.
An annoying caveat
In a perfect world, we’d completely replace --numItems with sibling-count() so our CSS doesn’t have to manually track the number of logos in our marquee. But, in this current implementation, we need the parent element to use that to define the track width, not the children. Perhaps one day, we’ll have a function like children-count() to allow for more dynamic data in our CSS.
Another approach is to offload it onto Javascript by counting the siblings and setting the custom property inline. Our goal here, however, is to keep as much in the CSS as possible, and the tradeoff doesn’t seem worthwhile in this case.
And of course, there are a handful of other approaches to this web pattern that other people have solved in a variety of ways that would require us to rethink this architecture entirely. But we’re here for a quick and easy win.
A quick word on motion
You may have also noticed a declaration block in the code defining a reduced motion layout. This has nothing to do with sibling-index() or sibling-count() but feels worth mentioning (and has been a topic of discussion within our team). While we can do very fun things with animation in CSS, it’s still important to respect user preferences. Our scroll animation turns into static side-by-side images when that preference is reduced motion.
We could improve the code even more, by implementing an opt-in rather than an opt-out preference query.
.horizontally-scrolling-logos__list {
align-items: center;
display: flex;
flex-wrap: wrap;
justify-content: center;
width: auto;
container-type: inline-size;
display: grid;
gap: var(--spacing--medium);
grid-template-columns: var(--track-width) [track] 0 [resting];
width: max-content;
@media screen and (prefers-reduced-motion: no-preference) {
container-type: inline-size;
display: grid;
gap: var(--spacing--medium);
grid-template-columns: var(--track-width) [track] 0 [resting];
width: max-content;
}
}
.horizontally-scrolling-logos__item {
--single-slide-speed: calc(var(--speed) / sibling-count());
animation: none;
display: flex;
justify-content: center;
@media screen and (prefers-reduced-motion: no-preference) {
animation: marquee var(--speed) linear infinite var(--direction, forwards);
animation-delay: calc(
var(--single-slide-speed) * sibling-index() * -1
);
grid-area: resting;
width: var(--item-width);
}
}
All of this is to say: small changes like this add up. Replacing repetitive selectors with functions like sibling-index() and sibling-count() makes the code easier to read and maintain, and a little more resilient to change.
It’s also a glimpse at where CSS is headed. As more logic moves into the language itself, we can rely less on JavaScript for things like layout and interactivity. That shift doesn’t always come in big, flashy features, but in small utilities that quietly reduce verbosity.
This particular change won’t revolutionize your codebase. But it does make things a bit simpler, and that’s usually a good trade.
And if the pace of new CSS features is any indication, we’ll have plenty more opportunities like this soon.