DEV Community

Cover image for Building a Reaction Component
Mads Stoumann
Mads Stoumann

Posted on

Building a Reaction Component

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Unfortunately, we can't use the value-attribute, as in:

counter-reset: reaction attr(value);
Enter fullscreen mode Exit fullscreen mode

– 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;
}
Enter fullscreen mode Exit fullscreen mode

And that empty <span> in the markup will now play it's part:

span::after {
  content: counter(reaction);
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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 POSTed:

{
  result: "create",
  category: "like"
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
grahamthedev profile image
GrahamTheDev • Edited

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):-

  1. 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.

  2. It isn't accessible. Your <label> will either say "React With Heart" (if you include the aria-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 add focusable="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 😉

Collapse
 
afif profile image
Temani Afif

Should I prepare the guns??

Collapse
 
grahamthedev profile image
GrahamTheDev • Edited

You picked the wrong Gif from that film! 🤣🤣

Endhiran film - ball of guns scene

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 🤣

Thread Thread
 
madsstoumann profile image
Mads Stoumann

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! 😂😂😂

Thread Thread
 
grahamthedev profile image
GrahamTheDev

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?

Thread Thread
 
madsstoumann profile image
Mads Stoumann

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! 😂

Thread Thread
 
grahamthedev profile image
GrahamTheDev

Absolutely brilliant! That image in my head is hilarious 🤣🤣

Thread Thread
 
siddharthshyniben profile image
Siddharth

By the way you should definitely watch "Endhiran", I've watched it and it's a cool movie

Thread Thread
 
madsstoumann profile image
Mads Stoumann

I will! 😁

Collapse
 
madsstoumann profile image
Mads Stoumann

Yes, please go ahead and make that unicorn-icon with a single <div> and CSS ;-)

Thread Thread
 
madsstoumann profile image
Mads Stoumann

I can make the heart and bookmark in CSS ... but the unicorn?! 😭

Collapse
 
madsstoumann profile image
Mads Stoumann • Edited

Thanks, great feedback as always!

  1. I guess it's the -webkit-tap-highlight-color: transparent;, which were missing, not box-shadow.
  2. I know, I didn't spend time to a11y-fixes this time - it was more a “How much is possible with only CSS”-challenge, and the CSS-only will never be accessible, obviously! ;-) But I guess an aria-label="Total reactions 75" on the span, and pointer-events:none and aria-hidden on the SVG would be enough? But ... if the reactions are “live”, I guess even more araia-stuff is required for feedback.
Collapse
 
grahamthedev profile image
GrahamTheDev

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 take alt 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!

Thread Thread
 
madsstoumann profile image
Mads Stoumann

It's a bit tricky! You have the “total amount of likes” in the value, but want the label to announce “React with ...”! Interesting about the content-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)

Thread Thread
 
grahamthedev profile image
GrahamTheDev

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!) 😋🤣🤣

Thread Thread
 
madsstoumann profile image
Mads Stoumann

"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 😁

Thread Thread
 
link2twenty profile image
Andrew Bone

I'm gonna do a "top 10 @inhuofficial posts" post \s

Thread Thread
 
grahamthedev profile image
GrahamTheDev • Edited

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!

Collapse
 
lexlohr profile image
Alex Lohr

Instead of aria-hidden, I would prefer role="presentation", which is for that exact use case.

Collapse
 
grahamthedev profile image
GrahamTheDev

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.

Thread Thread
 
lexlohr profile image
Alex Lohr

I prefer role="presentation", because it conveys more semantic meaning than "hide it from assistive technology".

Thread Thread
 
grahamthedev profile image
GrahamTheDev

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:

  • removing semantic meaning from an element (back in the days when tables were used for layout for example)
  • marking a graphic or image as decorative (background images).

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 using aria-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:

<label>
    <input type="checkbox" name="reaction-heart" value="75" style="--c:75" />
    <svg role="image" aria-labelledby="unicorn-title" viewBox="0 0 24 24">
         <title id="unicorn-title">React with a unicorn to this article? 75 others have given it a unicorn</title>
         [...]
    </svg>
    <span aria-hidden="true">75</span>
  </label>
  <label>
Enter fullscreen mode Exit fullscreen mode

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.

Collapse
 
lexlohr profile image
Alex Lohr

You don't need to use a counter. CSS knows content: attr(…) for quite some time, see developer.mozilla.org/en-US/docs/W....

Collapse
 
madsstoumann profile image
Mads Stoumann • Edited

Yes, but as I write in the article, it doesn't work with attr(value), unfortunately :-(
Or do you mean some other way?

Collapse
 
lexlohr profile image
Alex Lohr

You can use an additional data attribute.

Thread Thread
 
madsstoumann profile image
Mads Stoumann • Edited

How would you do the CSS-only version without a counter (adding or subtracting 1)?

Thread Thread
 
lexlohr profile image
Alex Lohr

Can't you initialize the counter from an attr()? Then at least you wouldn't have to abuse style for content.

Thread Thread
 
lexlohr profile image
Alex Lohr

Just checked again, that does not work either.

 
madsstoumann profile image
Mads Stoumann

That’s, unfortunately, the issue: it doesn’t work!

Collapse
 
siddharthshyniben profile image
Siddharth

It's actually not the exact same thing, there's supposed to be a pulse when we click it. Realy nice though!

Collapse
 
madsstoumann profile image
Mads Stoumann

Thx! I didn’t look into animations, I’ll update the examples later