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.
<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>
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.