Unicode mood selector (star-rating)

There have been several posts written this week about star ratings, as part of the "Star (rating) Wars". I've written a couple of articles first how to make an accessible star rating system and then a follow up about how to make it a little more interesting with animations.

In this article I'll be doing something a little different. I'll take the lessons we've learnt from the past two articles and making an animated mood selector.

the Star (rating) wars

If you're interested, and I think you will be, there are a few posts from different authors worth reading. Check out posts by @inhuofficial , @lapstjup , @madsstoumann , @afif , @siddharthshyniben and @lionelrowe .

The code

I'm going to briefly touch on each part of the component and how it's built but I won't be going into too much depth, that being said if you have any questions, suggestions or want clarification feel free to leave a comment and I'll do my best to answer.

HTML

The HTML will be a little different to my last two posts in that it will be segregated into three sections. The whole things will still be wrapped in a fieldset but within that there will be a block of inputs (radio buttons specifically), a block of labels inside a div and a block of divs that we'll call tooltips as we're going to have a tooltip to show which mood we have selected.

Inputs

Inputs will be easy just a set of inputs with their type set to radio and a shared name, they will also need a unique id so our labels can reference them.

<input name="rating" value="1" type="radio" id="rating1">

Labels

Labels will be described by their tooltip, which means we know our tooltips will need ids, I've added a title so we can see what each mood is meant to be on mouse over.

We also have a span inside the label that contains the unicode character we want to display I've chosen 5 emojis but you could use whatever you like.

<label title="Sad" aria-describedby="SadTooltip" for="rating1">
  <span aria-hidden="true" class="star">😞</span>
</label>

Tooltips

Our tooltips are divs that contain some text to be displayed. We know we're linking them to the labels with aria-describeby so we've added a unique id.

<div class="tooltip" id="SadTooltip">😞 Sad</div>

CSS

Most of the magic happens in the CSS, there are a few simple animations so I'll try and go over anything interesting but the full code will be at the end of the post.

Default label

Each label has some default styles, which aren't that interesting, the only only ones of note are color: transparent; which means our unicode characters will be invisible and transform: scale(0.2); which mean the label will appear tiny.

.emotion-rating .labels>label {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  position: relative;
  line-height: 1em;
  text-align: center;
  transform: scale(0.2);
  color: transparent;
  transition: all 350ms cubic-bezier(0.36, 0.07, 0.19, 0.97);
  background-color: #F08080;
  height: 1.75em;
  width: 1.75em;
  border-radius: 1em;
  user-select: none;
}

Label hover

When we hover over a label we increase the size a little and stop hiding the emoji. We also increase the z index just incase.

.emotion-rating .labels>label:hover {
  transform: scale(0.6);
  color: initial;
  z-index: 1;
}

Default tooltip

We have a bunch of boring styles in here but we also have the opacity and height set to 0 meaning, by default, the tooltip won't take up any space in the dom.

.emotion-rating .tooltip {
  font-size: 0.75em;
  height: 0;
  text-align: left;
  box-sizing: border-box;
  border-radius: 15px;
  background-color: white;
  opacity: 0;
  transform: translateY(-50%);
  position: relative;
  transition-property: opacity, transform;
  transition: 0 cubic-bezier(0.36, 0.07, 0.19, 0.97);
}

Changes based on checked

I've added some very basic labels for the CSS below. With an approach like this, where everything is manual, you gain performance (because there is no JS to wait for) but it does mean you have to write a lot of extra code. There is a block like this for every input.

/* label for input before currently checked */
#rating3:checked~.labels>[for=rating2] {
  transform: scale(1);
  color: initial;
  z-index: 1;
}

/* emoji for input before currently checked */
#rating3:checked~.labels>[for=rating2] .star {
  opacity: 0.8;
}

/* label for input currently checked */
#rating3:checked~.labels>[for=rating3] {
  transform: scale(1.4);
  color: initial;
  z-index: 0;
}

/* label for input after currently checked */
#rating3:checked~.labels>[for=rating4] {
  transform: scale(1);
  color: initial;
  z-index: 1;
}

/* emoji for input after currently checked */
#rating3:checked~.labels>[for=rating4] .star {
  opacity: 0.8;
}

/* tooltip related to checked input */
#rating3:checked~#NeutralTooltip {
  display: block;
  opacity: 1;
  transform: translate(0);
  height: auto;
  padding: 0.4em 0.8em;
  margin-top: 0.8em;
  border: 4px solid #F08080;
  transition-duration: 350ms;
  transition-delay: 150ms;
}

Prefers Reduced Motion

There is a lot of motion in this component and because of that add a prefers-reduced-motion media query is super important.

I've taken out all the animations times that involve moving or growing/shrinking and change the style of the labels to make a little more sense without the motion. I don't think it's worth going through this code but if you have any question leave a comment.

@media (prefers-reduced-motion) {
  .emotion-rating .tooltip {
    transform: translate(0);
  }

  .emotion-rating .labels>label {
    transform: scale(0.6) !important;
    color: initial;
    transition-duration: 0ms;
  }

  .emotion-rating .labels>label .star {
    opacity: 0.8;
  }

  #rating1:checked~.labels>[for=rating1] {
    transform: scale(1) !important;
  }

  #rating2:checked~.labels>[for=rating2] {
    transform: scale(1) !important;
  }

  #rating3:checked~.labels>[for=rating3] {
    transform: scale(1) !important;
  }

  #rating4:checked~.labels>[for=rating4] {
    transform: scale(1) !important;
  }

  #rating5:checked~.labels>[for=rating5] {
    transform: scale(1) !important;
  }
}

The result

Well wasn't that a lot of code, almost 300 lines of CSS, but I think it shows you what is possible without needing JS and also what sort of interactions you can have without sacrificing accessibility.

Fin

Thank you all for reading, I think this was probably my last entry into the Star (rating) wars. Though do let me know if group posts exploring the same topic are useful and I'll have a chat with the other to see if we want to do this more often (no promises).

Thanks again ❤️👾🧠🤖👾🦄🤖

20