Skip to content

About

Customizable star rating component for Frost UI with fractional values, keyboard, pointer, and touch interaction.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

Frost UI StarRating

CI codecov npm version npm downloads JS gzip size CSS gzip size license

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.

Highlights

  • 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 StarRating class and starrating fQuery 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.

Installation

Browser projects / bundlers

npm i @fr0st/ui-starrating

Frost 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.

Browser (ESM)

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>

Browser (UMD)

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.

Usage

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.5

Options

Options are resolved in this order:

  1. Component defaults
  2. The element's data-ui-* attributes
  3. 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".

Data attributes

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.

Methods

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.

Lifecycle

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.

Events

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.

fQuery API

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.

Accessibility

  • The rendered control uses role="slider" with aria-valuemin, aria-valuemax, aria-valuenow, aria-valuetext, aria-required, aria-readonly, and aria-disabled.
  • Explicit labels, wrapping labels, multiple labels, and existing aria-labelledby references contribute to the rendered slider's accessible name.
  • An input aria-label is 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" and tabindex="-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.

Customization

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);
}

Custom Sass builds

Install Sass and create an application stylesheet to customize the component:

npm i -D sass

src/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.css

Configure 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.

Themes and RTL

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.

Development

Install dependencies with npm ci, then install Playwright browsers with npx playwright install --with-deps.

npm test
npm run lint
npm run build

npm 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.

License

Frost UI StarRating is released under the MIT License.

About

Customizable star rating component for Frost UI with fractional values, keyboard, pointer, and touch interaction.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages