Say you have five buttons. Each button is a step. If you click on the fourth button, you’re on step 4 of 5, and you want to display that.
This kind of counting and displaying could be hard-coded, but that’s no fun. JavaScript could do this job as well. But CSS? Hmmmm. Can it? CSS has counters, so we can certainly count the number of buttons. But how do we calculate only up to a certain button? Turns out it can be done.
HTML
It doesn’t have to be buttons; it just needs to be some sibling elements we can count. But we’ll go ahead and use buttons here:
<div class="steps">
<button class="active">Shop</button>
<button>Cart</button>
<button>Shipping</button>
<button>Checkout</button>
<button>Thank You</button>
<div class="message"></div>
</div>
The empty .message
div there will be where we output our step messaging with CSS content.
CSS
The trick is that we’re actually going to use three counters:
- A total count of all the buttons
- A count of the current step
- A count of how many remaining steps are after the current step
.steps {
counter-reset:
currentStep 0
remainder 0
totalStep 0;
}
Now let’s actually do the counting. To count all buttons is straightforward:
button {
counter-increment: totalStep;
}
Next, we need another thing to count that will also count the buttons. We can use a pseudo-element that’s only purpose is to count buttons:
button::before {
content: "";
counter-increment: currentStep;
}
The trick is to stop counting that pseudo-element on all the elements after the active element. If we’re using an .active
class that looks like this:
button.active ~ button::before {
/* prevents currentStep from being incremented! */
counter-increment: remainder;
}
We’re counting the remainder
there, which might also be useful, but because we’re only incrementing the remainder, that means we’re not counting the currentStep
counter. Fancy, fancy.
Then we can use the counters to output our messaging:
message::before {
content: "Step: " counter(currentStep) " / " counter(totalStep);
}
Here it is!
There is a little JavaScript there so you can play with moving the active state on the button, but the counting and messaging is all CSS.
Maybe this is nuts, but could you use “ elements and the `:checked` pseudo selector trick here instead of updating the `active` class? Maybe that gets too murky since you’re already dealing with sibling selectors. My brain hurts…
Thanks.
I don’t understand why “counter-increment: currentStep;” works for pseudo-element “before” one by one and it doesn’t work like an accumulative counter like in “button” element
Regards
Maybe use radio buttons and :checked instead of active class to make it fully css? I did not try. Nice trick!
Hi Chris,
Using radio buttons, instead of buttons, allows to do this with CSS only (no JS needed).
I understand this is a demo, and in reality these buttons could be links or toggles. A screen reader user will likely be using
Tab
to navigate the controls and will not see the visible counter changes. But witharia-current
you can programmatically convey the current step to them and use it instead of a class to assign styles. Now if your programmatic state does not match, it will be visually apparent to the developer.I was thinking the same thing, that CSS pseudo element content, either :before or :after, must only be used for decorative purposes. This is a violation because assistive technologies may not be able to access the content inserted with css. So must add some idea to inform users what is going on.
Hi Adrian,
Don’t we get that “for free” using radio buttons instead of buttons?
Isn’t it better to use markup that does not need ARIA (even more if those attributes need to rely on JavaScript)?
@Martin, all modern browsers (and IE11 or even back to IE9) expose CSS generated content / pseduo-content (
::before
and::after
) to assistive technology. They are just additional text nodes / leafs (leaves?) in the accessibility tree.@Thierry, in the original example, the
<button>
s appear to be intended to navigate a user through views. Using radio buttons to navigate users through a flow would be a mis-use since they are for data entry.Having the screen change when choosing one radio button would also be a 3.2.2 On Input WCAG violation. If anything, it might be worth upgrading the buttons with
aria-pressed
to make them into toggles (which I noted in my comment), but for the context of the demo plain<button>
s are fine.I forked mine from above and set
aria-pressed
; it is more verbose so it may not fit all use cases.@Adrian
Good call about “navigat[ing] users through a flow”; I hadn’t seen it like this at first.