A rating is a choice from a known scale. A radio group communicates that only one value can be selected. Custom-looking choices still need names, selected state, and a keyboard route. If every choice becomes a separate Tab stop, a long scale forces a person to tab through options they could navigate with arrow keys.
You'll build a service rating with a scale supplied by the fixture. Keep one choice in the Tab order, move the selected value with arrow keys, and support Home and End. Clearing the rating returns to an unanswered state while preserving the controls. Use the same state value for the output and aria-checked attributes. The boundary at the last star exposes implementations that forget to wrap back to the first.
function App(){const [rating,setRating]=useState(0);const refs=useRef([]);function choose(n){setRating(n);refs.current[n-1]?.focus()}function keyboard(e,n){let next=n;if(e.key==='ArrowRight')next=n===f.limit?1:n+1;else if(e.key==='ArrowLeft')next=n===1?f.limit:n-1;else if(e.key==='Home')next=1;else if(e.key==='End')next=f.limit;else return;e.preventDefault();choose(next)}return <><h1>Service feedback</h1><div role="radiogroup" aria-label="Service rating">{Array.from({length:f.limit},(_,i)=>i+1).map(n=><button key={n} ref={el=>{refs.current[n-1]=el}} role="radio" aria-label={n+' stars'} aria-checked={rating===n} tabIndex={rating===n||rating===0&&n===1?0:-1} onKeyDown={e=>keyboard(e,n)} onClick={()=>choose(n)}>{n} stars</button>)}</div><output aria-label="Rating">{rating}</output><button onClick={()=>setRating(0)}>Clear rating</button></>}
Trace the control, state change and accessible output in the completed example.