We're all familiar with dev.to's “Reaction Component” (although I'm still not sure, what the unicorn is for!) Here's a short tutorial on how to create a “Reaction Component” – both with and without JavaScipt.
Let's start with the CSS version!
Markup
We're going to wrap each reaction in a <label>
, and add the <svg>
and an empty <span>
after a <input type="checkbox">
:
<label aria-label="React with heart">
<input type="checkbox" name="reaction-heart" value="75" style="--c:75" />
<svg></svg>
<span></span>
</label>
The <input type="checkbox">
is what we'll use to control both state
and value
.
Icon
On dev.to, two different icons are used, when you “react” to something. For the “like“-action, there's an unfilled heart and a filled heart. Same story for the “unicorn” and “bookmark”-reactions.
One could argue, that with slight design changes, the icons could simply toggle SVG's fill
, stroke
or stroke-width
– but let's leave it at two icons. We'll <g>
roup them within a single SVG:
<svg viewBox="0 0 24 24">
<g><path d="M21.179 12.794l.013.014L12 22l-9.192-9.192.013-.014A6.5 6.5 0 0112 3.64a6.5 6.5 0 019.179 9.154zM4.575 5.383a4.5 4.5 0 000 6.364L12 19.172l7.425-7.425a4.5 4.5 0 10-6.364-6.364L8.818 9.626 7.404 8.21l3.162-3.162a4.5 4.5 0 00-5.99.334l-.001.001z"></path></g>
<g><path d="M2.821 12.794a6.5 6.5 0 017.413-10.24h-.002L5.99 6.798l1.414 1.414 4.242-4.242a6.5 6.5 0 019.193 9.192L12 22l-9.192-9.192.013-.014z"></path></g>
</svg>
In CSS, we an use the :checked
pseudo-selector to toggle between the two icons (in <g>
-tags):
[name*="reaction-"]:checked + svg g:first-of-type,
[name*="reaction-"]:not(:checked) + svg g:last-of-type {
opacity: 0;
}
[name*="reaction-"]:checked + svg g:last-of-type {
opacity: 1;
}
Cool, now we can toggle between the two icons using the checkbox, let's add a counter! Did you notice the style="--c:75"
in the markup?
We'll use that for a CSS counter:
counter-reset: reaction var(--c);
Unfortunately, we can't use the value
-attribute, as in:
counter-reset: reaction attr(value);
– so we have to use that extra custom property, --c
, for the initial value.
Then, we'll hook into the :checked
-selector again:
[name*="reaction-"]:checked {
counter-increment: reaction;
}
And that empty <span>
in the markup will now play it's part:
span::after {
content: counter(reaction);
}
But why the empty <span>
? That's because we have to add the counter as pseudo-element content (::before
or ::after
).
Unfortunately, we can't add a pseudo-element to the <input type="checkbox">
, as <input>
-tags are part of the group of tags, that can't have children (aka “self-closing” tags) or pseudo-content (actually they can in Chrome and Safari, but it's not part of the spec!).
The rest is just bits of styling. Here's the CSS-only example on Codepen:
JavaScript
Even though the CSS-only version is cool, it's not very practical. You'll probably want to store the reaction somewhere!
Let's remove the counter-related stuff from the CSS, and the style="--c"
-part from the markup. We'll wrap the reactions in a <form id="react">
, and listen for changes using the onchange
-eventListener:
react.addEventListener('change', (e) => {
const t = e.target;
t.parentNode.lastElementChild.innerText = t.value = t.value - 0 + (t.checked ? 1 : -1);
});
This small snippet will add or subtract 1
(one) from the value
of the current reaction, then set the innerText
of the <span>
to that.
It's within this snippet, you can add a fetch()
(with POST
) to store the current reaction.
On dev.to, for instance, a small JSON
-object is POST
ed:
{
result: "create",
category: "like"
}
Example, using JavaScript:
If you want to set the text of all the <span>
-elements to the value
of the <input>
s, use this small snippet to iterate the elements
-collection of the <form>
:
[...react.elements].forEach(t => t.parentNode.lastElementChild.innerText = t.value);
That's it! Recently, there was a “Star Rating”-challenge here on dev.to (my entries were Star Rating Using a Single Input and Mood Selector).
It's always interesting to see how other developers solve problems, so please share a link in the comments, if you modify my example, or – event better – make your own “Reaction Component”!
Top comments (31)
Oh you want to start the next war? I didn't think the last one had truly ended.
I don't remember signing the peace treaty on that one and Temani decided to fire another shot so I am just gathering my "forces" (pun intended)! 😉😋🤣
Few things (yes you know what I am like):-
iPhone - the
box-shadow
animation is buggy so your surrounding circle flickers when it transitions. Pretty sure it is an easy fix but cant spot it on my phone.It isn't accessible. Your
<label>
will either say "React With Heart" (if you include thearia-label
you showed in the first code snippet...you didn't add it to any codepens) and not read the value inside the<label>
, or it will just read the value inside the<label>
(which is not useful as it will literally read "75").Also when you increment your counter the number will not change for screen readers in your CSS only version so I don't think you could ever make that accessible without some hacks!
It would also be a good idea to add
aria-hidden
to the SVGs just for robustness (as some older screen reader / browser combos will attempt to tell the screen reader that there is an SVG there). If you really want to cover all the bases also addfocusable="false"
due to some ancient browsers making inline SVGs focusable!Obviously it still gets my ❤ and 🦄 as always, just thought I would point them out!
Now I am going to look at how DEV implemented it and see what issues they have when I get back to my PC 😉
Should I prepare the guns??
You picked the wrong Gif from that film! 🤣🤣
Oh and "Endhiran" is a film on my watch list if I can find an English Dub due to the insane clips I have seen, just looks beyond silly! It is Matrix, iRobot, Terminator, Transformers, Alita and Austin Powers in one movie 🤣
Never heard about that movie!
BTW: I was once in a Bollywood-movie! Spent a whole day “playing” a guy in the audience in a cinema in the movie “Na Tum Jaano Na Hum". I later bought it on DVD, but couldn't find the scene! 😂😂😂
Ok so now I have a new mission far more important than winning a war, find Mads on a film!
I will have to get the Blu-ray version so the definition is high enough to spot you! 🤣
That is one of those great little factoids to pull out of your back pocket, how on Earth did you manage that one?
I was backpacking with a friend in India, when a guy approached us in a café. He needed some “white dudes” to sit in the audience of a cinema, just behind the main characters. There wasn't any movie playing, so to simulate the “flicker”, a guy was waving two baseball bats in front of a large lamp! 😂
Absolutely brilliant! That image in my head is hilarious 🤣🤣
By the way you should definitely watch "Endhiran", I've watched it and it's a cool movie
I will! 😁
Yes, please go ahead and make that unicorn-icon with a single
<div>
and CSS ;-)I can make the heart and bookmark in CSS ... but the unicorn?! 😭
Thanks, great feedback as always!
-webkit-tap-highlight-color: transparent;
, which were missing, notbox-shadow
.aria-label="Total reactions 75"
on the span, andpointer-events:none
andaria-hidden
on the SVG would be enough? But ... if the reactions are “live”, I guess even morearaia
-stuff is required for feedback.It’s an Interesting one for the label as it serves two purposes, it tells you what the control is for (“like this article”) and at the same time current likes (“this article has 75 likes” and that number changes if you like it). So you have to find a good way of combining the two.
If you got that right with some careful phrasing you wouldn’t need any
aria
attributes.As for CSS being accessible the
content
property can now takealt text
...it just doesn’t have good support yet!So you can do
content: * / “required”
and the “required” part is what a screen reader will hear! So in the future it might actually be possible!It's a bit tricky! You have the “total amount of likes” in the
value
, but want thelabel
to announce “React with ...”! Interesting about thecontent
-attribute, didn't know that – I always love it, when something is or will be solvable without JavaScript (I have nothing against JavaScript, but only use it when necessary)Yup, that is one of the "soft skills" of accessibility, label names and alt text is as much art as science.
My gut reaction is:
"React with a unicorn to this article? 75 others have given it a unicorn".
That way when you do check your checkbox that makes sense and at the same time you don't have to worry about incrementing the number as it still makes sense.
It would be one of those things I ponder as I work and come up with better phrasing, but the key to any answer would be "other people's reactions" as it eliminates the need to increment and report back.
Glad I managed to teach you something as it has been seeming like a one way street lately (you teaching me!) 😋🤣🤣
"React with a unicorn to this article? 75 others have given it a unicorn".
This is brilliant, precise and clear!
I’m always learning from your articles as well! dev.to has great content, when you ignore the Top 10 and listicles 😁
I'm gonna do a "top 10 @inhuofficial posts" post \s
Oh you do like to make me love and hate your comments at the same time lately...glad you get me 😜🤣
Anyway if you actually liked and read my posts you would know it has to be a prime number so it should be 11 at least! 😂🙈 go reread my guide to listicles as punishment for causing me pain again!
Instead of
aria-hidden
, I would preferrole="presentation"
, which is for that exact use case.aria-hidden
removes it from the accessibility tree entirely,role="presentation"
purely removes roles from an item to give it no semantic meaning.To be fair here it probably doesn't matter as either way and support is pretty similar for both attributes so this is one of those very rare "personal preference" instances I suppose, both would be accessible.
I prefer
role="presentation"
, because it conveys more semantic meaning than "hide it from assistive technology".It is semantically incorrect to use
role="presentation"
here.The icon has meaning, it isn't accompanied by a visible label.
role="presentation"
is not really designed for this scenario, it is designed for either:Neither of the above are true.
Where
role="presentation"
is applicable is if you have a button with text and an icon, the icon adds nothing of value so it is presentational. That is not the case here as the icon is the label.The most semantically correct way of creating this control would be to use the
<title>
and or<description>
on the SVG as the label information (and usingaria-labelledby
etc. to ensure it is as robust as possible.When the icon is the sole source of information you either have to give it
alt
attributes (or<title>
which is the equivalent in SVG) or hide it entirely and replace it with a label for screen readers that makes sense.At the end of the day, both options will work, but if we really want to be semantically correct the following is the proper way of doing it:
That is the best option from a semantics perspective.
Now in the scenario I suggested we instead decided to create a label just for screen readers. In that scenario it is entirely correct to use
aria-hidden
as we want to remove the item we are replacing from the accessibility tree.As I said, both options work so we are splitting hairs here, but it certainly isn't semantically correct to mark an item as presentational if it is the source of information.
You don't need to use a counter. CSS knows
content: attr(…)
for quite some time, see developer.mozilla.org/en-US/docs/W....Yes, but as I write in the article, it doesn't work with
attr(value)
, unfortunately :-(Or do you mean some other way?
You can use an additional data attribute.
How would you do the CSS-only version without a counter (adding or subtracting 1)?
Can't you initialize the counter from an attr()? Then at least you wouldn't have to abuse style for content.
Just checked again, that does not work either.
That’s, unfortunately, the issue: it doesn’t work!
It's actually not the exact same thing, there's supposed to be a pulse when we click it. Realy nice though!
Thx! I didn’t look into animations, I’ll update the examples later