Rating Widget

A Clickable BookNest Rating Widget

The widget combines the star <symbol> of <use>, the clipped overlay that showed The Quiet Harbor's 4.6 in Clipping and Masking, and this section's events. It shows the average until the reader rates the book: the pointer previews a rating, a click sets it, and as a role="slider" with tabindex it also takes the arrow keys and announces its value.

The Quiet Harbor's 4.6 average as a clickable, keyboard-operable ratingHTMLLive
<p style="font:16px Georgia; margin:4px 0">The Quiet Harbor
  <svg id="rating" width="156" height="30" viewBox="0 0 208 40" role="slider" tabindex="0"
    aria-label="Rating" aria-valuemin="1" aria-valuemax="5" aria-valuenow="4.6"
    style="vertical-align:middle; cursor:pointer">
    <symbol id="star" viewBox="0 0 24 24"><path d="M12 2.6 14.5 9.2 21.5 9.5 16 13.9 17.9
      20.7 12 16.8 6.1 20.7 8 13.9 2.5 9.5 9.5 9.2Z"/></symbol>
    <clipPath id="fill"><rect height="40"/></clipPath>
    <defs><g id="five"></g></defs>
    <use href="#five" fill="#e0d8c8"/><use href="#five" fill="#e09a10" clip-path="url(#fill)"/>
  </svg> <span id="label">4.6 average</span></p>
<script>
  five.innerHTML = [0, 42, 84, 126, 168]
    .map(x => `<use href="#star" x="${x}" width="40" height="40"/>`).join('');
  const show = r => fill.firstChild.setAttribute('width', 42 * Math.floor(r) + 40 * (r % 1));
  let value = 4.6;
  show(value);
  const set = r => {
    value = Math.min(5, Math.max(1, r)); show(value);
    rating.setAttribute('aria-valuenow', value); label.textContent = `you rated ${value}`;
    console.log(`rating = ${value}`);
  };
  const starAt = e => Math.ceil(new DOMPoint(e.clientX, e.clientY)
    .matrixTransform(rating.getScreenCTM().inverse()).x / 42);
  rating.onpointermove = e => show(starAt(e));          // preview under the pointer
  rating.onpointerleave = () => show(value);
  rating.onclick = e => set(starAt(e));
  rating.onkeydown = e => {
    const step = { ArrowRight: 1, ArrowUp: 1, ArrowLeft: -1, ArrowDown: -1 }[e.key];
    if (step) { e.preventDefault(); set(Math.round(value) + step); }
  };
</script>
Browser output of Listing 1.33
Browser output of 33
Output of 33
hover star 2: clip width 84
pointer left: clip width 192
rating = 5
rating = 4
rating = 3
slider 3, you rated 3

A Playwright 26,280 session hovered the second star, moved away, clicked the fifth star and pressed the Left arrow twice, then read the slider's state. The pointer maps to a star through Pointer Events and the SVG DOM's inverse screen matrix, so the widget works at any size, and the clip width is Clipping and Masking's formula: 42 per whole star plus 40 times the fraction. It is saved as rating-widget.html; D3.js charts the ratings themselves.