macbetter

Buttons and controls

Rating

Give a score from a row of stars, or show one.

import { Rating } from "macbetter/rating"

export default function Example() {
  return <Rating aria-label="Your rating" defaultValue={4} />
}

When to use it

Use a rating to collect a quick score, like how much you liked a recipe, or to show an average, like an app's ratings. Five stars is the convention people know; keep it unless your scale means something different.

  • Show the number beside a read-only rating, and how many ratings it's based on.
  • Let people clear their score with deselectable when no score is a valid answer.

Usage

import { Rating } from "macbetter/rating"

Rating draws its own stars. Name it with aria-label or a Field label.

Half stars

step={0.5} splits each star in two. Pressing the first half of a star picks the half score.

import { Rating } from "macbetter/rating"

export default function Example() {
  return <Rating aria-label="Rate this episode" step={0.5} defaultValue={3.5} />
}

Read-only

readOnly shows any score, like an average of 4.6, as an image named by getValueLabel. Its empty stars are solid and quieter, so it doesn't look pressable.

4.6
12,408 Ratings
"use client"

import { Rating } from "macbetter/rating"

export default function Example() {
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
      <span style={{ font: "var(--mb-text-title-2)", fontVariantNumeric: "tabular-nums" }}>4.6</span>
      <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
        <Rating readOnly size="sm" defaultValue={4.6} getValueLabel={(value, max) => `${value} out of ${max} stars`} />
        <span style={{ color: "var(--mb-label-secondary)", font: "var(--mb-text-caption)" }}>12,408 Ratings</span>
      </div>
    </div>
  )
}

Sizes

import { Rating } from "macbetter/rating"

export default function Example() {
  return (
    <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 12 }}>
      <Rating size="sm" aria-label="Rate this song" defaultValue={3} />
      <Rating size="md" aria-label="Rate this album" defaultValue={3} />
      <Rating size="lg" aria-label="Rate this app" defaultValue={3} />
    </div>
  )
}

Controlled

Tap a star to rate
"use client"

import { Rating } from "macbetter/rating"
import { useState } from "react"

const labels = ["Tap a star to rate", "Hated it", "Didn't like it", "It was fine", "Liked it", "Loved it"]

export default function Example() {
  const [score, setScore] = useState<number | null>(null)

  return (
    <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 10 }}>
      <Rating aria-label="Rate this recipe" size="lg" deselectable value={score} onValueChange={setScore} />
      <span style={{ color: "var(--mb-label-secondary)" }}>{labels[score ?? 0]}</span>
    </div>
  )
}

Platform behavior

  • macOS. 16-point stars. Moving the pointer over them previews the score in a lighter accent; moving away shows the chosen score again.
  • iPadOS and iOS. 24-point stars with more room between them. Touch has no preview: a tap picks the score at once, and the star gives a little under the finger.
  • Every platform. Stars fill with the accent from the leading edge, so half and partial stars mirror in right-to-left text.

Accessibility

  • A rating is a radio group: one tab stop, and the arrow keys move and pick a score. Each star's radio is named "3 stars", or by getValueLabel.
  • A read-only rating has the img role and one name for the whole score.
  • The focus ring goes around the focused star.

API

PropTypeDefaultDescription
size"sm" | "md" | "lg""md"Star size.
valuenumber | null—The score. Pass it to control the rating.
defaultValuenumber | null—The score at first.
onValueChange(value, details) => void—Runs when the score wants to change.
maxnumber5How many stars.
stepnumber1The smallest change, as a fraction of a star.
deselectablebooleanfalseLets a press on the checked star clear the score.
readOnlybooleanfalseShows the score as an image.
getValueLabel(value, max) => string—Names a score, for each star and for a read-only rating.
disabledbooleanfalseCan't be focused or changed.
namestring—The name the score submits under.
childrenReactNode—A RatingItem with your own symbol, repeated once per star.

Data attributes

AttributeOnValues
data-sizeRatingsm, md, lg
data-filledRatingItemPresent on stars the score covers whole
data-partialRatingItemPresent on the star the score ends partway through
data-checkedRatingItemPresent on the star the score ends in
data-highlightedRatingItemPresent on stars up to a previewed score
data-readonly, data-disabledBothPresent in those states

CSS variables

VariablemacOSiOSWhat it sets
--mb-rating-size16px24pxStar size
--mb-rating-size-sm / -lg12px / 22px14px / 32pxSmall and large stars
--mb-rating-gap2px4pxSpace between stars
--mb-rating-color--mb-accent--mb-accentFilled stars, set on the rating
--rating-fillFrom ui-sdk: how much of each star is filled, 0 to 1