npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rxova/react-rating-input

v1.0.2

Published

Any icon, any precision, accessible. A headless, zero-dependency React rating component.

Readme

Any icon, any precision, accessible. A headless, zero-dependency React rating component.

npm install @rxova/react-rating-input

📖 Documentation & live examples → — guides, form recipes, theming, and migration from react-rating / react-stars.

  • Bring your own icon — SVG, emoji, image, arbitrary JSX
  • Any precision — continuous 4.3, halves, tenths, or whole stars, with the rounding direction you choose
  • Accessible — native radios in a radiogroup when interactive, role="img" when not
  • Zero runtime dependencies, ~2.8 kB brotli, no stylesheet to import
  • Form-ready — first-class React Hook Form / Formik / React Final Form / TanStack Form integration

Display

import { Rating } from "@rxova/react-rating-input";

function Scores() {
  return (
    <>
      <Rating value={4.3} /> {/* continuous — a 30%-filled fifth star */}
      <Rating value={4.3} precision={1} /> {/* 4 stars */}
      <Rating value={4.3} precision={0.5} /> {/* 4.5 stars */}
      <Rating value={4.3} icon="⭐" /> {/* emoji */}
      <Rating value={3} max={10} icon={<Heart />} emptyIcon={<HeartOutline />} />
    </>
  );
}

| value={4.3} | precision={1} | precision={0.5} | icon="⭐" | custom <Heart/> | | -------------------------------------------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------- | | | | | | |

Rounding

Two orthogonal props: precision is the grid, rounding is the direction.

| Intent | precision | rounding | 4.3 → | 4.7 → | | ---------------------------- | ----------- | ----------- | ------- | ------- | | Exact / continuous (default) | 0 | 'none' | 4.3 | 4.7 | | Round to whole | 1 | 'nearest' | 4 | 5 | | Floor / truncate | 1 | 'down' | 4 | 4 | | Ceil | 1 | 'up' | 5 | 5 | | Half stars | 0.5 | 'nearest' | 4.5 | 4.5 | | Half stars, never inflate | 0.5 | 'down' | 4 | 4.5 | | Tenths | 0.1 | 'nearest' | 4.3 | 4.7 |

Snapping is display-only. A value of 4.28 shown as whole stars renders 4 but is never rounded back into your state. Out-of-range, NaN and Infinity clamp to [0, max] rather than throwing — a display component must never crash a page over a data value.

Interactive

Providing onChange makes it an input. Nothing else changes.

import { useState } from "react";
import { Rating } from "@rxova/react-rating-input";

function RateYourMeal() {
  const [score, setScore] = useState(0);
  return <Rating value={score} onChange={setScore} precision={0.5} label="Rate your meal" />;
}

Renders a radiogroup of visually-hidden native radios, so arrow keys, form participation, focus-visible and screen-reader announcements all come from the platform rather than from hand-rolled JavaScript.

Interactive mode requires precision >= 0.5. A radiogroup offers discrete options, so continuous input has no steps to select; continuous display works at any precision.

Forms

onChange emits a number, not an event, so {...register('rating')} will not work. Every library below has a controlled adapter — that is the supported path, and it is one line longer.

<Controller
  name="rating"
  control={control}
  rules={{ min: { value: 1, message: "Please rate" } }}
  render={({ field, fieldState }) => (
    <>
      <Rating
        {...field} // value, onChange, onBlur, name, ref all line up
        precision={0.5}
        invalid={fieldState.invalid}
        aria-describedby={fieldState.error ? "rating-err" : undefined}
      />
      {fieldState.error && <p id="rating-err">{fieldState.error.message}</p>}
    </>
  )}
/>
function RatingField() {
  const [field, meta, helpers] = useField("rating");

  return (
    <Rating
      name="rating"
      value={field.value}
      onChange={helpers.setValue}
      onBlur={field.onBlur}
      invalid={meta.touched && !!meta.error}
    />
  );
}
<Field name="rating">
  {({ input, meta }) => (
    <Rating
      value={typeof input.value === "number" ? input.value : 0} // RFF uses '' when empty
      onChange={input.onChange}
      onBlur={input.onBlur}
      invalid={meta.touched && !!meta.error}
    />
  )}
</Field>
<form.Field
  name="rating"
  validators={{ onChange: ({ value }) => (value < 1 ? "Please rate" : undefined) }}
>
  {(field) => (
    <Rating
      name="rating"
      value={field.state.value}
      onChange={field.handleChange} // handleChange takes the number directly
      onBlur={field.handleBlur}
      invalid={!field.state.meta.isValid}
    />
  )}
</form.Field>

onBlur fires only when focus leaves the whole group, never when arrowing between icons — so validation doesn't fire while the user is still choosing.

With a plain <form>, just pass name and the value posts natively.

Styling

No stylesheet to import. Only layout-critical CSS is inlined; everything visual is a custom property or a data-* hook.

| Property | Default | Applies to | | ------------------------------- | ------------------------------- | -------------------------------- | | --rx-rating-size | 1.25rem | Icon size | | --rx-rating-gap | 0.125rem | Space between icons | | --rx-rating-color-filled | #f5a623 | Filled portion | | --rx-rating-color-empty | #d8d8d8 | Track | | --rx-rating-color-hover | var(--rx-rating-color-filled) | Hover/keyboard preview | | --rx-rating-empty-filter | grayscale(1) opacity(0.35) | Dims the empty layer — see Emoji | | --rx-rating-transition | 120ms | Fill transitions | | --rx-rating-focus-ring | 2px solid Highlight | Ring on the focused icon | | --rx-rating-focus-ring-offset | 2px | Its offset | | --rx-rating-focus-ring-radius | 2px | Its corner radius |

data-* attributes

These are public API, covered by semver.

| Attribute | On | Meaning | | -------------------------------------------------- | ------- | ------------------------------ | | data-rx-rating-root | wrapper | Always present | | data-rx-rating-item | icon | The icon's index | | data-rx-rating-layer | layer | fill or empty | | data-state | icon | full, partial or empty | | data-fill | icon | Fraction filled, 0–1 | | data-active | icon | Under the pointer or keyboard | | data-idx | icon | Index, for nth-style selectors | | data-invalid / data-disabled / data-readonly | wrapper | Mirrors the props |

Keyboard

Interactive ratings are a real radiogroup of real radios, so this comes from the browser rather than from JavaScript:

| Key | Effect | | -------------------- | ------------------------------------------------- | | ← / →, ↑ / ↓ | Move between values, wrapping at the ends | | Home / End | First or last value | | Space | Select the focused value | | Tab | Leave the group — one tab stop, whatever max is |

Emoji

Emoji work anywhere an icon does, with one caveat worth knowing: they render from a colour font, so --rx-rating-color-filled has no effect on them. That is why the implicit empty layer is dimmed with --rx-rating-empty-filter (a grayscale/opacity filter) rather than with colour — filters work on emoji and SVG alike. Pass an explicit emptyIcon and no filter is applied.

Props

See src/types.ts for the full annotated interface — every prop carries a TSDoc comment explaining what it does and why it exists.

Resilient input

Out-of-range props never break the component — they are coerced to the nearest sensible value so it always renders something valid:

| Input | Coerced to | | ------------------------------ | ------------------ | | value above max | max | | Negative value | 0 | | NaN / ±Infinity value | 0 | | max below 1, or non-finite | 5 | | Non-integer max (e.g. 4.7) | Math.floor (4) |

Because that coercion is silent, a bug like value={7} on a 5-star widget would otherwise pass unnoticed. In development the component surfaces each coercion; pass onWarn to route them into your own logging, or leave it off for a console.warn:

<Rating
  value={score}
  max={5}
  onWarn={(w) => {
    // w: { code, prop, received, used, message }
    if (w.code === "value-above-max") reportToTelemetry(w);
  }}
/>

onWarn fires once per distinct problem, and the whole path — handler included — is stripped from production builds, so it costs nothing there and the value is still clamped either way. Codes are stable and safe to switch on: see RatingWarning / RatingWarningCode in src/types.ts.

Headless

useRating exposes the state machine — value, hover preview, focus, group blur, fills and steps — if you want to render the whole thing yourself. ~1.2 kB on its own.

Accessibility

| Mode | Semantics | | ----------- | ----------------------------------------------------------------- | | Read-only | role="img" with an aria-label; icons hidden; not a tab stop | | Interactive | role="radiogroup" of native radios; one tab stop; arrow-key nav |

Also handled: visible focus ring that the fill layer's overflow: hidden cannot clip, prefers-reduced-motion, RTL (the fill origin flips via CSS logical properties), and aria-invalid / aria-describedby wiring for form errors.

Known limitation: at precision={0.5} each half-star target is narrower than the WCAG 2.2 §2.5.8 24×24 px minimum. That is inherent to half-star input. Use precision={1} or a larger --rx-rating-size where that matters.

UI-library recipes

The docs contain maintained examples for shadcn/ui, Radix Themes, Material UI, Chakra UI, Mantine and Ant Design. Copy a ready-labelled wrapper with:

npx shadcn@latest add https://rxova.dev/packages/react-inputs/r/rating-field.json

Open the recipes.

Part of rxova

Part of the rxova headless React inputs suite — install the whole set from @rxova/react-inputs, or read the full generated API reference for this package.

Migrating from react-rating, react-stars or plain radio buttons? The migration guide has a section for each.

License

MIT © rxova