Rating
Read-only stars are server HTML and ship no script. Only an editable rating hydrates, as a group of native radios.
split - size not measured yet
Rating
split - size not measured yetRead-only stars are server HTML and ship no script. Only an editable rating hydrates, as a group of native radios.
Try it
value
max
size
readOnly
defaultValue
disabled
The code
rating.tsx
import { Rating } from 'topclassui/rating'
import type { RatingDesignProps } from 'topclassui/rating'
/** The site's Try it stage: each schema prop arrives from a control, and an absent one keeps its default. */
export default function Demo(props: RatingDesignProps = {}) {
const { readOnly, defaultValue, disabled, ...shown } = props
if (readOnly === false) {
return <Rating defaultValue={defaultValue ?? 3} disabled={disabled} {...shown} readOnly={false} aria-label="Rate this course" />
}
return <Rating value={3.5} {...shown} />
}
value alone renders stars on the server; only readOnly={false} ships the client half.
Every state
6 statesImport from the package
topclassui/rating
import { Rating } from 'topclassui/rating'Install once:npm i topclassui@beta zod
Examples
- Read-only is the default:
valueis clamped to 0 andmaxand drawn to the nearest half star. readOnly={false}renders native radios, one per star, named "1 star" to "5 stars".- With a
name, the checked step, 1 tomax, is submitted before any script runs. A form reset goes back to the served step and callsonValueChange. valuewithonValueChangeneeds a 'use client' file: a server page cannot pass a function.defaultValueworks from a server page.maxis 1 to 10, 5 by default. Tenxlstars are 436px, so on a phone the row wraps rather than scroll the page.
read-only
128 reviews
read-only.tsx
import { Rating } from 'topclassui/rating'
/** Stars that only show a value: server HTML, no script. 3.5 draws half a star, and 3.7 rounds to it. */
export default function Demo() {
return (
<div className="flex flex-col items-start gap-3">
<Rating value={0} />
<Rating value={3} />
<Rating value={5} />
<Rating value={3.5} />
<p className="flex flex-wrap items-center gap-2">
<Rating value={3.7} />
<span className="text-sm text-muted">128 reviews</span>
</p>
</div>
)
}
editable
Rate this course
editable.tsx
import { Rating } from 'topclassui/rating'
/** readOnly={false}: a radio group of five, the only part that hydrates. The arrows move and
* select, and the pointer fills up to the star it is on. A server page can render it as it is. */
export default function Demo() {
return (
<div className="flex flex-col items-start gap-2">
<h2 id="rating-editable-title" className="text-sm font-medium">Rate this course</h2>
<Rating readOnly={false} defaultValue={3} aria-labelledby="rating-editable-title" />
</div>
)
}
in-form
in-form.tsx
import { Button } from 'topclassui/button'
import { Rating } from 'topclassui/rating'
/** Each radio submits its step under name, so the form works before any script runs. Reset puts
* the stars back where the server drew them. */
export default function Demo() {
return (
<form className="flex flex-col items-start gap-3">
<h2 id="rating-form-title" className="text-sm font-medium">How was your stay?</h2>
<Rating readOnly={false} name="stay" defaultValue={4} aria-labelledby="rating-form-title" />
<div className="flex flex-wrap gap-2">
<Button type="submit">Send</Button>
<Button type="reset" variant="outline">Reset</Button>
</div>
</form>
)
}
controlled
Chosen: 2 of 5
controlled.tsx
'use client'
import { useState } from 'react'
import { Button } from 'topclassui/button'
import { RatingInput } from 'topclassui/rating'
/** RatingInput takes onValueChange, so a client file renders it: a Server Component page cannot pass a function. */
export default function Demo() {
const [value, setValue] = useState(2)
return (
<div className="flex flex-col items-start gap-3">
<RatingInput value={value} onValueChange={setValue} aria-label="Delivery" />
<p className="text-sm" data-chosen={value}>Chosen: {value} of 5</p>
<Button variant="outline" onClick={() => { setValue(0) }}>Clear</Button>
</div>
)
}
disabled
disabled.tsx
import { Rating } from 'topclassui/rating'
/** Every radio is disabled: no key, no click, no hover fill, and nothing is submitted. */
export default function Demo() {
return <Rating readOnly={false} disabled defaultValue={2} aria-label="Locked rating" />
}
sizes
sizes.tsx
import { Rating } from 'topclassui/rating'
const SIZES = ['xs', 'sm', 'md', 'lg', 'xl'] as const
/** The star follows the icon scale, 16 to 40 px. custom draws it at 1em, so the text size sets it.
* An editing step never drops under 24 px, even at xs. */
export default function Demo() {
return (
<div className="flex flex-col items-start gap-3">
{SIZES.map((size) => <Rating key={size} size={size} value={3.5} />)}
<Rating size="custom" value={2} className="text-3xl" />
<Rating readOnly={false} size="xs" defaultValue={2} aria-label="Extra small" />
<Rating readOnly={false} size="custom" defaultValue={2} aria-label="Your own size" className="text-3xl" />
</div>
)
}
max-ten
max-ten.tsx
import { Rating } from 'topclassui/rating'
/** Ten stars at xl, the widest a rating gets: 436 px, so on a phone the row wraps rather than
* scrolling the page. */
export default function Demo() {
return (
<div className="flex flex-col items-start gap-4">
<Rating max={10} size="xl" value={7.5} />
<Rating readOnly={false} max={10} size="xl" defaultValue={8} aria-label="Out of ten" />
</div>
)
}
In Arabic, right to left
قيّم هذه الدورة
rtl.tsx
import { getLabels } from 'topclassui/labels/get-labels'
import { TopClassProvider } from 'topclassui/provider'
import { Rating } from 'topclassui/rating'
const arabic = getLabels('ar')
/** Arabic, inside TopClassProvider as the root layout would mount it. The first star is at the
* right, and half a star fills its right half. */
export default function Demo() {
return (
<TopClassProvider direction="rtl" locale="ar">
<div dir="rtl" lang="ar" className="flex flex-col items-start gap-3">
<Rating value={3.5} {...arabic} />
<h2 id="rating-rtl-title" className="text-sm font-medium">قيّم هذه الدورة</h2>
<Rating readOnly={false} defaultValue={2} aria-labelledby="rating-rtl-title" {...arabic} />
</div>
</TopClassProvider>
)
}
Props
Keyboard and accessibility
- Read-only, it is one image named "4.5 out of 5"; pass
aria-labelto say more, as in "4.3 from 128 reviews". - Editable, the group needs
aria-labeloraria-labelledby. Each star is at least 24px to press, and 44px under a touch pointer. - A filled star is warning-on-soft, 3:1 or more on every surface; under forced colours a filled star stays solid and an empty one an outline.
Right to left
The first star is at the right, and half a star fills its right half. Pass the Arabic labels and locale for Arabic names.
Server and client
Renders on the server; the interactive part is a client component that ships only when it is used.
Files: index.tsx, client.tsx.
On the chip: split - size not measured yet.