Dynamic percentage mask in SVG (for 5-star-reviews)

Viewed 217

I'm creating a 5-star-review using SVG, but need to be able to display a value to 1-decimal-point (e.g. 2.5/5 or 4.2/5).

I also need to be able to display multiple 5-star-reviews on the same page.

I've been able to create the following (which displays 2.5) but the 50% on the second <rect> in the mask (which is required for the 0.5 on the 3rd star) is hardcoded...

<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
  <defs>
    <mask id="partial">
      <rect x="0" y="0" width="64" height="64" fill="white" />
      <rect x="50%" y="0" width="64" height="64" fill="black" />
    </mask>
    <symbol xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" id="star">
      <path d="M32 2l-10 22l-20 2l15 14l-5 22l20-12l20 12l-5-22l15-14l-20-2Z"
        stroke-width="3" stroke="#FFC800" />
    </symbol>
  </defs>
</svg>
<svg width="32" height="32" viewBox="0 0 64 64">
  <use href="#star" fill="#FFC800"></use>
  <use href="#star" fill="none"></use>
</svg>
<svg width="32" height="32" viewBox="0 0 64 64">
  <use href="#star" fill="#FFC800"></use>
  <use href="#star" fill="none"></use>
</svg>
<svg width="32" height="32" viewBox="0 0 64 64">
  <use href="#star" fill="#FFC800" mask="url(#partial)"></use>
  <use href="#star" fill="none"></use>
</svg>
<svg width="32" height="32" viewBox="0 0 64 64">
  <use href="#star" fill="none"></use>
</svg>
<svg width="32" height="32" viewBox="0 0 64 64">
  <use href="#star" fill="none"></use>
</svg>

Is there any way (preferably without javascript/jquery) that I can set the x="50%" on the <use> element?

Please remember that I will have multiple instances of the 5-star-review on the page at the same time, so it's not possible for me to dynamic set the x=50% on the <mask> because it will effect all instances.

What I don't want is to have to have 9 different id="partial10", id="partial20" <mask> elements.

I would like to do something like...

<use href="#star" fill="#FFC800" maskwidth="30%"></use>
1 Answers

If you want multiple instances on one page, then go for a native Web Component (supported in all browsers)

All HTML required:

<star-rating stars=5 rating="3.5"
             bgcolor="green" nocolor="grey" color="gold"></star-rating>
             
<star-rating stars=7 rating="50%"
             bgcolor="rebeccapurple" nocolor="beige" color="goldenrod"></star-rating>

to create:

But it does require JavaScript to create the Web Component

Key is to draw the rating color behind cutouts of all stars

Needs minor work to make 51% notation possible,
add more <rect> elements, it now does 2 for each star, and set the correct width for each mouseover capturing <rect>

The beauty of Web Components is: It totally does NOT matter when the <star-rating> is defined.

 customElements.define("star-rating", class extends HTMLElement {
    set rating(rate) {
      if (!String(rate).includes("%")) rate = Number(rate) / this.stars * 100 + "%";
      this.querySelector(":nth-child(2)").setAttribute("width", rate); //2nd rect
    }
    connectedCallback() {
      let {bgcolor,stars,nocolor,color,rating} = this.attributes;
      let repeat = (count, func) => Array(count).fill().map(func);
      this.stars = ~~stars.value || 5;
      this.innerHTML = `<svg viewBox="0 0 ${this.stars*100} 100" style=cursor:pointer>` +
        `<rect height=100 fill=${nocolor.value} width=100% />` +
        `<rect height=100 fill=${color.value} />` +
        repeat(this.stars    , (i, n) => `<path fill=${bgcolor.value} d="m${ n*100 } 0h102v100h-102v-100m91 42a6 6 90 00-4-10l-22-1a1 1 90 01-1 0l-8-21a6 6 90 00-11 0l-8 21a1 1 90 01-1 1l-22 1a6 6 90 00-4 10l18 14a1 1 90 010 1l-6 22a6 6 90 008 6l19-13a1 1 90 011 0l19 13a6 6 90 006 0a6 6 90 002-6l-6-22a1 1 90 010-1z"/>`) +
        repeat(this.stars * 2, (i, n) => `<rect x=${ n*50 } n=${n} opacity=0 width=50 height=100 ` +
          ` onclick="this.closest('star-rating').dispatchEvent(new Event('click'))" ` +
          ` onmouseover="this.closest('star-rating').rating=${(n+1)/2}"/>`) +
        "</svg>";
      this.rating = rating.value;
    }
  });
<star-rating stars=5 rating="3.5"
             bgcolor="green" nocolor="grey" color="gold"></star-rating>
             
<br>

<star-rating stars=7 rating="50%"
             bgcolor="rebeccapurple" nocolor="beige" color="goldenrod"></star-rating>

Related