opsinjs
ComponentsHealth input

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

Theme
Density
Text
Status
scale-input · base base · style base-lyraOpen under the product theme

Installation

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 textarea instead.
  • The options are a fixed set of labelled choices with no end word and no ordering.Use radio-group instead.
  • A multi-question instrument where several rated items add up under one heading.Use questionnaire instead.

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

  1. ScaleInputThe root, a Base UI RadioGroup with role radiogroup and one roving tab stop, named by label.Controlled by label
  2. ScaleInput.TrackThe row of points. It wraps rather than scrolling sideways and carries no role.Controlled by points
  3. ScaleInput.PointOne point, a Base UI Radio with role radio and aria-checked, floored at a 44pt target.Controlled by value
  4. ScaleInput.PointLabelThe point's number: the visible label and most of what a screen reader reads.
  5. ScaleInput.AnchorOne end word under the first or last point, folded into that point's accessible name.Controlled by minLabel
  • ScaleInput 1
    • ScaleInput.Track 1
      • ScaleInput.Point 2..nOne per point, numbered 1 to points.
        • ScaleInput.PointLabel 1
    • ScaleInput.Anchor 0..2Only when minLabel or maxLabel is supplied.

Examples

A rating with end words

Ten points with a word at each end, from label, points, minLabel and maxLabel.

Theme
Density
Text
Status
scale-input-rating-with-anchors · base base · style base-lyraOpen under the product theme

No answer yet

value is null, so the scale draws with no point chosen rather than defaulting to a middle the reader never picked.

Theme
Density
Text
Status
scale-input-no-answer-yet · base base · style base-lyraOpen under the product theme

Content 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" and aria-checked on each point, and selection follows focus.
  • Every point floors its hit area at --opsin-target-minimum in 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.

KeyActionNotes
TabMoves focus into the scale, onto the chosen pointOne tab stop. Unanswered, the first point takes it; a disabled point never does.
Shift+TabMoves focus back out of the scaleA single stop in reverse too.
Arrow Right, Arrow DownMoves to the next point and selects itSelection follows focus. Wraps last to first. Arrow Left and Right swap in right-to-left.
Arrow Left, Arrow UpMoves to the previous point and selects itWraps from the first to the last.
SpaceSelects the focused pointA point is a role="radio", so Space is its activator.
EnterDoes nothingThe primitive cancels Enter, so the scale does not submit a form.
Home, EndNot boundThe RadioGroup does not enable them, so they keep the browser default.
PairThemeAPCA LcWCAG 2.2Floor
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

Data attributes

AttributeConditionValue
data-checked, data-uncheckedOn each point, stamped by the Base UI RadioOne 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.

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

On this page