This post is the third in a series about the power of CSS.
Article Series:
- Colorizing SVG Backgrounds
- Dropdown Menus
- Logical Styling Based On the Number of Given Elements (this post)
Did you know that CSS is Turing complete? Did you know that you can use it to do some pretty serious logical styling? Well you can! You don’t have to set all of your logic-based styling rules in JavaScript, or even have to use JavaScript to set classes you are styling against. In many cases, CSS can handle that itself. I’m still discovering new CSS tricks everyday, and it just makes me love it even more.
This year, I started working at Bustle Digital Group. In media, as with a lot of products, the engineering team builds a platform that should support all use cases. Our CMS provides capabilities for authors and editors to create articles as well as curate pages, and control ad injection.
Unlike working with a static site, the engineering team doesn’t have full control over what data comes in from the user, so design decisions and governing rules must be made for a good user experience. Some of these scenarios we’ve faced in the digital media space have really inspired me to look into ways of using CSS to solve those UI challenges, and that’s when solutions involving this idea really came into my periphery.
So let’s take a look at some examples!
Example 1: Binary States
An often forgotten and very useful selector is the :empty
pseudo selector. It allows you to style elements based on if they contain any content, or if they don’t. Hello empty states! Empty states are a great way to reach out to your users and show personality in your app, and you can inject that personality right from your CSS.
In this example, we have any list from a user. This could be posts the user has published (as an author), or bookmarked articles a user has saved (as an editor). The use cases are really endless here. Instead of injecting JavaScript, we can use pseudo elements to inject images, styles, and text:
Our solution here is a mere three lines of code:
div:empty:after {
content: 'oh no...';
}
You can also add a :before
pseudo element to inject images or any other content you may want. Alternatively, the :not
pseudo selector may be used in combination with :empty
to create a :not(:empty)
rule and style all elements which are not empty, and therefore do contain children.
See the Pen Empty States by Una Kravets (@una) on CodePen.
Note: This demo is for display purposes only. It is not advised to put content in pseudo elements for accessibility purposes. You can use the same technique of targeting :empty
or :not(:empty)
elements to apply styles to child elements that are more accessible to screen readers.
Advanced Numeric Selection
That was a nice soft ball example, but we can get much more complex than this binary choice of child elements in CSS, and to do this, we will use the :nth-child
pseudo selector! CSS-Tricks has a great tool to help you test and play around with the :nth-child
selection, and it can really come in handy as some of the examples will show you.
But before we get into those, how exactly does this work?
The meat of the code is this, with div
standing in for any given sibling element, and x
standing in for the number we are using to determine style breaks:
div:first-child:nth-last-child(n + x),
div:first-child:nth-last-child(n + x) ~ div
Using :nth-last-child
instead of using :nth-child
for selection allows us to start from the end of a series instead of from the beginning. When we select :nth-last-child(n + x)
, we are selecting the x
value starting from the end. If x = 3
that would look like this:
Now, if we want to count values of n + 3
, we are selecting all items that match 3 or more than 3 from the end. Starting with n = 0
(which would mean 0 + 3
, and the 4th item being the first from the end after 3
). It looks like this:
This is a great start, but the idea here is to conditionally style all of the items based on how many exist. So we need to work with these conditions but select all of the items. Let’s start with selecting the first item. We need to make a condition to see if the entire selection qualifies for the styling, and then start with that first sibling:
Uh oh. We only have the first item selected at this point, and we want to select all of the items. Luckily, we can use the super handy adjacent sibling selector (~
) for that!
Well, now you can see all of the items that follow the first item are selected, but we’re missing the first one, so we need to use two selectors, and thus the final answer becomes:
Example 2: List Formatting
Say you want to list some credits at the end of an article. You’ve got some space to fill, and most articles have a small number of credits, but there are those exceptions that have a high production value and a lot of people involved in the making of them. We want to make sure both of these are good visual experiences, and can do that with CSS alone.
Here’s the plan: if there are four or fewer credits, list them in bullet format. Let them take up vertical space to fill the block appropriately. Once we have five or more credits listed, let’s turn that list into a horizontal format to not get too overwhelming for a reader. This is a small credits box after all!
We can check out the number of elements we have available and style them as block
elements until we hit our cap. At that point, we’ll switch to inline
styling, and add a pseudo element to visually break up the data.
/* 5 or more items display next to each other */
li:first-child:nth-last-child(n + 5),
li:first-child:nth-last-child(n + 5) ~ li {
display: inline;
}
/* Adds semicolon after each item except the last item */
li:first-child:nth-last-child(n + 5) ~ li::before {
content: ';';
margin: 0 0.5em 0 -0.75em;
}
:nth-first-child:nth-last-child(n + 5)
allows us to state: “start with the first child and apply styling to that child and every sibling after it if the original child matches having five or more siblings.”” Is that confusing? Well, it works.
li:first-child:nth-last-child(n + 5)
selects the first list item, and li:first-child:nth-last-child(n + 5) ~ li
selects each list item following the initial one.
See the Pen vrQBMv by Una Kravets (@una) on CodePen.
Example 3: Conditional Carousel
Using this technique, let’s style a carousel to be responsive. At a large size, you want it to be centered in the middle of page when it has three items within it. But when it has enough items to fill the screen horizontally, let it be left-aligned for the user to swipe through it.
What we can do here is stretch the elements to fit the screen unless we have too many elements and they would require an overflow. At that point, let’s go all-in on this overflow and really showcase the carousel capabilities by signaling scroll-ability with arrows and by increasing the margin between items. On top of that, let’s add a sticky arrow button to show that we can scroll through the elements and can tie JavaScript events to make the carousel scroll.
We can do the same thing as above in terms of the technique, but we will also use only the first-child
to detect an arrow
div and display it in the UI. The HTML would look like this:
<ul>
<li>
<div class="box">1</div>
</li>
<li>
<div class="box">2</div>
</li>
...
<button class="arrow">——></button>
</ul>
It’s not ideal to have empty elements in the DOM, but work with me. It’s still a clever hack. We’ll style the .arrow
button to be invisible to the DOM and to screen readers with visibility: hidden
unless the conditions apply (in this case, if four or more items are present). At that point we’ll give it a visible display (display: block
), style, and position it appropriately:
li:first-child:nth-last-child(n + 5) ~ .arrow {
display: block;
position: sticky;
...
}
See the Pen Box Alignment by Una Kravets (@una) on CodePen.
More Information!
In my research for this post, I discovered an excellent post by Heydon Pickering about this technique, called Quantity Queries, and another example by Lea Verou! In the comment thread of Heydon’s post, Paul Irish notes that this is a slower way of selecting elements, so maybe use it with caution.
Great article! I used the same technique to make sure I had a nicely styled shortcut bar. Awesome solution!
https://medium.com/@bramdijkhuis/context-aware-list-items-witch-css-well-sort-of-2a433500fb15
Fantastic article. Learned a very handy new CSS technique. Thank you for sharing this.
I hate to be an HTML markup nitpicker but in the final carousel example you have a “button” as a direct child of a “ul” element. It was my understanding that according to the spec only “li” elements are permitted as direct children of “ul” elements.
Nice write up, as usual. I really love what you are doing and how you are motivate developers to explore and try new things.
A little note though you’ve written that: “That would start with the fourth from the end, starting with n = 1”
it’s not quite correct as
n
actually do starts with 0 and not 1 so in this example style will be applied from the third element from the end and not from the fourth.quote from the specification: “…The :nth-last-child(an+b) pseudo-class notation represents an element that has an+b-1 siblings after it in the document tree, for any positive integer or zero value of n.” (link: https://drafts.csswg.org/selectors-3/#nth-last-child-pseudo)
I’m updating this now! The graphic is correct because it’s the first item after the third from the end, which would be the fourth from the end, but slipped with writing
n = 1
when I should have putn = 0
It’s possible that I’m missing out something but I think that diagram/graphic/image is also doesn’t correct, as the style will be applied from the third element from the end not from the fourth. as it goes: 0 + 3, 1 + 3, 2 + 3, etc….
here is a little demo that I’ve made to demonstrate it: https://goo.gl/LPoC42
The only problem with the :empty selector is that it fails to match when the only thing contained in the element is whitespace. So your code has to be very clean in that respect. Also similar to pseudo elements, you can’t use it on tags which do not have a corresponding closing tag.
Yep! That is true.
Further reading: Heydon Pickering’s ALA article Quantity Queries for CSS https://alistapart.com/article/quantity-queries-for-css
The markup in the last example is not valid HTML:
button
is not allowed to be a child oful
.Wouldn’t that “oh no” state be inaccessible by screenreaders?
Yeah, hence the note right after the example. :)
Una, some amazing stuff in here. I’ve been trying to solve some of these problems lately. The examples you’ve outlined here are so well explained, it’s made me look at them from a completely different perspective, for which I’m so grateful. Thank you!
I first got confused, because you said “adjacent sibling selector”. That would only select the second element.
What you actually mean is the “general sibling selector” (or according to Selectors Level 4 “subsequent-sibling”).
And yes, that is “~”.
Apart from that: nice trick, thanks for the post :-)
Hi. I was wondering, what is the purpose of the
:first-child
selectors in the above examples?I run them on CodePen with the
:first-child
selector removed and they (appear to) behave just the sameAbout the last part in the article:
“In the comment thread of Heydon’s post, Paul Irish notes that this is a slower way of selecting elements, so maybe use it with caution.”
This is the URL for the test that Paul enclosed in the mentioned article: http://output.jsbin.com/gozula/1/quiet and the results:
1. div.box:not(:empty):last-of-type .title: 5.777099609375ms
2. .box–last > .title-container > .title: 3.202880859375ms
3. .box:nth-last-child(-n+1) .title: 6.501708984375ms
So it is not as bad as it was years ago (#3 500 times slower then #1). Tested on Chrome 70.0.
I have pushed your ‘Example 2’ a bit farther by applying appended semicolons to all but the last item when displayed inline. Your current example prepends them to all but the first item. The layout, spacing, and wrapping results in a less than optimal presentation.
The first code snippet should have two colons after the :empty selector. Like so:
div:empty::after
Both are valid syntax:
https://developer.mozilla.org/en-US/docs/Web/CSS/::after#Syntax