Home > OS >  Partially Fill SVG icon using Css
Partially Fill SVG icon using Css

Time:11-13

Is it possible to fill an svg icon partially based on some values? use case : Star Rating

svg{
fill: red;
}
<!DOCTYPE html>
<html lang="en">
  <title></title>
  <script src="https://unpkg.com/feather-icons"></script>
  <body>

    <!-- example icon -->
    <i data-feather="star"></i>
     <i data-feather="star"></i>
      <i data-feather="star"></i>

    <script>
      feather.replace()
    </script>
  </body>
</html>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

CodePudding user response:

Assuming you can't directly edit the original path, you could manipulate the dom of the SVG to insert a gradient tag with an ID, then reference it in the CSS with fill : url(#gradient);. It seems a bit hacky but it works :

svg.full {
  fill:red;
}

svg#half {
  fill : url(#gradient);
}
<!DOCTYPE html>
<html lang="en">
  <head>
    <title></title>
    <script src="https://unpkg.com/feather-icons"></script>
  </head>
  <body>

    <!-- example icon -->
    <i class="full" data-feather="star"></i>
    <i id="half" data-feather="star"></i>
    <i data-feather="star"></i>
    
    <script type="text/javascript">
      feather.replace();
      
      // create the defs SVG tag
      // it is important to use createElementNS in that case
      const defs = document.createElementNS("http://www.w3.org/2000/svg", 'defs');
      defs.innerHTML = '<linearGradient id="gradient"><stop offset="50%" stop-color="#F00"></stop><stop offset="50%" stop-color="transparent" ></stop></linearGradient>';
      
      // Here I'm targeting only the middle start by ID
      // You should modify the code to target all half-stars
      document.getElementById('half').append(defs);
      
    </script>
   
  </body>
</html>
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

PS : By playing with the <stop> tags attribute's, you could achieve some original effects.

CodePudding user response:

You can do it in ONE SVG

  • I ditched the Font-Awesome icon
  • searched for "star" in the 7000 icons on https://iconmeister.github.io/ (first load takes a minute)
  • Picked the star icon with the best d-path (Clarity Iconset: cl-social-star-solid)
  • copied only the d-path
  • Edited the d-path in https://yqnn.github.io/svg-path-editor/ to a 100x100 viewBox/grid
  • made it an inverse star by prepending M0 0h100v100h-100v-100 to the path
  • Created a new SVG file in a 0 0 300 100 viewBox to fit 3 stars.. see below
  • Added a background rectangle setting gold color rating with width="50%"
  • Used 3 inverse stars, each at an x-offset
  • added 6 rectangles covering all half-stars
  • set inline events on every "half-star"
    (the click works in this SO snippet, but SO adds a long delay)

Proof of Concept

<svg viewBox="0 0 300 100" width="500px">
  <rect id="rating" width="50%" fill="gold" height="100%" />

  <path id="star" fill="green" 
        d="M0 0h100v100h-100v-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"/>
    <use href="#star" x="100" />
    <use href="#star" x="200" />

  <rect id="c" width="16.66%" height="100%" fill="transparent" stroke="red" 
        onclick="console.log(this)" />
    <use href="#c" x="50" />
    <use href="#c" x="100" />
    <use href="#c" x="150" />
    <use href="#c" x="200" />
    <use href="#c" x="250" />
</svg>
<iframe name="sif3" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

A Star Rating Component <star-rating stars=N >

You don't want to create all this SVG by hand... couple lines of JavaScript can create the SVG, for any number of stars

Using a W3C standard Web Component here, because it runs in this page and is not as complex as a React Component.

https://developer.mozilla.org/en-US/docs/Web/Web_Components

  • Not using <use>, just duplicate all paths and rects with a x offset
  • mouseover events set the background % color
  • click shows the index of the clicked halfstar (0 )
  • Rating can be set with values or percentage; document.querySelector('[stars="5"]').rating="90%" (4.5 stars)
  • might needs extra work for your use case

All required HTML & JavaScript:

<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>
<script>
  document.addEventListener("click", (evt) => console.log(evt.target.getAttribute("n")))

  customElements.define("star-rating", class extends HTMLElement {
    set rating( rate ) {
      if (!String(rate).includes("%")) rate = Number(rate) / this.stars * 100   "%";
      this.querySelector("#rating").setAttribute("width", rate);
    }
    connectedCallback() {
      let { bgcolor, stars, nocolor, color, rating } = this.attributes;
      this.stars = ~~stars.value || 5;
      this.innerHTML = 
        `<svg viewBox="0 0 ${this.stars*100} 100" style="cursor:pointer;width:300px">`
        `<rect width="100%" height="100" fill="${nocolor.value}"/>`
        `<rect id="rating"  height="100" fill="${color.value}"  />`
          Array(  this.stars     ).fill()
               .map((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"/>`)
               .join("")
          Array(  this.stars * 2 ).fill()
               .map((i, n) => `<rect x="${ n*50 }" n="${n}" opacity="0" width="50" height="100"`
                    ` onclick="dispatchEvent(new Event('click'))" `
                    ` onm ouseover="this.closest('star-rating').rating = ${(n 1)/2}"/>`)
              .join("") 
        "</svg>";

      this.rating = rating.value;
    }
  });
</script>
<iframe name="sif4" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

Notes:

  • This native <star-rating> Component (also called Custom Element because NO shadowDOM is involved) has ZERO dependencies
    • no libraries
    • no external SVG
  • native components are not self-closing tags and must contain a hyphen, so notation is: <star-rating></star-rating>
  • Changed the star to M0 0h102v100h-102v-100 (2 pixel overlap) to cover SVG rounding issues

Supported in all Frameworks... except...

React doesn't support this modern W3C Web Components Standard yet.

React scores just 71% on https://custom-elements-everywhere.com/

All other Frameworks (Angular, Vue, Svelte) have 100% support

You have to do some extra work to handle native DOM Elements and Events in React; but the Custom Element isn't complex.. it creates SVG; should be easy to replicate as a React Component.

  • Related