How to use svg for clip path

Viewed 729

I'm trying to build a star rating feature, and am surprised to find that the part that's giving me the most trouble is figuring out how to get the star shape. It seems to me that the most straightforward approach, given what I have built already, is to try and use an SVG star shape to create a clip path on the div elements that show the rating. I'm familiar with clip-path in a general sense, and have used it for more basic shapes, but am struggling a lot to understand how to use it with an svg and why my current code is not working (svgs are not exactly in my typical wheelhouse).

The code:

const stars = document.getElementsByClassName('star');

function fillStars(starClasses, placeRating) {
   const wholeFill = Math.floor(placeRating);
   const decimalFill = placeRating % 1;
   const yellowFill = decimalFill.toFixed(2).toString().replace('0.', '');

   for (let i = 0; i < wholeFill; i++) {
      starClasses[i].style.backgroundColor = '#fbff01';
   }

   if (placeRating - wholeFill !== 0) {
      starClasses[starClasses.length - 1].style.background = `linear-gradient(90deg, #fbff01 ${yellowFill}%, #FFFFFF 0%)`;
   } 
 }

fillStars(stars, 4.3);
.star {
  width: 30px;
  height: 30px;
  border: solid 1px gray;
  clip-path: url(#star-clip);
}
      
#rating {
  display: flex;
}
<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0">
    <defs>
        <clipPath id="star-clip" clipPathUnits="objectBoundingBox">
            <rect fill="#fff" id="canvas_background" height="40" width="41" y="-1" x="-1"/>
              <rect fill="url(#gridpattern)" stroke-width="0" y="0" x="0" height="100%" width="100%"/>
              <path id="svg_1" d="m3.885,14.64105l11.84103,0l3.65897,-12.22295l3.65897,12.22295l11.84103,0l-9.57958,7.55411l3.65916,12.22295l-9.57958,-7.55431l-9.57958,7.55431l3.65916,-12.22295l-9.57958,-7.55411z" stroke-width="1.5" stroke="#000" fill="#fff"/>
        </clipPath>
    </defs>
    </svg>
    <div id="header">
      <div id="rating">
        <div id="star-1" class="star"></div>
        <div id="star-2" class="star"></div>
        <div id="star-3" class="star"></div>
        <div id="star-4" class="star"></div>
        <div id="star-5" class="star"></div>
      </div>
    </div>

3 Answers

There's a good answer here about calculating a clipPath's 0-1 range from the original SVGs dimensions.

.star {
  width: 30px;
  height: 30px;
  clip-path: url(#star-clip);
  background: Gold;
}

#rating {
  display: flex;
}
<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0">
  <defs>
    <clipPath id="star-clip" clipPathUnits="objectBoundingBox" transform="scale(0.025, 0.02439)">
      <path d="m3.885,14.64105l11.84103,0l3.65897,-12.22295l3.65897,12.22295l11.84103,0l-9.57958,7.55411l3.65916,12.22295l-9.57958,-7.55431l-9.57958,7.55431l3.65916,-12.22295l-9.57958,-7.55411z" />
    </clipPath>
  </defs>
</svg>

<div id="rating">
  <div id="star-1" class="star"></div>
  <div id="star-2" class="star"></div>
  <div id="star-3" class="star"></div>
  <div id="star-4" class="star"></div>
  <div id="star-5" class="star"></div>
</div>

Can you use clip-path: polygon(); ?

I think url(#id) is not fully supported or hard to get correct.

A not about the border: the border will be applied to the bounding box

see the last star

.star {
  width: 50px;
  height: 50px;
  background-color: pink;
  border: solid 1px gray;
  clip-path: polygon(54.5% 24.5%, 80.25% 24.27%, 60.73% 34.91%, 75% 52.25%, 44.5% 41.24%, 15% 52.75%, 30.00% 34.75%, 1.5% 25.25%, 31.77% 24.86%, 41.82% 9.01%);
 
}

.star-last {
  background-color: #f1e745;
  height: 50px;
  width: 50px;
  clip-path: polygon(54.5% 24.5%, 80.25% 24.27%, 60.73% 34.91%, 75% 52.25%, 44.5% 41.24%, 15% 52.75%, 30% 34.75%, -200.42% 25.25%, 31.77% 24.86%, 41.82% 9.01%);
  border: 1px solid black;
}
      
#rating {
  display: flex;
}
   
    <div id="header">
      <div id="rating">
        <div id="star-1" class="star"></div>
        <div id="star-2" class="star"></div>
        <div id="star-3" class="star"></div>
        <div id="star-4" class="star"></div>
        <div id="star-5" class="star-last"></div>
      </div>
    </div>

Edit: As I stated it is hard to get right with a standard SVG path, however adding the transform scale is what you need to do as pointed out in the chosen answer, something I now learnt. Below are other way to achieve the same ends but the chosen answer is the best way.

Maybe it is your path definition, as I said in my other answer url(#id) seems somewhat difficult to get right with standard SVG paths.

I copied a heart path from MDN and removed the rects and your code works.

Edit: If you can't work out correct definition syntax for a star path it is possible to use the polygon() code from CSS as decimal in place of the path in the svg. remove <path> from svg and replace with.

 <polygon points="0.545 0.245, 0.8025 0.2427, 0.6073 0.3491, 0.75 0.5225, 0.445 0.4124, 0.15 0.5275, 0.3000 0.3475, 0.15 0.2525, 0.3177 0.2486, 0.4182 0.0901" />

const stars = document.getElementsByClassName('star');

function fillStars(starClasses, placeRating) {
   const wholeFill = Math.floor(placeRating);
   const decimalFill = placeRating % 1;
   const yellowFill = decimalFill.toFixed(2).toString().replace('0.', '');

   for (let i = 0; i < wholeFill; i++) {
      starClasses[i].style.backgroundColor = '#fbff01';
   }

   if (placeRating - wholeFill !== 0) {
      starClasses[starClasses.length - 1].style.background = `linear-gradient(90deg, #fbff01 ${yellowFill}%, #FFFFFF 0%)`;
   } 
 }

fillStars(stars, 4.3);
.star {
  width: 30px;
  height: 30px;
  border: solid 1px gray;
  clip-path: url(#star-clip);
}
  
#rating {
  display: flex;
}

  
<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0">
    <defs>
      <clipPath id="star-clip" clipPathUnits="objectBoundingBox">
         <path d="M0.5,1
                    C 0.5,1,0,0.7,0,0.3
                    A 0.25,0.25,1,1,1,0.5,0.3
                    A 0.25,0.25,1,1,1,1,0.3
                    C 1,0.7,0.5,1,0.5,1 Z" ></path>     
      </clipPath>
    </defs>
    </svg>
    <div id="header">
      <div id="rating">
        <div id="star-1" class="star"></div>
        <div id="star-2" class="star"></div>
        <div id="star-3" class="star"></div>
        <div id="star-4" class="star"></div>
        <div id="star-5" class="star"></div>
      </div>
    </div>

Related