Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

InputsReact

Rating

A row of stars that sets a rating from 1 to max (5 by default).

Rendered by React 19.3.1

Loading React…
import { Rating } from '@hamur/react/rating'

const stars = [1, 2, 3, 4, 5]

export default function RatingExample() {
  return (
    <div className="field">
      <span id="rating-recipe-label" className="rating-label">
        Rate this recipe
      </span>
      <Rating.Root
        className="rating"
        defaultValue={3}
        aria-labelledby="rating-recipe-label"
      >
        {stars.map((star) => (
          <Rating.Item key={star} value={star} className="rating-item" />
        ))}
        <Rating.Value className="rating-value" />
        <Rating.Clear className="button rating-clear">
          Clear
        </Rating.Clear>
      </Rating.Root>
    </div>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<Rating.Root>
  <Rating.Item />
  <Rating.Value />
  <Rating.Clear />
</Rating.Root>

Examples

Half

step of 0.5 from 2.5: the pointer previews the half star it is over, Value shows the preview in italics, and the text under it says the value and preview.

RatingHalf.tsx
import * as React from 'react'
import { Rating } from '@hamur/react/rating'

const stars = [1, 2, 3, 4, 5]

export default function RatingHalfExample() {
  const [value, setValue] = React.useState<number | null>(2.5)
  const [preview, setPreview] = React.useState<number | null>(null)
  return (
    <div className="field">
      <span id="rating-half-label" className="rating-label">
        Sound quality
      </span>
      <Rating.Root
        className="rating"
        step={0.5}
        value={value}
        onValueChange={setValue}
        onPreviewValueChange={setPreview}
        aria-labelledby="rating-half-label"
      >
        {stars.map((star) => (
          <Rating.Item key={star} value={star} className="rating-item" />
        ))}
        <Rating.Value className="rating-value" />
      </Rating.Root>
      <p className="muted">
        Value: {value ?? 'none'}. Preview: {preview ?? 'none'}.
      </p>
    </div>
  )
}

Form

A required rating in a Field of a Form: Send without a rating shows "Choose a rating before you send.", and a rated form submits stay=4.

RatingForm.tsx
import * as React from 'react'
import { Field } from '@hamur/react/field'
import { Form } from '@hamur/react/form'
import { Rating } from '@hamur/react/rating'

const stars = [1, 2, 3, 4, 5]

export default function RatingFormExample() {
  const [submitted, setSubmitted] = React.useState('')
  return (
    <Form
      className="stack"
      onFormSubmit={(_values, details) => {
        const form = details.event.target as HTMLFormElement
        setSubmitted([...new FormData(form)].map(([key, value]) => `${key}=${value}`).join('&'))
      }}
    >
      <Field.Root name="stay" className="field">
        <Field.Label className="rating-label">Rate your stay</Field.Label>
        <Rating.Root className="rating" required>
          {stars.map((star) => (
            <Rating.Item key={star} value={star} className="rating-item" />
          ))}
        </Rating.Root>
        <Field.Description className="muted">
          From 1 star (poor) to 5 (excellent).
        </Field.Description>
        <Field.Error className="error" match="valueMissing">
          Choose a rating before you send.
        </Field.Error>
      </Field.Root>
      <button className="button" type="submit">
        Send
      </button>
      {submitted ? <pre className="output">{submitted}</pre> : null}
    </Form>
  )
}

Read Only

readOnly at 4, focusable and submitted, and disabled at 2, out of the Tab order, each with its state in its label.

RatingReadOnly.tsx
import { Rating } from '@hamur/react/rating'

const stars = [1, 2, 3, 4, 5]

export default function RatingReadOnlyExample() {
  return (
    <div className="stack">
      <div className="field">
        <span id="rating-read-only-label" className="rating-label">
          Your last rating (read-only)
        </span>
        <Rating.Root
          className="rating"
          readOnly
          defaultValue={4}
          aria-labelledby="rating-read-only-label"
        >
          {stars.map((star) => (
            <Rating.Item key={star} value={star} className="rating-item" />
          ))}
          <Rating.Value className="rating-value" />
        </Rating.Root>
      </div>
      <div className="field">
        <span id="rating-disabled-label" className="rating-label">
          Delivery (disabled until it arrives)
        </span>
        <Rating.Root
          className="rating"
          disabled
          defaultValue={2}
          aria-labelledby="rating-disabled-label"
        >
          {stars.map((star) => (
            <Rating.Item key={star} value={star} className="rating-item" />
          ))}
          <Rating.Value className="rating-value" />
        </Rating.Root>
      </div>
    </div>
  )
}

Custom

Five faces from "Very bad" to "Very good", each with its own icon and aria-label, in a column with orientation="vertical", right to left through Direction Provider.

RatingCustom.tsx
import { DirectionProvider } from '@hamur/react/direction-provider'
import { Rating } from '@hamur/react/rating'

const faces = [
  { value: 1, label: 'Very bad', mouth: 'M8 17q4-4 8 0' },
  { value: 2, label: 'Bad', mouth: 'M8 16.5q4-2 8 0' },
  { value: 3, label: 'Okay', mouth: 'M8 15.5h8' },
  { value: 4, label: 'Good', mouth: 'M8 14.5q4 2 8 0' },
  { value: 5, label: 'Very good', mouth: 'M8 14q4 4 8 0' },
]

export default function RatingCustomExample() {
  return (
    <DirectionProvider direction="rtl">
      <div dir="rtl" className="field">
        <span id="rating-custom-label" className="rating-label">
          How was your visit?
        </span>
        <Rating.Root
          className="rating rating-faces"
          orientation="vertical"
          defaultValue={4}
          aria-labelledby="rating-custom-label"
        >
          {faces.map((face) => (
            <Rating.Item
              key={face.value}
              value={face.value}
              aria-label={face.label}
              className="rating-face"
            >
              <svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
                <circle cx="12" cy="12" r="9.5" />
                <circle cx="9" cy="10" r="1" className="rating-face-eye" />
                <circle cx="15" cy="10" r="1" className="rating-face-eye" />
                <path d={face.mouth} />
              </svg>
              {face.label}
            </Rating.Item>
          ))}
        </Rating.Root>
      </div>
    </DirectionProvider>
  )
}

API reference

25 props across 4 parts, in React syntax. className, style, render are listed once, under Shared props.

ClearRating.Clear

Clears the rating. Place it after Rating.Root's stars, outside the group: a Tab stop inside a radio group is skipped by arrow key users. Renders a <button>.

Clear props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
focusableWhenDisabledWhether the button stays focusable while there is nothing to clear, so focus stays on itbooleantrue
Clear data attributes
Data attributeDescription
data-disabledPresent when there is no rating to clear, or the rating is disabled or read-only.

ItemRating.Item

One star of the rating: a radio whose name carries its value ("3 stars"), or the exact value on the checked star ("2.5 stars"). Renders a <span>.

Item props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleanfalse
valuerequiredThe star, from 1 to max.number—
disabledWhether the star cannot be chosen.booleanfalse
Item data attributes
Data attributeDescription
data-checkedPresent when the rating's value is in this star.
data-uncheckedPresent when the rating's value is not in this star.
data-fillHow much of the star the shown value (the preview, else the value) fills.
data-disabledPresent when the star cannot be chosen: it or the rating is disabled, or it is below min.
data-readonlyPresent when the rating is read-only.
data-requiredPresent when the rating is required.
data-validPresent when the rating is in a valid state (when wrapped in Field.Root).
data-invalidPresent when the rating is in an invalid state (when wrapped in Field.Root).
data-touchedPresent when the rating has been touched (when wrapped in Field.Root).
data-dirtyPresent when the rating's value has changed (when wrapped in Field.Root).
data-filledPresent when the rating has a value (when wrapped in Field.Root).
data-focusedPresent when a star is focused (when wrapped in Field.Root).
data-previewingPresent while a hover preview is shown.
Item CSS variables
CSS variableDescription
--rating-item-fillHow much of the star the shown value fills, from 0% to 100%.

RootRating.Root

Groups the stars of a rating: a radio group with one radio per star, and the hidden input that submits the value. and a hidden <input> beside it. Renders a <div>.

Root props
PropTypeDefault
valueThe rating, a multiple of step from min to max, or null for no rating.number | null—
defaultValueThe rating when the component is initially rendered.number | nullnull
onValueChangeEvent handler called when the rating changes.((value: number | null, eventDetails: RatingRootChangeEventDetails) => void)—
onPreviewValueChangeEvent handler called when the hover preview changes: the value under a mouse or pen pointer,((value: number | null, eventDetails: RatingRootPreviewChangeEventDetails) => void)—
maxThe number of stars.number5
stepThe smallest change: 1 / n for an integer n from 1 to 10 (1, 0.5, 0.25, 0.2, 0.1).number1
minThe smallest value that can be set.number—
disabledWhether the component should ignore user interaction.booleanfalse
readOnlyWhether the user should be unable to change the rating.booleanfalse
requiredWhether the user must choose a rating before submitting a form.booleanfalse
allowClearWhether pressing the checked star, or Space on it, clears the rating.booleanfalse
previewOnHoverWhether a mouse or pen hovering the stars previews the value under the pointer.booleantrue
loopFocusWhether the arrow keys wrap from the last value to the first and back.booleanfalse
orientationThe layout of the stars, set as aria-orientation.Orientation'horizontal'
labelsThe texts, in English by default.Partial<RatingLabels>—
localeThe locale used by Intl.NumberFormat when formatting the numbers of the names.Intl.LocalesArgument—
nameIdentifies the field when a form is submitted.string—
formIdentifies the form that owns the hidden input.string—
inputRefA ref to access the hidden input element.React.Ref<HTMLInputElement>—
Root data attributes
Data attributeDescription
data-disabledPresent when the rating is disabled.
data-readonlyPresent when the rating is read-only.
data-requiredPresent when the rating is required.
data-previewingPresent while a hover preview is shown.
data-validPresent when the rating is in a valid state (when wrapped in Field.Root).
data-invalidPresent when the rating is in an invalid state (when wrapped in Field.Root).
data-touchedPresent when the rating has been touched (when wrapped in Field.Root).
data-dirtyPresent when the rating's value has changed (when wrapped in Field.Root).
data-filledPresent when the rating has a value (when wrapped in Field.Root).
data-focusedPresent when a star is focused (when wrapped in Field.Root).

ValueRating.Value

The shown rating as visual text: "3 out of 5", or what its children function returns ("Good"). Hidden from assistive technology, since the stars' names carry the value. Renders a <span>.

Value props
PropTypeDefault
childrenThe text, or a function of the value and the hover preview (each null when there is none).React.ReactNode | ((value: number | null, previewValue: number | null) => React.ReactNode)—
Value data attributes
Data attributeDescription
data-previewingPresent while a hover preview is shown.

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
TabMoves into the group on the checked star, or the first star when there is no value, and checks nothingRoot
ArrowRightArrowDownRaises the value by one step and moves focus to the star that holds itItem
ArrowLeftArrowUpLowers the value by one step; in RTL ArrowRight lowers and ArrowLeft raises, the up and down stayItem
ArrowUpArrowDownWith orientation="vertical": the only arrows that move; left and right do nothingRoot
HomeEndSets the lowest value (min, step by default) or maxItem
Stop at the first and last value; with loopFocus they wrapRoot
SpaceChecks the focused star; on the checked star it clears when allowClearItem
1maxSets that whole value; 0 clears when allowClearItem
EnterNothing: a star is not a buttonItem
EnterSpaceClearClears the value with reason clear-press; focus stays on ClearClear

Accessibility

What the parts set
PartSetsSo that
Rootrole="radiogroup", aria-orientation, aria-readonly, aria-required, aria-disabled; its name from Field.Label, aria-label or aria-labelledbythe stars are read as one choice with its name and state; a development build warns when it has no name
Itemrole="radio", aria-checked on the star that holds the value only, aria-label "1 star", "3 stars"the value is one checked radio among max stars
Itemon the checked star of a fractional value, the exact value: "2.5 stars"the half is in the name of the focused star, not only in its fill
Itemaria-disabled and tabIndex -1 on a disabled star, never disableda disabled star stays in the group and the arrows skip it
Cleara button with type="button", named "Clear rating" (labels.clear) or by its textclearing is a control of its own, reached without a gesture on a star
Valuearia-hidden="true""3 out of 5" is not read again after the checked star

Focus order

  1. The group is one Tab stop: the checked star, or the first star that is not disabled when there is no value. Nothing takes focus on mount, and Tab into the group checks nothing.
  2. The arrows, Home, End and the digits move focus to the star that holds the new value as they change it; a change within one star (a half step) keeps focus there.
  3. Clear is a Tab stop of its own after the stars, focusable while disabled; after a press focus stays on it.
  4. A read-only group keeps its Tab stop; a disabled one has none. A required, invalid group in a Form gets focus on submit, on its checked or first star.