> ## Documentation Index
> Fetch the complete documentation index at: https://hyperframes.heygen.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Star Rating Fill

> A token-driven star row whose colored layer sweeps to a rating, preserves the fractional final star, and can count the value in sync.

export const InstallCommand = ({command}) => {
  const [copied, setCopied] = React.useState(false);
  const copy = async () => {
    try {
      if (navigator.clipboard && window.isSecureContext) {
        await navigator.clipboard.writeText(command);
      } else {
        const previous = document.activeElement;
        const scratch = document.createElement("textarea");
        scratch.value = command;
        scratch.setAttribute("readonly", "");
        scratch.style.position = "fixed";
        scratch.style.opacity = "0";
        document.body.appendChild(scratch);
        scratch.select();
        document.execCommand("copy");
        document.body.removeChild(scratch);
        previous?.focus?.();
      }
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch {}
  };
  return <div className="hf-install-command not-prose my-4 flex items-stretch overflow-hidden rounded-xl border border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900">
      <code className="flex-1 overflow-x-auto whitespace-nowrap border-r border-zinc-200 px-4 py-3 font-mono text-sm text-zinc-800 dark:border-zinc-800 dark:text-zinc-100">
        {command}
      </code>
      <button type="button" onClick={copy} data-copied={copied ? "true" : "false"} aria-label={`Copy ${command} to the clipboard`} className="hf-install-copy">
        <svg className="hf-install-copy-clipboard" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M14.25 5.25H7.25C6.14543 5.25 5.25 6.14543 5.25 7.25V14.25C5.25 15.3546 6.14543 16.25 7.25 16.25H14.25C15.3546 16.25 16.25 15.3546 16.25 14.25V7.25C16.25 6.14543 15.3546 5.25 14.25 5.25Z" />
          <path d="M2.80103 11.998L1.77203 5.07397C1.61003 3.98097 2.36403 2.96397 3.45603 2.80197L10.38 1.77297C11.313 1.63397 12.19 2.16297 12.528 3.00097" />
        </svg>
        <svg className="hf-install-copy-check" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M2.75 9.5L6.5 13.25L15.25 4.5" />
        </svg>
      </button>
      <span className="hf-install-copy-status" role="status" aria-live="polite">
        {copied ? "Copied" : ""}
      </span>
    </div>;
};

<iframe className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800" title="star-rating-fill preview" loading="lazy" srcDoc={`<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;height:100%;overflow:hidden;background:transparent}hyperframes-player{display:block;width:100%;height:100%}</style><script src="https://cdn.jsdelivr.net/npm/@hyperframes/player@0.7/dist/hyperframes-player.global.js"><\/script></head><body><script>fetch("/public/catalog/components/star-rating-fill.json").then(function(r){return r.json()}).then(function(d){var p=document.createElement("hyperframes-player");p.setAttribute("srcdoc",d.html);p.setAttribute("controls","");p.setAttribute("autoplay","");p.setAttribute("loop","");p.setAttribute("muted","");p.setAttribute("poster","https://static.heygen.ai/hyperframes-oss/docs/images/catalog/components/star-rating-fill.png");document.body.appendChild(p)});<\/script></body></html>`} />

## Install

<InstallCommand command="npx hyperframes add star-rating-fill" />

That writes one file: `compositions/components/star-rating-fill.html`.

## Paste it into your composition

Open `compositions/components/star-rating-fill.html` and copy what is inside into your own composition.

A component has no size or duration of its own. It takes both from the composition
you paste it into.

## Variables

Every one of these has a default, so the piece works untouched. Set the ones you
want to change on the element:

| Variable    | Default | Accepts          | What it does                                  |
| ----------- | ------- | ---------------- | --------------------------------------------- |
| `rating`    | `4.8`   | 0 to 5, step 0.1 | Target rating in star units from 0 to 5.      |
| `starCount` | `5`     | 1 to 10, step 1  | Number of stars shown in the row.             |
| `showValue` | `yes`   | `yes`, `no`      | Show or hide the synchronized numeric rating. |

Set them with `data-variable-values` on the element that mounts it. These are the
defaults, so this behaves exactly like the preview above until you change one:

```html wrap theme={null}
<div
  data-composition-id="star-rating-fill"
  data-composition-src="compositions/components/star-rating-fill.html"
  data-variable-values='{"rating":4.8,"starCount":5,"showValue":"yes"}'
></div>
```

## Source

<Accordion title={`star-rating-fill.html`}>
  ```html theme={null}
  <!doctype html>
  <!--
    star-rating-fill: HyperFrames video primitive (data / holdable / prove)

    Concept: one token-colored star layer is clipped over a muted copy. A
    single left-to-right clip-path sweep resolves at the target rating, so a
    fractional value leaves the final filled star visibly partial. Small
    per-star pops follow the same sweep and support the one fill mechanic.

    Compiled-from evidence: GAP A2+B1 convergent miss, rules
    stat-bars-and-fills. That evidence specifies a gray star layer, a colored
    overlay, and a fractional clip wipe as the deterministic seek handle.

    Use when: proving app-store credibility, customer satisfaction, review
    quality, or any scored result that needs to read in one glance.

    Variables:
      rating    number  0 to 5  default 4.8  target rating in star units
      starCount number          default 5    visible stars, clamped 1 to 10
      showValue yes | no        default yes  show the synchronized count-up

    Envelope:
      IN   1.50s fixed sweep, count, and per-star pop, power2.out
      HOLD elastic and deliberately still so the proof remains readable
      OUT  0.40s fixed opacity release, power2.in
      For durations shorter than 1.90s, IN and OUT scale together. HOLD is the
      only elastic phase, and the timeline is never time-scaled.

    Sync point: fill-settled at 1.30s into IN. It is fixed outside HOLD.

    Sound cues: none. The result should not imply an audio event for its host.

    Mount contract: this file is a template-wrapped sub-composition. The host
    owns dimensions. #root fills that box with inset:0, uses container queries,
    and has no data-width or data-height. The hardcoded composition id matches
    the root and timeline key. Variables come from getVariables().
  -->
  <html
    lang="en"
    data-composition-variables='[
      { "id": "rating", "type": "number", "role": "content", "label": "Rating", "description": "Target rating in star units from 0 to 5.", "default": 4.8, "min": 0, "max": 5, "step": 0.1 },
      { "id": "starCount", "type": "number", "role": "layout", "label": "Star count", "description": "Number of stars shown in the row.", "default": 5, "min": 1, "max": 10, "step": 1 },
      { "id": "showValue", "type": "enum", "role": "content", "label": "Show value", "description": "Show or hide the synchronized numeric rating.", "default": "yes", "options": [{ "value": "yes", "label": "Yes" }, { "value": "no", "label": "No" }] }
    ]'
  >
    <head>
      <meta charset="UTF-8" />
      <title>Star Rating Fill</title>
    </head>
    <body>
      <template>
        <div
          id="root"
          data-composition-id="star-rating-fill"
          data-start="0"
          data-duration="4"
          data-fps="30"
        >
          <style>
            *,
            *::before,
            *::after {
              box-sizing: border-box;
            }

            /* INVARIANT: #root owns only the mount boundary. The host owns the
               box, while all internal geometry stays container-relative. */
            #root {
              position: absolute;
              inset: 0;
              container-type: size;
              isolation: isolate;
              overflow: hidden;
              color: var(--fg, CanvasText);
              font-family: var(--font-display, system-ui);
            }

            .srf-clip {
              width: 100%;
              height: 100%;
              display: grid;
              place-items: center;
              overflow: hidden;
              background: var(--bg, transparent);
            }

            .srf-stage {
              display: flex;
              align-items: center;
              justify-content: center;
              gap: var(--space-2, 3cqw);
            }

            /* EDIT ZONE: the row owns only its responsive presentation size.
               --srf-count is supplied by the clamped public starCount value. */
            .srf-stars {
              --srf-count: 5;
              position: relative;
              width: min(68cqw, calc(72cqh * var(--srf-count)));
            }

            .srf-layer {
              width: 100%;
              display: flex;
              align-items: center;
            }

            .srf-base {
              color: color-mix(in srgb, var(--muted, currentColor) 68%, var(--surface, transparent));
            }

            .srf-fill {
              position: absolute;
              inset: 0;
              color: var(--brand, currentColor);
              clip-path: inset(0 100% 0 0);
            }

            .srf-star-cell {
              display: block;
              flex: 1 1 0;
              min-width: 0;
              aspect-ratio: 1;
              transform-origin: center;
            }

            .srf-star-cell svg {
              display: block;
              width: 100%;
              height: 100%;
              overflow: visible;
              fill: currentColor;
            }

            .srf-value {
              min-width: 2.2em;
              color: var(--fg, currentColor);
              font-size: min(9cqw, 16cqh);
              font-weight: 720;
              font-variant-numeric: tabular-nums;
              letter-spacing: -0.04em;
              line-height: 1;
              text-align: right;
            }
          </style>

          <div
            id="star-rating-fill-clip"
            class="srf-clip clip"
            data-start="0"
            data-duration="4"
            data-track-index="0"
          >
            <div class="srf-stage">
              <div class="srf-stars" role="img" aria-label="0 out of 5 stars">
                <div class="srf-layer srf-base" aria-hidden="true"></div>
                <div class="srf-layer srf-fill" aria-hidden="true"></div>
              </div>
              <div class="srf-value" aria-hidden="true">0.0</div>
            </div>
          </div>

          <template id="star-rating-fill-star-template">
            <span class="srf-star-cell">
              <svg viewBox="0 0 100 100" aria-hidden="true" focusable="false">
                <path
                  d="M50 0 61.8 36.2 100 36.2 69.1 58.6 80.9 95 50 72.4 19.1 95 30.9 58.6 0 36.2 38.2 36.2Z"
                />
              </svg>
            </span>
          </template>

          <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
          <script>
            (function () {
              "use strict";

              var root = document.getElementById("root");
              var stage = root.querySelector(".srf-stage");
              var stars = root.querySelector(".srf-stars");
              var baseLayer = root.querySelector(".srf-base");
              var fillLayer = root.querySelector(".srf-fill");
              var valueElement = root.querySelector(".srf-value");
              var starTemplate = root.querySelector("#star-rating-fill-star-template");
              var vars =
                window.__hyperframes && window.__hyperframes.getVariables
                  ? window.__hyperframes.getVariables()
                  : {};

              /* INVARIANT: zero is a valid rating. Missing values and zero are
                 handled separately, then every public number is clamped once. */
              var ratingValue = vars.rating == null || vars.rating === "" ? NaN : Number(vars.rating);
              var requestedRating = Number.isFinite(ratingValue)
                ? Math.max(0, Math.min(5, ratingValue))
                : 4.8;
              var starCountValue =
                vars.starCount == null || vars.starCount === "" ? NaN : Number(vars.starCount);
              var starCount = Number.isFinite(starCountValue) ? Math.round(starCountValue) : 5;
              starCount = Math.max(1, Math.min(10, starCount));
              var rating = Math.min(requestedRating, starCount);
              var showValue = vars.showValue === "no" ? "no" : "yes";
              var fillPercent = (rating / starCount) * 100;

              stars.style.setProperty("--srf-count", String(starCount));
              stars.setAttribute("aria-label", rating.toFixed(1) + " out of " + starCount + " stars");
              valueElement.hidden = showValue === "no";

              /* The template is the single owner of star geometry. Both paint
                 layers clone it so their silhouettes cannot drift. */
              var baseFragment = document.createDocumentFragment();
              var fillFragment = document.createDocumentFragment();
              for (var index = 0; index < starCount; index += 1) {
                baseFragment.appendChild(starTemplate.content.firstElementChild.cloneNode(true));
                fillFragment.appendChild(starTemplate.content.firstElementChild.cloneNode(true));
              }
              baseLayer.replaceChildren(baseFragment);
              fillLayer.replaceChildren(fillFragment);

              var baseCells = baseLayer.querySelectorAll(".srf-star-cell");
              var fillCells = fillLayer.querySelectorAll(".srf-star-cell");
              var valueState = { current: 0 };

              /* RETIME RANGE: these values own the full lifecycle. The sweep
                 uses --ease-standard's GSAP owner, power2.out. */
              var IN_BASE = 1.5;
              var OUT_BASE = 0.4;
              var FILL_START_BASE = 0.2;
              var FILL_DURATION_BASE = 1.1;
              var POP_DURATION_BASE = 0.2;
              var durationValue = Number(root.dataset.duration);
              var duration = Number.isFinite(durationValue) && durationValue > 0 ? durationValue : 4;
              var scale = duration < IN_BASE + OUT_BASE ? duration / (IN_BASE + OUT_BASE) : 1;
              var IN = IN_BASE * scale;
              var OUT = OUT_BASE * scale;
              var HOLD = Math.max(0, duration - IN - OUT);
              var OUT_START = IN + HOLD;
              var FILL_START = FILL_START_BASE * scale;
              var FILL_DURATION = FILL_DURATION_BASE * scale;
              var POP_DURATION = POP_DURATION_BASE * scale;

              var timeline = gsap.timeline({ paused: true });

              timeline.fromTo(
                fillLayer,
                { clipPath: "inset(0 100% 0 0)" },
                {
                  clipPath: "inset(0 " + (100 - fillPercent) + "% 0 0)",
                  duration: FILL_DURATION,
                  ease: "power2.out",
                },
                FILL_START,
              );

              if (showValue === "yes") {
                timeline.to(
                  valueState,
                  {
                    current: rating,
                    duration: FILL_DURATION,
                    ease: "power2.out",
                    onUpdate: function () {
                      valueElement.textContent = valueState.current.toFixed(1);
                    },
                  },
                  FILL_START,
                );
              }

              /* Per-star pops are subordinate to the sweep and occur in the
                 same left-to-right order. They never change the fill endpoint. */
              var popCount = Math.ceil(rating);
              for (var popIndex = 0; popIndex < popCount; popIndex += 1) {
                var popAt = FILL_START + FILL_DURATION * (popIndex / Math.max(1, rating)) * 0.82;
                var popTargets = [baseCells[popIndex], fillCells[popIndex]];
                timeline.to(
                  popTargets,
                  { scale: 1.06, duration: POP_DURATION * 0.45, ease: "power2.out" },
                  popAt,
                );
                timeline.to(
                  popTargets,
                  { scale: 1, duration: POP_DURATION * 0.55, ease: "power2.out" },
                  popAt + POP_DURATION * 0.45,
                );
              }

              timeline.to(stage, { opacity: 0, duration: OUT, ease: "power2.in" }, OUT_START);

              timeline.seek(0);

              window.__timelines = window.__timelines || {};
              window.__timelines["star-rating-fill"] = timeline;
            })();
          </script>
        </div>
      </template>
    </body>
  </html>
  ```
</Accordion>

Tagged `data` `rating` `stars` `fractional` `fill` `prove`.

## Related topics

* [Browse the complete Catalog](/catalog)
* [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
* [Build a richer composition](/go-further)


## Related topics

- [Testimonial Card](/catalog/components/testimonial-card.md)
- [Share feedback](/guides/feedback.md)
- [Spiral Galaxy](/catalog/blocks/spiral-galaxy.md)
