Primitives

Rating

The rating input — distinct from ScoreBar, which is the output. A native radio group implementing the W3C ARIA APG rating pattern, in Scott O'Hara's styled-radio variant, at WCAG 1.4.11 contrast. The legend is the category name; the hint is what that category measures. Every category requires a score from 1 to 5.

Communication

Information accessibility and clarity of instructions for participants

No rating selected
Merchandising

Availability and quality of event merchandising, including official apparel and accessories

No rating selected
Provisions

Availability and quality of event provisions

No rating selected

Decisions taken from the sources, not invented

01
Product never shows “Without rating”

Every category requires 1–5. The group starts empty; native required then means a score. A mis-tap can be changed to another star. It cannot go back to “no score”, and that is the product rule. The chip stays as a design-system specimen only, so the empty/undo state O'Hara documented can still be tested.

02
The inputs sit over the label at full size, at opacity: 0

Not moved off-screen. Hiding a control off-screen means anyone searching by touch, or hovering with a screen reader announcing as they go, never finds it.

03
The tier word never appears on hover

Revealing text on hover or focus pulls WCAG 1.4.13 (dismissable, hoverable, persistent) into scope and requires an Esc handler. The alternative the sources name is a static location that updates with the selection — our summary line.

Shape, not colour

Filled and empty are two different glyphs — ★ and ☆ — carried by a ::before. In forced-colors mode every colour is replaced by the system palette, and a colour-only fill would make full and empty stars identical. As a text glyph it also inherits currentcolor for free.

Cumulative fill is a property of the group, not of one input, so it is expressed once in the stars utility. :is() equalises specificity between the checked and hover rules — without it the checked rule outranks hover and the preview breaks the moment you hover a filled star.

@utility stars {
  & > label[data-star] { color: var(--color-text-muted); }
  & > label[data-star] .star::before { content: "\2606\FE0E"; } /* ☆ text */
  & > label[data-star]:is(
      :has(input:checked),
      :has(~ label[data-star] input:checked)
    ) {
    color: var(--color-star);                       /* #f59e0b */
    & .star::before { content: "\2605\FE0E"; }     /* ★ text */
  }
}
Native required

With only 1–5 in the group, native required means a score. Do not add an empty-value radio in product: that member would satisfy required without a rating. isRated() still guards controlled state before the first tap.

Display variants

displayWhen
stars · defaultA public survey. The convention is what a tired athlete recognises without reading. The number sits beside each star; the tier word goes to the summary, so the control is never shape alone.
scaleA vertical list, for when the word must be read rather than recalled. Five words do not fit one phone-width row.
compactA row of numbers with the word in sr-only, for dashboard grids where the words live in the column header.

Judgement calls, owned as such

  • The input is never tier-coloured. A green 5 next to a red 1 tells the respondent which answer is correct. That is survey methodology, not an accessibility rule.
  • 44px targets in every density, where WCAG asks 24px at AA and 44px at AAA.
  • No live region. The radio group already announces "option 4 of 5, Very Good, selected". An aria-live on top would say everything twice. The visible summary is for people who receive no announcement at all, and it sits outside aria-describedby.
  • Invalid outlines the group, not each star. Stars have no border to turn red, and framing each one would read as five separate controls.

Design-system specimen

Not for product. O'Hara's “Without rating” chip is the only native way back to an empty radio group after a tap. Keep it here so empty and undo states can be tested; never render it in the shipping form.

Sustainability

Waste, transport and the event's footprint on the venue

React

From @sportingscouter/ui-react. The component adds no visual values of its own — every class comes from the same tv() recipe the design system defines, so the two cannot drift. Do not pass allowUnrated in product.

import { Rating } from '@sportingscouter/ui-react';
import { isRated } from '@sportingscouter/ui';

<Rating
  legend="Communication"
  hint="Information accessibility and clarity of instructions for participants"
  required
  onValueChange={setScore}
/>

{/* legend = category name, hint = what it measures.
    Never a question. Same pair as ScoreBar. */}

{/* number + word, as a vertical list */}
<Rating legend="Provisions" hint="Availability and quality of event provisions" display="scale" required />

{/* numbers in a row, the word sr-only */}
<Rating legend="Merchandising" hint="Availability and quality of event merchandising, including official apparel and accessories" display="compact" required />

{/* Design-system testing only — never in product */}
<Rating legend="Specimen" allowUnrated />

{!isRated(score) && <p>Pick a rating to continue.</p>}
The 1–5 threshold lives once

isRated() is exported from @sportingscouter/ui. It is deliberately not reimplemented in ui-react: the threshold is a product decision, and two copies of it is one too many.

Sporting Scouter Design System
Every token on this site is generated from theme.css.