Rating
A score, as a row of stars. Left choosable it is a radio group; set to readOnly it becomes a single picture reporting an average.
import { Rating } from 'neba';
<Rating defaultValue={4} />;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | — | How much is rated. Use with onValueChange for a controlled Rating |
| defaultValue | number | 0 | Where an uncontrolled Rating starts |
| onValueChange | (value: number) => void | — | Called with the new score. 0 is what a cleared Rating reports |
| count | number | 5 | How many stars there are, and therefore the highest score |
| precision | number | 1 | The smallest step that can be chosen: 0.5 gives half stars. It bounds what a reader can pick and nothing else: a value of 4.3 is drawn as 4.3 at every precision |
| icon | ReactNode | — | The glyph a filled star is drawn with |
| emptyIcon | ReactNode | — | And the one an empty star is drawn with. Has to be the same shape |
| clearable | boolean | true | Choosing the score that is already chosen clears it back to 0 |
| readOnly | boolean | false | Shows the score without letting it be changed: an average, somebody else’s rating. The inputs go and one role="img" is left; the one readOnly in the library that does not drain the saturation |
| disabled | boolean | false | Unavailable. Drops the colour family for neutral grey |
| name | string | — | Identifies the value when a form is submitted |
| required | boolean | false | A form will not submit until a star has been chosen |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Height and type scale |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'warning' | Semantic colour role. warning by default(the amber a star is expected to be) which makes this the one component whose default colour is chosen by what the object is |
| locale | string | 'en' | Which language the accessible names are written in: a BCP 47 tag. Unsupported tags fall back to English |
| label | string | — | Names the whole control. Defaults to the locale’s word for "Rating" |
| valueLabel | (value: number, count: number) => string | — | What one star, and the whole control once it is read only, is called. Defaults to the locale’s way of saying "3 out of 5" |
Every other <div> attribute passes through to the root, except onChange: the change worth listening for is onValueChange.
The shared axes (size color) are defined in prop conventions.
Examples
count, precision
count is how many stars there are and therefore the highest score. precision is the smallest step that can be chosen: at 0.5 each star is split into two hit areas and half stars can be picked.
precision bounds what can be chosen and nothing else. A value of 4.3 is drawn as four stars and a third at every precision: an average is not a choice, and rounding it to the nearest half would be reporting a different number from the one it was handed.
import { useState } from 'react';
import { Rating, Typography } from 'neba';
export default function RatingPrecision() {
const [score, setScore] = useState(3.5);
return (
<div className="flex flex-col items-center gap-3">
<Rating value={score} onValueChange={setScore} precision={0.5} size="lg" />
<Typography level="caption" className="text-(--neba-muted-fg)">
{score || 'nothing'} chosen
</Typography>
<Rating defaultValue={7} count={10} precision={0.5} size="sm" label="Out of ten" />
</div>
);
}readOnly
readOnly is a picture rather than a control. No inputs are rendered at all and one role="img" carries the score as a sentence, so a star display never leaves twenty tab stops on a page that was reporting a number.
It is also the one readOnly in the library that does not drain the saturation: this is not a control being held still, it is the value itself, and a row of grey stars would say the score was unavailable.
import { Rating, Typography } from 'neba';
const reviews = [
{ name: 'Cold brew concentrate', score: 4.6, count: 1284 },
{ name: 'Ceramic pour-over cone', score: 3.2, count: 96 },
{ name: 'Paper filters, 100 pack', score: 5, count: 12 }
];
export default function RatingReadonly() {
return (
<div className="flex w-full max-w-sm flex-col gap-3">
{reviews.map((review) => (
<div key={review.name} className="flex items-center justify-between gap-4">
<Typography level="body">{review.name}</Typography>
<div className="flex shrink-0 items-center gap-2">
<Rating value={review.score} readOnly size="sm" />
<Typography level="caption" className="text-(--neba-muted-fg)">
{review.score} ({review.count})
</Typography>
</div>
</div>
))}
</div>
);
}size, color
size takes the height of one star from the standalone-glyph ladder. color is the one place in the library where the default is warning: the amber a star is expected to be.
import { Rating } from 'neba';
export default function RatingAppearance() {
return (
<div className="flex flex-col items-center gap-4">
<div className="flex flex-col items-center gap-2">
{(['xs', 'sm', 'md', 'lg', 'xl'] as const).map((size) => (
<Rating key={size} size={size} defaultValue={3} label={`Rating, ${size}`} />
))}
</div>
<div className="flex flex-wrap items-center justify-center gap-4">
{(['primary', 'success', 'danger', 'info'] as const).map((color) => (
<Rating key={color} color={color} defaultValue={4} label={`Rating, ${color}`} />
))}
</div>
</div>
);
}icon, emptyIcon
Both glyphs can be replaced. The filled copy is laid over the empty one and clipped to a percentage of the width, so the two have to be the same shape for a half star to land on the outline underneath it.
import { Rating } from 'neba';
function HeartIcon() {
return (
<svg viewBox="0 0 16 16" fill="currentColor">
<path d="M8 14S1.5 10 1.5 5.75A3.25 3.25 0 0 1 8 4.4a3.25 3.25 0 0 1 6.5 1.35C14.5 10 8 14 8 14Z" />
</svg>
);
}
function HeartOutlineIcon() {
return (
<svg viewBox="0 0 16 16" fill="none">
<path
d="M8 14S1.5 10 1.5 5.75A3.25 3.25 0 0 1 8 4.4a3.25 3.25 0 0 1 6.5 1.35C14.5 10 8 14 8 14Z"
stroke="currentColor"
strokeWidth="1.25"
strokeLinejoin="round"
/>
</svg>
);
}
export default function RatingIcons() {
return (
<Rating
icon={<HeartIcon />}
emptyIcon={<HeartOutlineIcon />}
color="danger"
size="lg"
defaultValue={3}
label="How much did you like it?"
/>
);
}clearable, disabled
Choosing the star that is already chosen clears the score back to 0. clearable={false} stops that.
import { Rating, Typography } from 'neba';
export default function RatingStates() {
return (
<div className="flex flex-col items-center gap-4">
<div className="flex flex-col items-center gap-1">
<Rating defaultValue={3} clearable={false} />
<Typography level="caption" className="text-(--neba-muted-fg)">
clearable={'{false}'} — a score, once given, stays given
</Typography>
</div>
<div className="flex flex-col items-center gap-1">
<Rating defaultValue={2} disabled />
<Typography level="caption" className="text-(--neba-muted-fg)">
disabled
</Typography>
</div>
</div>
);
}locale, label, valueLabel
The only words this component invents are its accessible names, and locale decides their language. It takes a BCP 47 tag such as ko, pt-BR or zh-Hant; unsupported tags fall back to English. label names the group and valueLabel writes both one star's name and the read-only sentence.
<Rating locale="ko" />
<Rating label="How was the order?" valueLabel={(value, count) => `${value}/${count} stars`} />In a form
Give it a name and the radios are submitted under it. required stops the form until a star has been chosen.
<form>
<Rating name="score" required />
</form>Accessibility
- A choosable Rating is a
role="radiogroup"built out of real<input type="radio">s: one tab stop for the row, arrow keys within it,aria-checkedon the one that is taken, and a value in a form submission. - Each star is read out as "3 out of 5" rather than as "3 stars", because a count of stars is a plural in most languages and a fraction in none of them.
readOnlyremoves every input and leaves a singlerole="img".- Set
localeso the names are read in the page's language, or write them yourself withlabelandvalueLabel.