Vanilla UI Kit Rating v1.0.0 · tabs →

Star ratings, one file.

Zero-dependency rating input and display that follows your page's theme on its own. Hover, click, or Tab in and use the arrow keys — every card is live.

Basic, half steps

Hover the left half of a star for .5. One tab stop, arrow keys step by 0.5.

value: 3.5
new Rating('#basic', {
  value: 3.5,
  onChange: v => console.log(v)
})

Hearts

Built-in heart icon, same gold fill — or pass your own { empty, full } SVG pair.

new Rating('#hearts', {
  icon: 'heart', value: 2
})

Read-only average

Fractions render exactly — the fill is a clipped overlay, so a 4.3 average shows a 30% fifth star. No focus, no interaction.

4.3 · 1,284 reviews
new Rating('#avg', {
  value: 4.3, readOnly: true, size: 18
})

Max 10 + value text

showValue prints the live number after the stars; keys 1–9 jump straight to a value.

new Rating('#ten', {
  max: 10, value: 7, half: false,
  size: 18, showValue: true
})

In a real form

Enhancing an <input> hides and syncs it — it still submits, still fires change, and destroy() restores it.

<input id="score" name="score" value="3">
new Rating('#score')   // submits score=3.5

Clearable

Clicking the value that's already committed clears back to 0 (default). Set clearable: false to keep ratings sticky — try both below.

clearable — click ★3 twice
clearable: false
new Rating(el, { clearable: false })

API reference

The complete Rating API — constructor modes, every option, instance methods, statics, events, declarative attributes, theming and accessibility.

Constructor & usage

new Rating(target, options?)     // → instance
Rating.create(target, options?)  // same as new

target is a selector string or an element, in one of two modes:

The submitted value is the number as text ("3.5"), or the empty string when the rating is 0/cleared. Constructing on an element that already holds an instance destroys the old one first; a missing target throws; without a DOM (SSR) the constructor returns an inert no-op instance.

new Rating('#stars', { onChange: (v) => console.log(v) })

<input id="score" name="score" value="3">
new Rating('#score')   // submits score=3.5

Options

Every key of Rating.defaults, with its default value.

NameTypeDefaultDescription
maxnumber5 Number of stars (minimum 1, floored to an integer).
valuenumber0 Initial value, clamped to [0, max]. In input mode the input's own value wins unless value is passed explicitly.
halfbooleantrue Half-star steps on hover (left half of a star = .5), click and keyboard (step 0.5 instead of 1).
iconstring | object'star' 'star' | 'heart', or an { empty, full } pair of TRUSTED SVG strings (injected verbatim — never user input). Unknown names fall back to 'star'.
sizenumber22 Icon size in px (minimum 8).
readOnlybooleanfalse Display mode: no interaction, no focus stop — the group renders role="img" with the labels text.
clearablebooleantrue Clicking the value that is already committed clears back to 0.
namestring | nullnull Adds a hidden input for form submission (container mode).
showValuebooleanfalse Live value text ('3.5') after the stars.
disabledbooleanfalse Start disabled (input mode inherits the input's own disabled unless set explicitly).
labelsfunction(v, max) => `${v} of ${max}` Builds the aria-valuetext (interactive) or aria-label (read-only) — e.g. "3.5 of 5". Customize for i18n.
themestring'auto' 'auto' | 'light' | 'dark'. Auto re-resolves live against the page theme.
stylesbooleantrue false = headless: no CSS is injected — style the .vrt-* markup yourself.
onChangefunction | nullnull fn(value) — fired when a value is committed (click, keyboard, non-silent setValue).
onHoverfunction | nullnull fn(value) while previewing on hover, fn(null) on mouse-leave.

Methods

MethodReturnsDescription
getValue()number The committed value (0 when cleared).
setValue(v, {silent}?)instance Any number 0..max — fractions are fine and render exactly via the clipped overlay (setValue(4.3) shows a 30% fifth star). Clamped; fires onChange and rating:change only when the value actually changed and not with {silent: true}.
enable() / disable()instance Toggle interactivity. disable() takes the slider out of the tab order (tabindex="-1", aria-disabled) and, in input mode, mirrors disabled onto the native input so a disabled control never submits.
destroy()instance Remove everything the widget built and restore the target completely — an enhanced input reappears un-hidden, keeping its synced value.

Statics & helpers

StaticDescription
Rating.create(target, opts) Same as new Rating(target, opts).
Rating.get(target) The instance previously bound to an element (or null).
Rating.autoInit(root?) Initialize every [data-vrt] under root (default: document); returns the created instances. Runs automatically on DOMContentLoaded.
Rating.defaults The live defaults object — mutate before constructing (e.g. Rating.defaults.styles = false).
Rating.css The full stylesheet as a string (rendered with the current salt) — a starting point for headless styling.
Rating.salt CSS isolation token, default 'vc1'. Set your own token or false before the first instance.
Rating.version Version string, '1.0.0'.

Events & callbacks

document.getElementById('stars').addEventListener('rating:change', (e) => {
  console.log(e.detail.value)   // the new value
  console.log(e.detail.rating)  // the instance
})

Declarative init

Add data-vrt to a container or input — instances are built on DOMContentLoaded (or call Rating.autoInit(root) after inserting markup).

<div data-vrt data-value="3.5" data-name="score"></div>
AttributeMaps toNotes
data-vrt—Marks the element.
data-max / data-value / data-sizemax/value/sizeNumbers (data-value accepts fractions).
data-half / data-clearable / data-show-value / data-disabled / data-styleshalf/clearable/showValue/disabled/stylesBooleans; "false" and "0" are false.
data-read-only (or data-readonly)readOnlyBoolean; both spellings accepted.
data-iconiconstar | heart.
data-namenameHidden form input name.
data-themethemeauto | light | dark.

Theming

Auto light/dark with the family's resolution order: <html data-theme> / data-bs-theme / .dark class → prefers-color-scheme, re-resolved live. Pin per instance with theme: 'dark'. All colors are CSS custom properties; Rating is the one family component with a non-accent fill color — a warm star gold — themeable separately:

.vrt {
  --vrt-fill: #f5a623;    /* the star fill (gold; dark theme: #f7b84d) */
  --vrt-accent: #5b5bd6;  /* focus ring */
  --vrt-text: …; --vrt-muted: …; --vrt-faint: …;
  --vrt-radius: 8px; --vrt-font: …;
}

With the VC core loaded, VC.config({ accent: '#b45309' }) themes the focus ring and every other family component in one call (the gold fill stays put unless you override --vrt-fill). Widgets render as class="vrt vc1"; structural rules ship salted so host design systems can't override them, while the unsalted --vrt-* variable definitions keep page overrides working. Change the token with Rating.salt = 'acme' (or false) before the first instance.

Headless: Rating.defaults.styles = false (or per instance) injects no CSS — you keep the full behavior (hover preview, keyboard, ARIA, form sync) and style this markup contract yourself (Rating.css is the reference stylesheet as a string):

.vrt[data-theme=dark].vrt-readonly.vrt-disabled
  .vrt-stars[role=slider|img]      ← .vrt-static when non-interactive
    .vrt-star                      ← × max, sized inline
      .vrt-star-empty              ← outline SVG
      .vrt-star-fill               ← overlay clipped to N% width
        .vrt-star-full             ← filled SVG, fixed width
  .vrt-value                       ← only when showValue
  input[type=hidden]               ← only when name given (container mode)

Accessibility

The whole group is a single tab stop with role="slider", aria-orientation="horizontal", aria-valuemin/max/now and an aria-valuetext built by labels (e.g. "3.5 of 5").

KeyAction
ArrowRight / ArrowUp+ step (step = half ? 0.5 : 1); lands on the nearest grid mark upward — a programmatic 4.3 becomes 4.5
ArrowLeft / ArrowDown− step; nearest grid mark downward — 4.3 becomes 4
Home0 (clear)
Endmax
1–9Jump straight to that value (capped at max)

Headless mode

With styles: false Rating injects no CSS at all — the full behavior (hover preview, keyboard, form sync), the role="slider" ARIA contract and the stable .vrt-* class hooks all remain; you bring the stylesheet. The demos below each run in their own <iframe>: the styled examples above have already injected the component stylesheet into this page, so only a separate, clean document can show what headless truly looks like.

Unstyled (styles: false)

Zero injected CSS — no colors, no sizes, no transitions. The frame adds only the five structural lines below (the overlay-clip geometry every design needs), and hover, click, keyboard and half-stars all work.

new Rating('#stars', {
  value: 3.5,
  styles: false,   // ← zero CSS injected
  onChange: function (v) { /* … */ }
})

/* the component injected nothing — this structural minimum
   is the page's own (no colors, no sizes anywhere): */
.vrt-star { display: inline-block; position: relative; }
.vrt-star svg { display: block; width: 100%; height: 100%; }
.vrt-star-empty, .vrt-star-fill { position: absolute; inset: 0; }
.vrt-star-fill { overflow: hidden; }
.vrt-star-full { display: block; height: 100%; }

Bring your own design (Tailwind)

The same class hooks mapped to a deliberately different design with Tailwind utilities — hearts instead of stars, a rose palette, a scale-up hover. Nothing loads from the CDN until you click.

<style type="text/tailwindcss">
  .vrt        { @apply inline-flex items-center gap-3; }
  .vrt-stars  { @apply inline-flex gap-1 rounded-xl bg-white/70 p-2
                shadow ring-1 ring-rose-100 outline-none
                focus-visible:ring-2 focus-visible:ring-rose-400; }
  .vrt-star   { @apply relative inline-block cursor-pointer text-rose-200
                transition-transform hover:scale-125; }
  .vrt-star svg { @apply block h-full w-full; }
  .vrt-star-empty, .vrt-star-fill { @apply absolute inset-0; }
  .vrt-star-fill  { @apply overflow-hidden text-rose-500 drop-shadow; }
  .vrt-star-full  { @apply block h-full; }
  .vrt-value  { @apply text-sm font-bold tabular-nums text-rose-600; }
</style>