Rating
Choose a whole-number score with registered radio semantics, separate hover preview and recoverable feedback outcomes.
A score is a bounded opinion
Use Rating for a meaningful bounded ordinal score when people understand what its endpoints mean. Name the question and explain the scale. Use Radio for choices with different semantic labels, Slider for a continuous quantity and plain text for an aggregate statistic.
A usefulness score is not evidence that language content is correct, permission granted or a qualified review complete. The example is English-only local opinion feedback; it has no authority, authentication or durable-review claim.
Anatomy and visual specifications
| Part | Contract |
|---|---|
| Group | Native div/ref backed by the installed radio-group primitive. Caller gives a meaningful label/description and form name; stable internal radio registration belongs to the primitive. |
| Score option | Installed radio span with hidden native radio input. At least 44px × 44px target, 8px padding and 2px group gap. Targets wrap when the scale does not fit inline. |
| Icon sizes | sm=16px, md=20px, lg=24px. Icons are decorative; size changes do not shrink input targets. |
| Filled / checked | Cumulative fill previews or represents the score; aria-checked marks exactly the committed option. An inset 2px selected outline distinguishes checked state from hover fill. |
| Focus / contrast | Independent 2px outer ring and no transform/color transition. Empty shape uses muted-foreground and fill uses foreground; forced colors retain checked/focus/unavailable cues. |
| Draft / requested / applied | Current selection is a draft. Native form capture starts a request; only explicit completion updates the visible local result. |
Actual feedback and native form values
Choose a usefulness score and request local feedback. Native FormData contains the whole-number score, not the hover preview. A second uncontrolled group is registered independently. Failure/cancellation retain both the selected score and the loaded result; retry is explicit.
Clear makes the controlled group empty, while external restoration sets a real draft. Five/ten-choice controls reconcile a score that no longer fits. Disabled omission differs from read-only registration. The example changes actual page-memory feedback text, without remote writes or durable saving.
Draft score: none.
Loaded local feedback summary
No local score applied.
No native submission.
No local feedback applied.
'use client';
import {useRef,useState,type CSSProperties} from 'react';
import {Rating,Button} from '@mobtranslate/ui';
const action:CSSProperties={minWidth:0,maxWidth:'100%',minHeight:44,height:'auto',whiteSpace:'normal',overflowWrap:'anywhere'};
export function RatingFeedbackExample(){
const [score,setScore]=useState(0);
const [size,setSize]=useState<'sm'|'md'|'lg'>('md');
const [maximum,setMaximum]=useState(5);
const [disabled,setDisabled]=useState(false);
const [readOnly,setReadOnly]=useState(false);
const [rtl,setRtl]=useState(false);
const [error,setError]=useState('');
const [requested,setRequested]=useState<number | null>(null);
const [applied,setApplied]=useState<number | null>(null);
const [payload,setPayload]=useState('No native submission.');
const [status,setStatus]=useState('No local feedback applied.');
const rating=useRef<HTMLDivElement>(null);
const heading=useRef<HTMLHeadingElement>(null);
const pending=requested!==null;
function focusRating(){(rating.current?.querySelector<HTMLElement>('[role=radio][aria-checked=true]')??rating.current?.querySelector<HTMLElement>('[role=radio]'))?.focus();}
function outcome(result:'complete'|'fail'|'cancel'){
if(requested===null)return;
if(result==='complete'){setApplied(requested);setStatus('Actual local feedback summary updated. No remote review or durable save.');}
else setStatus(`Local feedback ${result==='fail'?'failed':'cancelled'}. Draft and loaded summary remain; submit again to retry.`);
setRequested(null);heading.current?.focus();
}
return <div data-rating-lab className="space-y-4"><div className="flex flex-wrap gap-3"><Button style={action} type="button" variant="outline" disabled={pending} aria-pressed={disabled} onClick={()=>setDisabled(v=>!v)}>Disable local ratings</Button><Button style={action} type="button" variant="outline" disabled={pending} aria-pressed={readOnly} onClick={()=>setReadOnly(v=>!v)}>Make local ratings read only</Button><Button style={action} type="button" variant="outline" aria-pressed={rtl} onClick={()=>setRtl(v=>!v)}>Use RTL rating choices</Button><Button style={action} type="button" variant="outline" disabled={pending} onClick={()=>{setMaximum(5);setScore(3);setDisabled(false);setReadOnly(false);setError('');}}>Restore local rating draft</Button></div>
<div className="flex flex-wrap gap-3"><label>Star icon size <select data-rating-size value={size} onChange={event=>setSize(event.target.value as 'sm'|'md'|'lg')} className="mt-input" disabled={pending}><option value="sm">Small</option><option value="md">Medium</option><option value="lg">Large</option></select></label><label>Available score count <select data-rating-max value={maximum} onChange={event=>{const next=Number(event.target.value);setMaximum(next);setScore(current=>current>next?0:current);}} className="mt-input" disabled={pending}><option value={5}>Five choices</option><option value={10}>Ten choices</option></select></label></div>
<form noValidate className="space-y-4" onSubmit={event=>{event.preventDefault();if(pending||disabled)return;if(score<1||score>maximum){setError('Choose a score before requesting local feedback.');focusRating();return;}const data=new FormData(event.currentTarget);setPayload(JSON.stringify([...data.entries()]));setRequested(Number(data.get('score')));setError('');setStatus('Feedback requested; loaded summary remains until an explicit outcome.');heading.current?.focus();}}>
<p id="rating-local-label" className="font-semibold">English-only example usefulness</p><Rating ref={rating} aria-labelledby="rating-local-label" aria-describedby={error?'rating-help rating-error':'rating-help'} aria-invalid={!!error} name="score" required value={score} onChange={next=>{setScore(next);setError('');}} max={maximum} size={size} disabled={disabled} readOnly={readOnly||pending} dir={rtl?'rtl':'ltr'} getItemLabel={(value,max)=>`${value} of ${max} usefulness points`} />
<p id="rating-help">Choose one whole score. Hover previews fill only; it does not change the checked value or native submission.</p>{error&&<p id="rating-error" role="alert">{error}</p>}
<p id="rating-independent-label">Independent optional usefulness score</p><Rating name="otherScore" defaultValue={2} aria-labelledby="rating-independent-label" disabled={disabled} readOnly={readOnly||pending} dir={rtl?'rtl':'ltr'} />
<div className="flex flex-wrap gap-3"><Button style={action} type="submit" disabled={disabled||pending}>Request local rating feedback</Button><Button style={action} type="button" variant="outline" disabled={disabled||readOnly||pending} onClick={()=>{setScore(0);setError('');focusRating();}}>Clear local score</Button></div>
</form><p data-rating-score>Draft score: {score||'none'}.</p><h3 ref={heading} tabIndex={-1} data-rating-heading className="font-semibold">Loaded local feedback summary</h3><p data-rating-applied>{applied===null?'No local score applied.':`Actual local score: ${applied}.`}</p><p data-rating-payload style={{overflowWrap:'anywhere'}}>{payload}</p>
{pending&&<div className="flex flex-wrap gap-3"><Button style={action} type="button" onClick={()=>outcome('complete')}>Complete local rating</Button><Button style={action} type="button" variant="outline" onClick={()=>outcome('fail')}>Fail local rating</Button><Button style={action} type="button" variant="outline" onClick={()=>outcome('cancel')}>Cancel local rating</Button></div>}<p role="status" data-rating-status>{status}</p>
</div>;
}
Complete public API
| Prop | Ownership |
|---|---|
| value / onChange | Controlled whole-number score and number callback. 0 means no selection; actual selectable scores are 1 through max. Reflect proposals in the parent. Callback is not a DOM event or submitted result. |
| defaultValue | Uncontrolled initial score, default 0. Later changes are not an external reset; do not switch control modes. Wrapper owns this initial state and delegates registered interaction to the primitive. |
| max | Positive safe integer choice count, default 5. Malformed counts fall back to five; validate dynamic product configuration upstream. Large scales should use a more readable input. |
| Invalid external value | Fractional/nonfinite/out-of-range values render unselected and mark data-invalid-value/initial aria-invalid. Reconcile stale dynamic data visibly; do not silently pretend a rounded score was chosen. |
| size | sm, md or lg, default md. Controls decorative icon dimensions while targets remain 44px. |
| name / required | Installed native radio registration/required state. Empty groups submit no selected value. The consuming form also validates its current allowed scale. |
| disabled / readOnly | Prevent mutation through the primitive; disabled groups omit native entries. Read-only groups retain selected entries and discoverable current state. |
| getItemLabel(value,max) | Localizable accessible option label. Default uses English star counts; pass useful scale-specific labels and locale-aware numbers when needed. |
| id / aria-label / aria-labelledby / aria-describedby | Native group identity/name/help/error attributes. Default accessible label is Rating, but applications should supply the actual question. |
| dir | Explicit ltr/rtl reaches DOM and the installed DirectionProvider, keeping arrow direction aligned. Without it, use the surrounding installed provider for keyboard direction; dir=auto alone is not a keyboard-direction policy. |
| className / style / native handlers / ref | Native div attributes and HTMLDivElement ref. Pointer-leave and key handlers compose with preview cleanup; they do not replace primitive selection behavior. |
Keyboard, pointer and focus
- Tab reaches the group’s registered current/initial radio stop. Arrow keys move focus and selection through the installed radio model; test actual direction, wrapping and readonly behavior. Space selects the focused choice.
- Hover by a mouse previews cumulative fill without changing aria-checked, callback or form data. Leaving the group and keyboard interaction clear the preview. Touch does not rely on hover to select or discover meaning.
- Every star has a meaningful accessible option name; the graphic itself is decorative. Visible supporting text explains the scale without requiring a tooltip.
- Required correction focuses a surviving actual radio. Pending preserves readable selected values, rejects duplicate submit and leaves explicit outcomes reachable outside the group.
- Clear is a separate action, not a hidden zero-star radio. Controlled/native form reset policy belongs to the parent; do not infer reset from a later defaultValue prop.
States and failure recovery
| State | Verify |
|---|---|
| Empty / required | No checked choice or submitted score. Linked error focuses an actual group option and preserves the optional score. |
| Hover / selected / focused | Three distinct states: preview fill, committed checked outline and keyboard focus ring. Preview must not become saved feedback. |
| Disabled / read-only | No mutation. Native omission versus retained registration is explicit; an independent restore path remains reachable. |
| External restore / changed scale | Actual chosen identity follows controlled values. The fixture resets an out-of-range draft to empty when reducing max. |
| Requested / failed / cancelled | Capture the actual native score, keep loaded text unchanged and retain useful selection for retry. |
| Completed | Only completion changes actual feedback text and moves focus to a surviving result heading. |
| Multiple groups / large scale | Independent names/relationships and own state. More choices wrap; validate readability and the meaning of the scale before choosing a large max. |
Migration and verification
The previous component rendered ordinary buttons with radio roles but no arrow behavior or native value registration, despite the guide’s keyboard claim. Selection now uses the installed radio primitives; readonly remains distinct from disabled rather than treating every display as unavailable.
- Run test:rating, checked-source/token freshness, lint, UI/styleguide types, production build and complete route/fragment crawl. Verify real values/callbacks, keyboard/pointer/hover separation, required correction, independent groups and actual outcomes.
- Check both themes, sizes, narrow wrapping, RTL and forced colors. Spoken assistive-technology/device behavior and every consuming feedback service need separate evidence.
- Do not claim a rating certifies source quality, accessibility or qualified language review. Preserve evidence and review status independently from opinion feedback.