Rating — <mv-rating>

Star rating, for display (true decimal precision: 4.3 fills 30% of the 5th star, review count as a link) or as form-associated input: half stars, hover preview, captions like “Very good”, full keyboard support, clearing, required; compact variant ★ 4.8 (2,413).

CategoryForms
TypeWeb Component (<mv-rating>)
Statusstable
Keywordsrating, stars, étoiles, note, avis, review, half-star, form-associated, e-commerce

When to use

Avoid when

Install

node scripts/add.mjs rating --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["rating"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/form.js, core/motion.js, components/rating/rating.js, components/rating/rating.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div id="rating-demo" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,19rem),1fr));gap:1.5rem;width:min(100%,48rem);margin-inline:auto;font-size:.875rem">
  <style>
    #rating-demo .panel { display:grid; gap:1rem; align-content:start; padding:1.25rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface) }
    #rating-demo .cap { margin:0; font-size:.75rem; font-weight:500; color:var(--mv-fg-muted) }
    #rating-demo .row { display:flex; align-items:center; justify-content:space-between; gap:1rem }
  </style>

  <div class="panel">
    <p class="cap">Display (read-only)</p>
    <div style="display:grid;gap:.375rem">
      <strong style="font-size:1rem">Merino crew-neck sweater</strong>
      <mv-rating readonly value="4.6" count="128" href="#reviews" show-value></mv-rating>
    </div>
    <div class="row"><span>Decimal precision</span><mv-rating readonly value="3.7" show-value data-size="sm"></mv-rating></div>
    <div class="row"><span>Compact</span><mv-rating readonly value="4.8" count="2413" data-variant="compact" data-size="sm"></mv-rating></div>
    <div class="row"><span>Large</span><mv-rating readonly value="4.5" data-size="lg"></mv-rating></div>
    <div class="row"><span>Out of 10</span><mv-rating readonly value="8.5" max="10" show-value data-size="sm" style="--mv-rating-color:var(--mv-accent)"></mv-rating></div>
  </div>

  <form class="panel" id="rating-demo-form" novalidate>
    <p class="cap">Input (form)</p>
    <div style="display:grid;gap:.5rem">
      <label for="rating-demo-note" style="font-weight:500">Your rating <span style="color:var(--mv-danger)" aria-hidden="true">*</span></label>
      <mv-rating id="rating-demo-note" name="note" required clearable placeholder="Pick a rating" data-size="lg" aria-describedby="rating-demo-help"></mv-rating>
      <small id="rating-demo-help" style="color:var(--mv-fg-muted)">Hover and click, or use the arrow keys (or type 1–5).</small>
    </div>
    <div style="display:grid;gap:.5rem">
      <label for="rating-demo-half" style="font-weight:500">Comfort (half stars)</label>
      <mv-rating id="rating-demo-half" name="comfort" precision="0.5" value="3.5" captions="Very uncomfortable,Uncomfortable,Okay,Comfortable,Very comfortable"></mv-rating>
    </div>
    <div style="display:grid;gap:.5rem">
      <span style="font-weight:500">Disabled</span>
      <mv-rating value="4" disabled label="Quality"></mv-rating>
    </div>
    <div style="display:flex;gap:.5rem;align-items:center">
      <button class="mv-button" type="submit">Post review</button>
      <button class="mv-button" type="reset" data-variant="ghost">Reset</button>
    </div>
    <output id="rating-demo-out" style="min-height:1.25rem;font-size:.8125rem;color:var(--mv-fg-muted)" aria-live="polite"></output>
  </form>
</div>
<script type="module">
  const form = document.getElementById("rating-demo-form");
  const out = document.getElementById("rating-demo-out");
  const note = form.querySelector("#rating-demo-note");
  form.addEventListener("submit", (e) => {
    e.preventDefault();
    if (!form.checkValidity()) {
      note.setAttribute("invalid", "");
      out.style.color = "var(--mv-danger)";
      out.textContent = "Pick a rating before posting.";
      note.focus();
      return;
    }
    out.style.color = "";
    out.textContent = `Submitted: ${new URLSearchParams(new FormData(form)).toString()}`;
  });
  note.addEventListener("mv-change", (e) => {
    note.removeAttribute("invalid");
    out.style.color = "";
    out.textContent = e.detail.value ? `Rating: ${e.detail.value}/5 — ${e.detail.label}` : "Rating cleared";
  });
  form.addEventListener("reset", () => { note.removeAttribute("invalid"); out.textContent = ""; });
</script>

API

Attributes

NameTypeDefaultDescription
valuenumber0Rating (0 = none). Observed: a framework can drive it via the attribute.
maxnumber5Number of stars.
precision1 | 0.51Input step (0.5 = half stars).
readonlybooleanDisplay mode: role=img, unrounded value.
clearablebooleanClicking the current rating again clears it.
name / required / disabledstring / booleanForm: submitted value “4.5”, valueMissing when required and empty.
countnumberReview count shown (“128 reviews”).
hrefstringTurns the review count into a link (anchor to the reviews section).
show-valuebooleanShows the numeric rating (“4.6”).
labelstringRatingAccessible name (otherwise <label for>).
captionsstringPoor,Fair,Good,Very good,ExcellentPer-star captions, shown during input and read in aria-valuetext.
localestringen-USNumber formatting.
data-sizesm | lg | xlStar size.
data-variantcompactOne star + rating + (count).
invalidbooleanError style.

Properties

NameTypeDescription
valuenumberCurrent rating.
form / validity / labelsVia ElementInternals.

Methods

NameDescription
focus()Focuses the star slider.
checkValidity() / reportValidity()Native validation.

Events

NameDescription
mv-changeUser change. detail: { value, label }.

CSS classes

NameDescription
mv-rating-stars / -star / -value / -text / -countGenerated parts; --_fill (0…1) on each star.

CSS variables

NameDefaultDescription
--mv-rating-size1.125remSize of one star.
--mv-rating-colorvar(--mv-warning)Filled stars.
--mv-rating-empty-colorgrayEmpty stars.

Accessibility

Display: role=img with aria-label “Rating: 4.6 out of 5 (128 reviews)”. Input: APG “Rating slider” pattern, a single focusable role=slider element, aria-valuenow / aria-valuetext (“4 stars out of 5, Very good” or “No rating”). Arrow keys ±precision (reversed in RTL), Page Up/Down ±1, Home/End, digits 0–9 directly, Backspace/Delete clears. Hover is only a preview; focus stays visible. Name via <label for>, label or aria-label.