Imagine you have a list of items. Say, fruit: Banana, Apple, Orange, Pear, Nectarine
We could put those commas (,) in the HTML, but let’s look at how we could do that in CSS instead, giving us an extra level of control. We’ll make sure that last item doesn’t have a comma while we’re at it.
I needed this for a real project recently, and part of the requirements were that any of the items in the list could be hidden/revealed via JavaScript. The commas needed to work correctly no matter which items were currently shown.
One solution I found rather elegant solution is using general sibling combinator. We’ll get to that in a minute. Let’s start with some example HTML. Say you start out with a list of fruits:
<ul class="fruits">
<li class="fruit on">Banana</li>
<li class="fruit on">Apple</li>
<li class="fruit on">Orange</li>
<li class="fruit on">Pear</li>
<li class="fruit on">Nectarine</li>
</ul>
And some basic CSS to make them appear in a list:
.fruits {
display: flex;
padding-inline-start: 0;
list-style: none;
}
.fruit {
display: none; /* hidden by default */
}
.fruit.on { /* JavaScript-added class to reveal list items */
display: inline-block;
}
Now say things happen inside this interface, like a user toggles controls that filter out all fruits that grow in cold climates. Now a different set of fruits is shown, so the fruit.on
class is manipulated with the classList
API.
So far, our HTML and CSS would create a list like this:
BananaOrangeNectarine
Now we can reach for that general sibling combinator to apply a comma-and-space between any two on
elements:
.fruit.on ~ .fruit.on::before {
content: ', ';
}
Nice!
You might be thinking: why not just apply commas to all the list items and remove it from the last with something like :last-child
or :last-of-type
. The trouble with that is the last child might be “off” at any given time. So what we really want is the last item that is “on,” which isn’t easily possible in CSS, since there is nothing like “last of class” available. Hence, the general sibling combinator trick!
In the UI, I used max-width
instead of display
and toggled that between 0
and a reasonable maximum value so that I could use transitions to push items on and off more naturally, making it easier for the user to see which items are being added or removed from the list. You can add the same effect to the pseudo-element as well to make it super smooth.
Here’s a demo with a couple of examples that are both slight variations. The fruits example uses a hidden
class instead of on
, and the veggies example has the animations. SCSS is also used here for the nesting:
I hope this helps others looking for something similar!
Why so complicated? Why not using something like this:
https://codepen.io/vstoitsov/pen/WNGaMQR
Hey Vladislav! I had the same thought, but that doesn’t work when the last item in the list is hidden. (display: none). The specific case I was building for required all the elements to exist in the DOM and use classes to show or hide them, thus :last-child doesn’t always refer to the last visible child.
Following your logic though you could select all non-hidden items and use the after selector to add commas to all except the last visible child. Thanks for the suggestion!
Your variation is not working on ios…
Actuall Llull, I think Vlad just missed the classes I was using to hide objects, I forked and restored them here: https://codepen.io/DaveSeidman/pen/vYXVjPM
It still fails when the last visible item is not the last actual item. Will take a look at that tomorrow :)
When the fruits split to the next line due to overflow, the commas are left on the top line, and the text is below it.
The same doesn’t seem to happen for the vegetables.
Nice catch! I was missing flex-wrap. Here’s a new pen with a partial fix:
https://codepen.io/DaveSeidman/pen/KKgGRjW
Only issue there is the comma’s attached to the begging (:before) of the second element instead of the end (:after) of the first so the commas wrap incorrectly.
Yup, especially prevalent on mobile devices.
I guess :nth-child(n of selector) would be useful here too (although currently only supported in Safari).
Don’t work if you have 2+ strings of text
I’m not sure I see the issue, can you provide an example?
If you wanted to observe a different grammatical rule, say an oxford comma, try;
.fruit:last-child::before { content: ', and '; }
So the result would be;
Banana, Pear, Peach, and Blueberry
I think there might be trouble there in that the last fruit visible shown isn’t necessarily the last-child.
Awesome idea, and yes, you’ll probably have to tweak the selector a bit. It should only select the last visible item:
.on
or:not(.hidden)
You’d also want to prevent it from adding “and” when there’s only one item remaining in the list. There’s probably a way to do that by checking if
:first-of-type(.on)
is not the same element as:last-of-type(.on)
One downside to using the content property on pseudo-elements is that the content is not typically selectable, which can result in a confusing experience if the user tries to copy-paste.
How it looks when copied to clipboard and pasted as plain text (on my Android phone; I expect similar behavior on Windows):
Random Fruits
Banana
Orange
Pear
Random Veggies
Broccoli
Carrots
Potatoes
Spinach
Not ideal that the commas are dropped, but the line breaks make it at least readable.
Both examples create a poor experience for users of assistive tech. The example using an unordered list was at least a bit better, in VoiceOver anyway, because it announces itself as a list. The first example skips the commas completely, and then the second example announces a list and reads each item separately, including reading the commas as their own items. Personally, based on what I’ve read about CSS content, you should only use it for purely decorative purposes as screen reader support for it is not great. See link: https://accessibleweb.com/question-answer/how-is-css-pseudo-content-treated-by-screen-readers/
Thanks for checking! I haven’t tested any screen readers on this but have played with Vox and VoiceOver in the past. Perhaps setting the “role” attribute of the comma in the <ul> to something different would create a better experience?
This is useful but wondering why the example goes with commas (something that could be mistaken as actual part of the prose) instead of some other separator like a bar or a bullet point (which would be more obvious as a list separator and much more likely to be an actual use case).
Good question, and yes, any separator should work!
In my specific case I had several arrays of items that were used in JavaScript to create a <button> and a <span> for each item. The buttons had an active and normal state but needed to remain visible at all times while the spans needed to show and hide based on their respective button’s state. I could have used JavaScript to generate a new comma-separated string of items each time but I felt leaving them in the DOM and toggling their classList to be more elegant.
Creative way to use CSS!
As with Sara’s comment, my concern for this approach however is for web accessibility. It is deemed a failure to use pseudo content for anything other than decorative content, as for users who need to customize or turn off style information, they may not be able read and understand the content.
This was super helpful and easy. Thanks!