Skip to content

RatingScale

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/rating-scale.json
See it liveScored, not judged yet, a longer scale, and disabled.

Marks rather than stars. Stars carry a meaning of their own — a review, a public verdict — and this is as often “how hard was this” or “how finished is it” as it is “how good”.

Not judged yet is a state, not a zero. This is the whole reason the component exists and the reason it is not a Slider with a small range or a RadioGroup with five options. “I have not scored this” and “I scored it nothing” are different facts, and a control that collapses them makes the difference unrecoverable the moment it saves.

<RatingScale
scale={5}
value={score} // number | undefined
onValueChange={setScore} // receives undefined when cleared
label="Difficulty"
emptyLabel="Not judged"
/>

There are two ways back to it, and both exist because losing either one loses the state:

  • the pointer — click the mark already chosen;
  • the keyboard — Backspace or Delete.

emptyLabel is required. It is what a screen reader hears in place of a number, and a default here would be English shipped inside a primitive. Same for label: a bare row of marks names nothing.

One control, not five. The container is the slider — one tab stop, arrows within it — and the marks are plain elements. A <button> per mark would be a nested interactive control inside a slider, which assistive technology is not promised to announce or reach; the donor this came from had them as aria-hidden buttons, which hides them from a reader without making them stop being controls.

Prop Type Default
scale number How many marks
value number | undefined undefined is not judged yet
onValueChange (value) => void Receives undefined when cleared
label string Required. What is being judged
emptyLabel string Required. Heard in place of a number
disabled boolean false