ScaleInput
A rating scale drawn from the points and end words the product supplies, for a self-report the product reads, shipping no instrument of its own.
Preview
scale-input · base base · style base-lyraOpen under the product themeInstallation
pnpm dlx shadcn@latest add @opsinjs/scale-input
The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.
Usage
import { ScaleInput } from "@/components/ui/scale-input"<ScaleInput
label="Comfort right now"
points={10}
value={rating}
onValueChange={setRating}
minLabel="Not at all"
maxLabel="Completely"
/>When to use it
Use it when
- One self-report on a scale the product's instrument defines, with the points and end words from the product.
- A single question asked repeatedly with the same points and end words, so the reader can watch their answer move.
- A rating with a low end and a high end the reader places themselves between, rather than unordered choices.
Do not use it when
- The answer is free text in the reader's own words, not a point on a scale.Use
textareainstead. - The options are a fixed set of labelled choices with no end word and no ordering.Use
radio-groupinstead. - A multi-question instrument where several rated items add up under one heading.Use
questionnaireinstead.
Clinical meaning
Asserts. The reader chose this point on a scale the product defined. The value
goes back through onValueChange, with no total, band, verdict or
interpretation added.
Never read as. A score, a measurement or a result opsinjs derived. A far point is a high self-report, and whether it matters is a clinical judgement this component does not make. It never diagnoses, triages or advises. See Who this is for.
Colour axis. Neither. The scale writes neither data-status nor data-category,
and the chosen point takes the neutral primary fill. A product that decides a rating
matters says so on its own status surface. See
The two colour axes.
Thresholds. None. The point count arrives through points and the end words
through minLabel and maxLabel. opsinjs ships no scale, no anchor words and no
scoring. See Clinical interaction guidelines
and Numbers, units and precision.
Anatomy
ScaleInputThe root, a Base UI RadioGroup with role radiogroup and one roving tab stop, named by label.Controlled bylabelScaleInput.TrackThe row of points. It wraps rather than scrolling sideways and carries no role.Controlled bypointsScaleInput.PointOne point, a Base UI Radio with role radio and aria-checked, floored at a 44pt target.Controlled byvalueScaleInput.PointLabelThe point's number: the visible label and most of what a screen reader reads.ScaleInput.AnchorOne end word under the first or last point, folded into that point's accessible name.Controlled byminLabel
- ScaleInput 1
- ScaleInput.Track 1
- ScaleInput.Point 2..nOne per point, numbered 1 to points.
- ScaleInput.PointLabel 1
- ScaleInput.Point 2..nOne per point, numbered 1 to points.
- ScaleInput.Anchor 0..2Only when minLabel or maxLabel is supplied.
- ScaleInput.Track 1
Examples
A rating with end words
Ten points with a word at each end, from label, points, minLabel and maxLabel.
scale-input-rating-with-anchors · base base · style base-lyraOpen under the product themeNo answer yet
value is null, so the scale draws with no point chosen rather than defaulting to
a middle the reader never picked.
scale-input-no-answer-yet · base base · style base-lyraOpen under the product themeContent guidelines
Write label as the thing being rated in the reader's words, "Comfort right now"
rather than "1 to 10". It is the accessible name and the only place the question is
stated. Take the end words from the product's instrument, short and opposite, never
a verdict. Keep the point count fixed.
Do
label="Comfort right now" with end words from the product's instrument. Named question, fixed ends, so answers compare over time.
Don’t
No label, invented end words, a point count that drifts between screens. Nameless radios whose answers cannot be compared.
Accessibility
Audited against WCAG 2.2 AA, in a source pass and a rendered pass. The audit is author-run, not an independent review, and clinical review is pending.
- One tab stop with a roving focus:
role="radiogroup"on the root,role="radio"andaria-checkedon each point, and selection follows focus. - Every point floors its hit area at
--opsin-target-minimumin rem, and the row wraps rather than shrinking a point below its target.
Fixed in this pass. In forced colours the chosen point now repaints to the system
Highlight pair with a Highlight border and forced-color-adjust-none, so selection
stays distinct.
Deferred, and named. The question reaches the radiogroup only as an aria-label,
weaker than a visible label, the recommended enhancement, a decision shared
with combobox. In normal mode the selection carries no glyph, so luminance inversion
and the border carry it without hue. Every contrast pair on the host's ground is
unmeasured, and the report below prints only what has been measured.
| Key | Action | Notes |
|---|---|---|
| Tab | Moves focus into the scale, onto the chosen point | One tab stop. Unanswered, the first point takes it; a disabled point never does. |
| Shift+Tab | Moves focus back out of the scale | A single stop in reverse too. |
| Arrow Right, Arrow Down | Moves to the next point and selects it | Selection follows focus. Wraps last to first. Arrow Left and Right swap in right-to-left. |
| Arrow Left, Arrow Up | Moves to the previous point and selects it | Wraps from the first to the last. |
| Space | Selects the focused point | A point is a role="radio", so Space is its activator. |
| Enter | Does nothing | The primitive cancels Enter, so the scale does not submit a form. |
| Home, End | Not bound | The RadioGroup does not enable them, so they keep the browser default. |
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| body text on the page | light | 101.6 | 17.18:1 | Pass |
| body text on the page | dark | -100.5 | 18.00:1 | Pass |
| body text on a card | light | 104.7 | 17.96:1 | Pass |
| body text on a card | dark | -99.6 | 16.32:1 | Pass |
| body text on the muted ground | light | 98.1 | 16.32:1 | Pass |
| body text on the muted ground | dark | -97.5 | 13.77:1 | Pass |
| secondary text on the page | light | 83.9 | 7.42:1 | Pass |
| secondary text on the page | dark | -80.7 | 13.44:1 | Pass |
| secondary text on a card | light | 87.0 | 7.76:1 | Pass |
| secondary text on a card | dark | -79.8 | 12.19:1 | Pass |
| secondary text on the muted ground | light | 80.4 | 7.05:1 | Pass |
| secondary text on the muted ground | dark | -77.8 | 10.28:1 | Pass |
| a hairline boundary on the page | light | 19.3 | 1.41:1 | Below floor |
| a hairline boundary on the page | dark | -8.2 | 1.90:1 | Below floor |
| a hairline boundary on a card | light | 22.4 | 1.47:1 | Below floor |
| a hairline boundary on a card | dark | -7.3 | 1.72:1 | Below floor |
| a hairline boundary on the muted ground | light | 15.8 | 1.34:1 | Below floor |
| a hairline boundary on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on the page | light | 69.8 | 4.52:1 | Pass |
| a placeholder boundary on the page | dark | -51.0 | 7.62:1 | Pass |
| a placeholder boundary on a card | light | 72.8 | 4.72:1 | Pass |
| a placeholder boundary on a card | dark | -50.1 | 6.91:1 | Pass |
| a placeholder boundary on the muted ground | light | 66.2 | 4.29:1 | Pass |
| a placeholder boundary on the muted ground | dark | -48.1 | 5.83:1 | Pass |
| a placeholder fill on the page | light | 8.3 | 1.18:1 | Below floor |
| a placeholder fill on the page | dark | -8.2 | 1.90:1 | Below floor |
| a placeholder fill on a card | light | 11.4 | 1.23:1 | Below floor |
| a placeholder fill on a card | dark | -7.3 | 1.72:1 | Below floor |
| a placeholder fill on the muted ground | light | 0.0 | 1.12:1 | Below floor |
| a placeholder fill on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on its own fill | light | 58.9 | 3.84:1 | Pass |
| a placeholder boundary on its own fill | dark | -41.5 | 4.01:1 | Below floor |
| the card hairline on the page | light | 69.8 | 4.52:1 | Pass |
| the card hairline on the page | dark | -28.7 | 4.19:1 | Below floor |
| the card hairline on a card | light | 72.8 | 4.72:1 | Pass |
| the card hairline on a card | dark | -27.9 | 3.80:1 | Below floor |
These are the measured token pairs this component draws colour from, not a measurement of the component itself.
Data attributes
| Attribute | Condition | Value |
|---|---|---|
| data-checked, data-unchecked | On each point, stamped by the Base UI Radio | One of the two, by whether the point is the chosen one |
API reference
Prop
Type
Generated from ScaleInputProps in registry/bases/base/scale-input.tsx.
points has no default, and fewer than two renders nothing with a development
warning. value is number | null: null draws nothing chosen, and a number
outside 1 to points does the same and warns. label is required: a
radiogroup with no accessible name is a defect the types cannot prevent.
Related
- RadioGroup is for a fixed set of labelled choices with no ordered ends.
- Textarea is for an answer in the reader's own words.
- Questionnaire is the name recorded for a multi-question instrument opsinjs does not ship.
- Field is the wrapper each rated item of a multi-question instrument sits in.
BodyMap
A neutral, non-realistic body schematic for pointing at where something is. It records region keys and interprets nothing, so the product owns every meaning.
DoseTracker
A record of the medicine doses a product has already logged, drawing each as a fact with a word and a shape, never computing adherence or judging a missed dose.