05Count breakdown bars — each score's response count value splits into a horizontal bar length value
Each score from five stars down to one has its response count value converted into a ratio value against the total response count value, becoming that row's bar length value.
Best forA review page showing detailed rating statistics, a survey results summary screen
total() / paint()counts = {5:42, 4:21, 3:9, 2:4, 1:3}bump = [5,3,4,5,2,5,1,4,5,3]

<main class="stage">
<div class="sr is-demo" id="sr">
<div class="sr__list" id="srList" role="list">
<button class="sr__row" type="button" role="listitem" data-star="5" aria-pressed="false">
<span class="sr__label">5</span>
<span class="sr__track"><span class="sr__bar" data-bar></span></span>
<span class="sr__pct" data-pct>0%</span>
</button>
<button class="sr__row" type="button" role="listitem" data-star="4" aria-pressed="false">
<span class="sr__label">4</span>
<span class="sr__track"><span class="sr__bar" data-bar></span></span>
<span class="sr__pct" data-pct>0%</span>
</button>
<button class="sr__row" type="button" role="listitem" data-star="3" aria-pressed="false">
<span class="sr__label">3</span>
<span class="sr__track"><span class="sr__bar" data-bar></span></span>
<span class="sr__pct" data-pct>0%</span>
</button>
<button class="sr__row" type="button" role="listitem" data-star="2" aria-pressed="false">
<span class="sr__label">2</span>
<span class="sr__track"><span class="sr__bar" data-bar></span></span>
<span class="sr__pct" data-pct>0%</span>
</button>
<button class="sr__row" type="button" role="listitem" data-star="1" aria-pressed="false">
<span class="sr__label">1</span>
<span class="sr__track"><span class="sr__bar" data-bar></span></span>
<span class="sr__pct" data-pct>0%</span>
</button>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: 별점별 응답 개수를 전체 응답 개수 합으로 나눈 비율(%)을 막대 길이 값으로 쓴다 (paint()).
@include stage($stage-ink);
.sr__list {
display: grid;
gap: $sp-2;
width: clamp(220px, 70vw, 320px);
}
.sr__row {
display: grid;
grid-template-columns: 16px 1fr 68px;
align-items: center;
gap: $sp-3;
border: 0;
background: none;
padding: $sp-1 $sp-2;
border-radius: $r-sm;
cursor: pointer;
transition: background $dur-quick $ease-out;
&--on { background: rgba($subject-blue-light, .18); }
}
.sr__label {
color: $stage-paper;
font: 600 13px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
text-align: center;
}
.sr__track {
height: 10px;
border-radius: $r-pill;
background: rgba($stage-paper, .14);
overflow: hidden;
}
.sr__bar {
display: block;
height: 100%;
width: 0%;
border-radius: $r-pill;
background: $subject-mint;
transition: width $dur-base $ease-out;
}
.sr__row--on .sr__bar { background: $subject-pink; }
.sr__pct {
color: $stage-paper;
font: 500 12px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
text-align: right;
font-variant-numeric: tabular-nums;
}
@media (max-width: 340px) {
.sr__list { width: 100%; gap: $sp-1; }
.sr__row { grid-template-columns: 14px 1fr 56px; gap: $sp-2; }
.sr__pct { font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.sr__row, .sr__bar { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.sr__list {
display: grid;
gap: 8px;
width: clamp(220px, 70vw, 320px);
}
.sr__row {
display: grid;
grid-template-columns: 16px 1fr 68px;
align-items: center;
gap: 12px;
border: 0;
background: none;
padding: 4px 8px;
border-radius: 8px;
cursor: pointer;
transition: background 220ms ease-out;
}
.sr__row--on {
background: rgba(91, 140, 248, 0.18);
}
.sr__label {
color: #ffffff;
font: 600 13px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
text-align: center;
}
.sr__track {
height: 10px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.14);
overflow: hidden;
}
.sr__bar {
display: block;
height: 100%;
width: 0%;
border-radius: 999px;
background: #4cd4a6;
transition: width 300ms ease-out;
}
.sr__row--on .sr__bar {
background: #ff9ebb;
}
.sr__pct {
color: #ffffff;
font: 500 12px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
text-align: right;
font-variant-numeric: tabular-nums;
}
@media (max-width: 340px) {
.sr__list {
width: 100%;
gap: 4px;
}
.sr__row {
grid-template-columns: 14px 1fr 56px;
gap: 8px;
}
.sr__pct {
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.sr__row, .sr__bar {
transition: none;
}
}(function () {
var root = document.getElementById('sr');
var rows = Array.prototype.slice.call(document.querySelectorAll('.sr__row'));
var counts = { 5: 42, 4: 21, 3: 9, 2: 4, 1: 3 };
var selected = null;
// 계산 겹: 각 별점의 응답 개수를 전체 응답 개수 합으로 나눈 비율(%)을 막대 길이 값으로 쓴다.
function total() {
var t = 0;
for (var k in counts) t += counts[k];
return t;
}
function paint() {
var t = total();
rows.forEach(function (row) {
var star = Number(row.getAttribute('data-star'));
var pct = t ? (counts[star] / t) * 100 : 0;
row.querySelector('[data-bar]').style.width = pct.toFixed(1) + '%';
row.querySelector('[data-pct]').textContent = Math.round(pct) + '% (' + counts[star] + ')';
row.classList.toggle('sr__row--on', star === selected);
row.setAttribute('aria-pressed', String(star === selected));
});
}
rows.forEach(function (row) {
row.addEventListener('click', function () {
root.classList.remove('is-demo'); stopAutoplay();
var star = Number(row.getAttribute('data-star'));
selected = selected === star ? null : star;
paint();
});
});
paint();
// 자동 루프: Star rating 5→1 순서로 한 줄씩 Selected 상태를 옮기고, 한 바퀴마다 응답 개수 하나를
// 늘려 막대 길이 비율이 실시간으로 바뀌는 것을 보여준다.
var order = [5, 4, 3, 2, 1];
var oi = 0;
var bump = [5, 3, 4, 5, 2, 5, 1, 4, 5, 3];
var bi = 0;
var autoTimer = null;
function autoTick() {
selected = order[oi];
oi = (oi + 1) % order.length;
if (oi === 0) { var v = bump[bi % bump.length]; bi++; counts[v] = counts[v] + 1; }
paint();
}
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function stopAutoplay() { if (autoTimer) { clearInterval(autoTimer); autoTimer = null; } }
if (!reduce) { autoTimer = setInterval(autoTick, 380); autoTick(); }
})();import { useEffect, useRef } from "react";
import styles from "./CountBreakdownBars.module.scss";
const STARS = [5, 4, 3, 2, 1];
export default function CountBreakdownBars() {
const rootRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const root = rootRef.current;
if (!root) return;
const rows = Array.prototype.slice.call(
root.querySelectorAll(`.${styles.row}`)
) as HTMLElement[];
let counts: Record<number, number> = { 5: 42, 4: 21, 3: 9, 2: 4, 1: 3 };
let selected: number | null = null;
// 계산 겹: 각 별점의 응답 개수를 전체 응답 개수 합으로 나눈 비율(%)을 막대 길이 값으로 쓴다.
function total() {
let t = 0;
for (const k in counts) t += counts[Number(k)];
return t;
}
function paint() {
const t = total();
rows.forEach((row, idx) => {
const star = STARS[idx];
const pct = t ? (counts[star] / t) * 100 : 0;
row.querySelector<HTMLElement>(`.${styles.bar}`)!.style.width = pct.toFixed(1) + "%";
row.querySelector<HTMLElement>(`.${styles.pct}`)!.textContent =
Math.round(pct) + "% (" + counts[star] + ")";
row.classList.toggle(styles.on, star === selected);
row.setAttribute("aria-pressed", String(star === selected));
});
}
const clickHandlers: Array<() => void> = [];
rows.forEach((row, idx) => {
const handler = () => {
stopAutoplay();
const star = STARS[idx];
selected = selected === star ? null : star;
paint();
};
clickHandlers.push(handler);
row.addEventListener("click", handler);
});
paint();
// 자동 루프: 별점 5→1 순서로 한 줄씩 선택 상태를 옮기고, 한 바퀴마다 응답 개수 하나를
// 늘려 막대 길이 비율이 실시간으로 바뀌는 것을 보여준다.
const order = [5, 4, 3, 2, 1];
let oi = 0;
const bump = [5, 3, 4, 5, 2, 5, 1, 4, 5, 3];
let bi = 0;
let autoTimer: number | null = null;
function autoTick() {
selected = order[oi];
oi = (oi + 1) % order.length;
if (oi === 0) { const v = bump[bi % bump.length]; bi++; counts[v] = counts[v] + 1; }
paint();
}
function stopAutoplay() { if (autoTimer !== null) { window.clearInterval(autoTimer); autoTimer = null; } }
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!reduce) { autoTimer = window.setInterval(autoTick, 380); autoTick(); }
return () => {
stopAutoplay();
rows.forEach((row, idx) => row.removeEventListener("click", clickHandlers[idx]));
};
}, []);
return (
<div ref={rootRef} className={styles.stage}>
<div className={styles.list} role="list">
{STARS.map((star) => (
<button key={star} type="button" className={styles.row} role="listitem" aria-pressed="false">
<span className={styles.label}>{star}</span>
<span className={styles.track}><span className={styles.bar} /></span>
<span className={styles.pct}>0%</span>
</button>
))}
</div>
</div>
);
}05 Count breakdown bars — each score's response count value splits into a horizontal bar length value — Measured area 22.699% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · Star Rating UI: 9 Interactive Rating Patterns
The post this demo lives in
Star Rating UI: 9 Interactive Rating PatternsOne fill value follows the finger's position value in real time, one overlays an average's decimal part on a star, and o See all demos in this post
If you like this effect - similar demos
Pickers07 Picker That Turns Its Value Into an Orbit's Arc Length9 Graph Animation CSS Pickers With Live Recompute
Pickers04 Span bar whose two handles push each other9 time picker ui patterns, from a point to a span
Pickers03 Keyboard-arrow value toggle — each time a direction value changes, a highlighted-cell value moves across the gridDate Picker UI: 9 Popover & Calendar Patterns