BetaForms

Rating

A row of marks, given or shown.

Overview

A row of marks, given or shown.

First example

<Rating value={score} onValueChange={setScore} />
<Rating value={4.3} isReadOnly />
<Rating value={score} onValueChange={setScore} precision={0.5}>
<Rating.Icon as={HeartIcon} />
</Rating>

Anatomy

<Rating>
  <Rating.Icon />
  <Rating.Item />
</Rating>

Usage

<Rating value={score} onValueChange={setScore} />

<Rating value={4.3} isReadOnly />

<Rating value={score} onValueChange={setScore} precision={0.5}>
  <Rating.Icon as={HeartIcon} />
</Rating>

Root props

RatingProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
variantRatingVariant | undefinedNo
sizeSize | undefinedNoThe mark's size and the gap between marks.
colorstring | undefinedNoA raw tint (R7) for the filled marks. The unfilled ones keep their neutral ground.
maxnumber | undefinedNoHow many marks.
valuenumber | undefinedNoControlled. Leave it out and the rating keeps the value itself.
defaultValuenumber | undefinedNoThe starting value when uncontrolled.
onValueChange((value: number) => void) | undefinedNo
precisionnumber | undefinedNoThe step a press lands on. `1` is whole marks, `0.5` halves. It governs **input only** — a `value` of any precision displays as it is, so an average of 4.3 fills the fifth mark three tenths of the way whatever this says.
isReadOnlyboolean | undefinedNoA display rather than a control: the marks stop responding and announce themselves as an image showing a value, not as buttons.
isDisabledboolean | undefinedNo
childrenReactNodeNo**The glyph**, drawn once per mark, in each of the two layers a mark has: ```tsx <Rating max={5} value={value} onValueChange={setValue}> <Rating.Icon as={HeartIcon} /> </Rating> ``` One element rather than a row of them, because the number of marks is `max` — data, not markup — and five hand-written marks is a row that disagrees with the prop the moment either changes. Left out, the mark is a star. A label beside the row is **not** in here: that is a `Typography` in a `Row`, and it is the screen's rather than the rating's (R1).
styleStyleProp<ViewStyle>No

Slots

RatingIconProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

This slot adds nothing to the props of its React Native node.

RatingItemProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
indexnumberYesWhich mark this is, counted from zero.
styleStyleProp<ViewStyle> | ((state: PressableStateCallbackType) => StyleProp<ViewStyle>)NoR9 — `Pressable`'s function form as much as an object or an array.
childrenReactNodeNo

Variants, sizes and colour

Appearance

Three variants, naming which colour a filled mark takes: primary the accent, secondary the neutral foreground, tertiary the page's own foreground for a rating on a coloured card.

The amber every store uses is not a token, because it is not a role the theme has an opinion about — it is color="#f59e0b", which is what a raw tint is for (R7). warning is deliberately not offered in its place: it is a status, and a four-star review is not a warning.

The unfilled marks read the neutral fill token rather than a faint version of the filled colour. A pale tint of the accent under a row of stars looks like a control that half failed to load; a neutral one looks like a mark that is simply not given yet. It is the same choice the Carousel's inactive dots make, and it is why a raw color reaches the filled marks only.

sizemarkgap
xs142
sm162
md204
lg244

No width in the recipe. A mark is as wide as the glyph in it, which is what keeps a star and a heart the same row without either being told how wide the other is — and the mark's width is its type size, because a glyph is text and the clip over it has to be measured in the unit the glyph was set in.

The two layers take the same size. A fill layer one point off would show a rim of the layer beneath it along every edge.

Accessibility

The marks carry the role, and which role depends on the job: button when it is an input, image when isReadOnly makes it a display — five buttons that do nothing is worse than one picture that says the value, and the read-only row announces "4.3 out of 5" on the row itself.

The root carries no role. A role there would have a screen reader describe the box that holds the marks as a second thing.

A mark carries no press feedback overlay: the mark filling is the answer to the press, and a wash over a glyph that is already changing colour reads as two things happening to it.

Migration from legacy

This component declares no rule of its own. The migration guide covers variants, colours and slots.

Implementation notes

One component for the input and the average

The fill of each mark is a fraction, not a boolean, so a value of 4.3 shows three tenths of the fifth mark. A boolean per mark would have had to round it — and rounding is exactly what an average must not do.

precision governs input only: it is the step a press lands on, and a value of any precision displays as it is whatever that says.

The row is data, not markup

max says how many marks there are, so the root renders them. Five hand-written marks is a row that disagrees with the prop the moment either changes.

children is therefore the glyph — written once, drawn once per mark:

<Rating>
  <Rating.Icon as={FlameIcon} />
</Rating>

A mark is the same glyph twice

The neutral one sits in the flow and decides how wide the mark is. The filled one is pinned over it inside a clip whose width is the fraction given, so the glyph is cut part-way through rather than swapped for a different one.

Which layer an instance is in comes from the layer, not from a prop, which is what lets the glyph be written once: Rating.Icon reads its own colour from where it was placed. A tone prop would have made the caller write the glyph twice and keep the two in step.

An empty mark mounts one glyph rather than two — a clip of width zero is a node per mark for every mark not yet given.

The default mark is in both layers

Never a hollow under a solid one. A solid pair is what every store's rating looks like, and (U+2606) is not in Android's system face, so the empty half of the row would render as a box on the platform where most of it is read. (U+2605) is present on both.

It is a character rather than an SVG, so the component needs no react-native-svg for the one glyph it draws itself.

The press reads where it landed

locationX over the mark's width is the fraction the finger meant, and it rounds up to the nearest precision step. That is the only rounding that matches the gesture: a tap anywhere in the first mark is one star rather than zero, and with precision={0.5} the left half of the third mark is 2.5 while the right half is 3.

Rounding to nearest would make the first sliver of every mark select the one before it — so the leftmost strip of the row would silently rate zero.

What is not here

No label. A "4,3 out of 5" beside the row is a Typography in a Row — the screen's, not the rating's (R1). The moment it were a slot it would need an alignment, a format and a language.

No isClearable. Tapping the mark you already chose to go back to nothing is a gesture half the world's rating inputs have and half do not, so it is not a default either way. A caller who wants it holds the value and compares, which is three words and says which behaviour they picked:

<Rating value={score} onValueChange={next => setScore(next === score ? 0 : next)} />

See also

  • Slider — a continuous value where this is a small number of discrete marks.