Accessible star-rating control for Frost UI with fractional values, configurable ranges and star counts, tooltips, keyboard navigation, mouse and touch input, and RTL support.
- Native number input remains the form control and source of truth
- Integer, fractional, exponent, and unrestricted
step="any"values - Configurable minimum, maximum, star count, rating text, and five sizes
- Keyboard, mouse, touch drag, and optional hover-preview interaction
- Direction-aware filling, pointer values, and horizontal keys in RTL
- Accessible slider state, native labels, required and disabled state, and display-only mode
- Frost UI v4 light, dark, system, reduced-motion, focus, and forced-colors presentation
- Native
StarRatingclass andstarratingfQuery plugin - Existing-instance reuse with frozen resolved options
- Prebuilt ESM and UMD bundles with source maps
- Expanded and minified component CSS with source maps
- JSDoc-powered IntelliSense
Explore the demo for interactive examples.
npm i @fr0st/ui-starratingFrost UI StarRating's package entry point is ESM-only and requires a browser DOM. Import the default StarRating export and the stylesheets in browser projects and bundlers.
import '@fr0st/ui/dist/frost-ui.min.css';
import '@fr0st/ui-starrating/dist/frost-ui-starrating.min.css';
import StarRating from '@fr0st/ui-starrating';@fr0st/ui and @fr0st/query are peer dependencies so the component shares the application's instances.
The ESM bundle imports @fr0st/ui and @fr0st/query. fQuery also imports @fr0st/core, so map all three dependencies when loading the bundle directly in a browser:
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@fr0st/ui@latest/dist/frost-ui.min.css">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@fr0st/ui-starrating@latest/dist/frost-ui-starrating.min.css">
<script type="importmap">
{
"imports": {
"@fr0st/core": "https://cdn.jsdelivr.net/npm/@fr0st/core@latest/dist/frost-core.esm.min.js",
"@fr0st/query": "https://cdn.jsdelivr.net/npm/@fr0st/query@latest/dist/fquery.esm.min.js",
"@fr0st/ui": "https://cdn.jsdelivr.net/npm/@fr0st/ui@latest/dist/frost-ui.esm.min.js"
}
}
</script>
<script type="module">
import StarRating from 'https://cdn.jsdelivr.net/npm/@fr0st/ui-starrating@latest/dist/frost-ui-starrating.esm.min.js';
</script>Load the bundles from your own copy or a CDN:
<link
rel="stylesheet"
href="/path/to/dist/frost-ui.min.css">
<link
rel="stylesheet"
href="/path/to/dist/frost-ui-starrating.min.css">
<script src="/path/to/dist/frost-ui-bundle.min.js"></script>
<script src="/path/to/dist/frost-ui-starrating.min.js"></script>
<!-- or -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@fr0st/ui@latest/dist/frost-ui.min.css">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@fr0st/ui-starrating@latest/dist/frost-ui-starrating.min.css">
<script src="https://cdn.jsdelivr.net/npm/@fr0st/ui@latest/dist/frost-ui-bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@fr0st/ui-starrating@latest/dist/frost-ui-starrating.min.js"></script>
<script>
const { StarRating } = globalThis.UI;
</script>The UMD bundle adds StarRating to the existing globalThis.UI object. Load Frost UI's all-in-one bundle first; it supplies the UI and fQuery globals.
The package root resolves to the prebuilt ESM bundle. Published files under dist/ and src/ are also available through matching package subpaths.
StarRating requires an <input type="number">. Passing another element throws a TypeError before initialization.
Start with a normal number input and an explicit or wrapping label. StarRating inserts the visible slider immediately before the input and visually hides the original control while keeping its value synchronized for forms:
<label for="product-rating">Product rating</label>
<input
id="product-rating"
name="rating"
type="number"
min="1"
max="5"
step="0.5"
value="3.5"
required>import StarRating from '@fr0st/ui-starrating';
const rating = StarRating.init(
document.querySelector('#product-rating'),
{
ratingText(value) {
return `${value} out of 5 stars`;
},
},
);
console.log(rating.getValue()); // 3.5Options are resolved in this order:
- Component defaults
- The element's
data-ui-*attributes - Options passed to
StarRating.init()
Resolved instance.options are shallow-frozen.
Native input state is applied during initialization without modifying that object: min, max, and step attributes override their matching resolved options; native readonly enables display-only behavior; and the input's current value supplies the initial rating. The initial disabled and required state, accessible label attributes, associated labels, and direction also come from the input and its DOM context.
Dispose and reinitialize to apply changes to the range, step, read-only mode, labels, or direction; use disable() and enable() to change disabled state.
| Option | Type | Default | Description |
|---|---|---|---|
animate |
boolean |
true |
Animate committed rating changes using Frost UI transition tokens. |
displayOnly |
boolean |
false |
Present a focusable, read-only rating without editing handlers. Native readonly also enables this mode. |
hover |
boolean |
true |
Preview the pointer rating without committing it. |
max |
number | null |
null |
Set the effective maximum. null uses the rendered star count. |
min |
number |
0 |
Set the effective minimum. |
ratingText |
(rating: number) => string |
Singular/plural star text | Create tooltip content and aria-valuetext. |
size |
'xs' | 'sm' | 'md' | 'lg' | 'xl' |
'md' |
Select the component font size. |
stars |
number |
5 |
Set the number of rendered stars. Values are normalized to a positive integer. |
step |
number | 'any' |
1 |
Set the rating increment. 'any' allows unrestricted finite values. |
tooltip |
boolean |
true |
Show rating text during focus, hover, and drag interaction. |
const rating = StarRating.init(node, {
animate: true,
displayOnly: false,
hover: true,
max: 8,
min: 2,
ratingText: (value) => `Score ${value} of 8`,
size: 'lg',
stars: 10,
step: .25,
tooltip: true,
});The effective range is clamped between zero and the rendered star count. Finite values are clamped to that range. Numeric steps are anchored at the effective minimum, and intermediate values advance to the next valid step; the exact minimum and maximum remain selectable. Decimal and exponent steps are supported within JavaScript numeric precision. When a step is too small for reliable snapping, the clamped value is retained instead of introducing rounding drift or an invalid value. Invalid or non-finite programmatic values are ignored, and invalid steps safely behave like step="any".
Use kebab-case data-ui-* attributes for serializable options. Arrays and objects use JSON. Supply callbacks and DOM nodes through JavaScript.
| Attribute | Example |
|---|---|
data-ui-animate |
data-ui-animate="false" |
data-ui-display-only |
data-ui-display-only="true" |
data-ui-hover |
data-ui-hover="false" |
data-ui-max |
data-ui-max="8" |
data-ui-min |
data-ui-min="2" |
data-ui-size |
data-ui-size="lg" |
data-ui-stars |
data-ui-stars="10" |
data-ui-step |
data-ui-step="0.25" |
data-ui-tooltip |
data-ui-tooltip="false" |
<input
id="product-rating"
type="number"
data-ui-toggle="starrating"
data-ui-size="lg"
data-ui-stars="10"
min="2"
max="8"
step="0.25"
value="6.5">import $ from '@fr0st/query';
import '@fr0st/ui-starrating';
$('[data-ui-toggle="starrating"]').starrating();Data attributes configure options; they do not initialize StarRating by themselves. Initialize the component through the class or fQuery plugin. Changing an option's data attribute after initialization does not reconfigure the existing instance.
| Method | Returns | Description |
|---|---|---|
StarRating.init(node, options?) |
StarRating |
Return the existing instance for an element or create one. |
disable() |
void |
Disable the number input, cancel dragging and hover previews, restore the committed rating, and remove the slider from the tab order. |
dispose() |
void |
Remove generated markup and events, unregister component state, and restore the original input. |
enable() |
void |
Enable the number input and restore slider interaction. |
getValue() |
number | null |
Return the current finite rating, or null when the input is empty or invalid. |
setValue(value) |
void |
Normalize, clamp, snap, display, and commit a finite rating. |
rating.setValue(4.5);
console.log(rating.getValue()); // 4.5
rating.disable();
rating.enable();
rating.dispose();dispose() restores the input's original visually-hidden state, aria-hidden, and tabindex, preserves its current value and disabled state and unrelated classes, and removes generated label IDs only if the application has not changed them. The input can then be initialized again with new options. Removing the original input through fQuery also disposes the component automatically.
Calling StarRating.init() again for the same element returns its existing instance. Dispose the current instance before reinitializing with different options.
An instance exposes its original element as instance.node and its shallow-frozen resolved configuration as instance.options. Both become null after disposal.
dispose() releases resources owned by the component and removes its registered instance. Repeated disposal is safe and does not affect a new instance initialized on the same element. Use a new instance before calling other methods after disposal.
If initialization fails, the component releases resources it created and removes its registered instance before rethrowing the error. The element can then be initialized again.
StarRating emits one namespaced fQuery event from the original number input when a component-driven action commits a distinct normalized rating:
| Event | Description |
|---|---|
change.ui.starrating |
The value changed through keyboard, mouse, touch, or setValue(). |
import $ from '@fr0st/query';
$.addEvent(
'#product-rating',
'change.ui.starrating',
(event) => {
console.log(event.currentTarget.value);
},
);The underlying event type is change; fQuery exposes event.namespace as ui.starrating. Setting or selecting the current normalized value again does not emit another event. Hover preview changes only the visible fill and tooltip; it does not commit a value or change the slider's accessible value. A change event dispatched on the input refreshes the rendered control from the native value.
Native form resets cancel active dragging and refresh the visible rating, accessible value, and tooltip after the browser restores the input, without emitting a change event. This also works for read-only ratings and inputs associated with an external form through the form attribute. Canceled resets leave the current interaction intact.
Touch cancellation ends the active drag and restores the latest committed rating without emitting a change event. Ratings committed during the drag are preserved.
Calling disable() stops an active drag, clears any hover preview, and restores the latest committed rating and tooltip text without emitting a change event. If a focus or change handler disables or disposes the component during drag startup, startup stops.
Changes to native disabled, required, and readonly state are observed and synchronized to the rendered slider. Disabled ancestor fieldsets are also observed, respecting the first legend exemption. Becoming disabled or read-only cancels dragging and hover preview while preserving the latest committed rating without emitting a change event. Removing native readonly restores editing unless displayOnly is enabled.
The slider inherits aria-describedby, aria-errormessage, aria-invalid, and aria-required, including later updates and removals. When aria-required is absent, the slider uses the input's native required state.
Importing StarRating registers starrating on fQuery.QuerySet:
import $ from '@fr0st/query';
import '@fr0st/ui-starrating';
const rating = $('#product-rating').starrating({
size: 'lg',
step: .5,
});
$('#product-rating').starrating('setValue', 4.5);
const value = $('#product-rating').starrating('getValue');
$('#product-rating').starrating('disable');
$('#product-rating').starrating('enable');
$('#product-rating').starrating('dispose');Pass an options object to initialize every matched element, or pass a public method name followed by its arguments. The first component or method result is returned.
- The rendered control uses
role="slider"witharia-valuemin,aria-valuemax,aria-valuenow,aria-valuetext,aria-required,aria-readonly, andaria-disabled. - Explicit labels, wrapping labels, multiple labels, and existing
aria-labelledbyreferences contribute to the rendered slider's accessible name. - An input
aria-labelis copied when no label references are available. - Labels without IDs receive temporary generated IDs while the component is active.
- The rendered slider enters the tab order while the original input becomes visually hidden and receives
aria-hidden="true"andtabindex="-1", leaving one control exposed to assistive technology. Focus on the original input is forwarded to the slider, including when the input is already focused during initialization. - Arrow Up and Arrow Down increase and decrease by one step. Arrow Left and Arrow Right are direction-aware. Page Up and Page Down use the larger of one rating point or one step, subject to step snapping and range limits. Home and End select the effective minimum and maximum.
- Handled slider keys prevent page scrolling.
- Disabled ratings leave the tab order and ignore keyboard and pointer interaction.
- Display-only and native read-only ratings expose
aria-readonly="true", remain focusable, and ignore editing interaction. - The original input remains the submitted form field and preserves native value, required, disabled, min, max, and step semantics.
Applications remain responsible for a meaningful label, instructions, validation feedback, and sufficient contrast when overriding the component color. ratingText should describe the rating in the surrounding language and scale.
The public static defaults, classes, icons, and lang objects can be customized before initialization:
StarRating.defaults.tooltip = false;
StarRating.lang.star = 'point';
StarRating.lang.stars = 'points';
StarRating.icons.filled = '<svg aria-hidden="true" ...></svg>';
StarRating.icons.outline = '<svg aria-hidden="true" ...></svg>';StarRating.classes exposes the generated class names for animation, the slider container, disabled state, filled and outline layers, and input hiding. Custom icons should remain decorative because the slider's accessible value comes from ARIA and ratingText.
The component stylesheet exposes these CSS custom properties:
| Property | Purpose |
|---|---|
--ui-starrating-font-size |
Star size for the current variant. |
--ui-starrating-color |
Outline and fill color. |
--ui-starrating-focus-box-shadow |
Focus-visible ring. |
--ui-starrating-disabled-opacity |
Disabled opacity. |
.product-score .starrating {
--ui-starrating-color: var(--ui-success);
}Install Sass and create an application stylesheet to customize the component:
npm i -D sasssrc/styles.scss
@use "@fr0st/ui-starrating/src/scss/vars" with (
$starrating-color: #6750a4,
$starrating-disabled-opacity: .5
);
@use "@fr0st/ui-starrating/src/scss/starrating";Compile the entry point with npm package resolution enabled:
npx sass --load-path=node_modules src/styles.scss dist/styles.cssConfigure the component variables before loading the starrating module. All variables have !default values. Include Frost UI CSS separately. Build tools that already resolve Sass modules from npm packages do not need the explicit load path.
Frost UI follows the user's preferred color scheme by default. Set data-ui-theme="light" or data-ui-theme="dark" on the document or an ancestor to select a theme explicitly.
StarRating combines its component stylesheet with Frost UI v4 transition, focus-ring, disabled, and reduced-motion tokens.
<section data-ui-theme="dark">
<label for="dark-rating">Dark theme rating</label>
<input id="dark-rating" type="number" value="4">
</section>Normal document and ancestor direction is respected. A dir attribute placed directly on the original input is copied to the rendered slider:
<input id="rtl-rating" type="number" value="4" dir="rtl">In RTL layouts, fill begins at the inline start, pointer values are mirrored, and Arrow Left increases while Arrow Right decreases. Vertical keys, Home, End, and Page keys retain their normal meaning. Forced-colors mode replaces the star and focus colors with system colors.
Install dependencies with npm ci, then install Playwright browsers with npx playwright install --with-deps.
npm test
npm run lint
npm run buildnpm test rebuilds the bundles, then runs the Playwright suite in Chromium, Firefox, and WebKit. npm run test:browser runs the suite against the existing bundles, so rebuild after changing source files.
After building, npm run test:coverage runs Chromium tests and writes coverage reports to coverage/.
npm run test:headed and npm run test:ui also use the existing bundles and open headed browsers or the Playwright UI.
To view the demo, open demo/index.html in your browser after building.
Frost UI StarRating is released under the MIT License.