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

# State Chip Rail

> A rail of mono status chips advances a seek-safe data-state snap machine on a cue schedule: active chip ink on surface, done chips dim, pending chips hollow, optional badges pop beside one state.

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="state-chip-rail 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/state-chip-rail.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/state-chip-rail.png");document.body.appendChild(p)});<\/script></body></html>`} />

## Install

<InstallCommand command="npx hyperframes add state-chip-rail" />

That writes one file: `compositions/components/state-chip-rail.html`.

## Paste it into your composition

Open `compositions/components/state-chip-rail.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                                                                                                              |
| ------------- | ------------------------------ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `states`      | `Queued,Reading,Drafting,Done` | string                    | Comma-separated chip labels in machine order (2 to 8 states).                                                             |
| `times`       | \`\`                           | string                    | Comma-separated activation times in seconds, one per advance (state 1 onward). Blank entries use the default even rhythm. |
| `badge_state` | `1`                            | 0 to 7, step 1            | Index of the state the badges pop beside.                                                                                 |
| `badges`      | \`\`                           | string                    | Comma-separated badge labels shown beside the badge state. Empty disables.                                                |
| `accent`      | `green`                        | `green`, `blue`, `violet` | Accent token family used by the active chip.                                                                              |
| `exit`        | `none`                         | `none`, `fade`, `up`      | How the rail leaves the frame. Default none: the hold ends the film.                                                      |

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="state-chip-rail"
  data-composition-src="compositions/components/state-chip-rail.html"
  data-variable-values='{"states":"Queued,Reading,Drafting,Done","times":"","badge_state":1,"badges":"","accent":"green","exit":"none"}'
></div>
```

## Source

<Accordion title={`state-chip-rail.html`}>
  ```html theme={null}
  <!doctype html>
  <!--
    state-chip-rail: HyperFrames video primitive (product demo / state machine)

    A rail of mono status chips advances a data-state snap machine on a cue
    schedule. CSS alone responds to each chip's [data-state]; the timeline only
    flips attributes with tl.set, so every advance is an instantaneous snap that
    is exact in both seek directions (GSAP zero-duration sets record the prior
    attribute value and restore it on backward seeks). The active chip is ink on
    surface, done chips dim, pending chips stay hollow. Optional badges pop
    beside one named state when it activates. Generalizes the flagship
    agent-console status rail (cohere-north frame 04).

    Variables (declared in data-composition-variables below):
      - states (string, default "Queued,Reading,Drafting,Done"): comma list of
        chip labels, in machine order. 2 to 8 states; extras are dropped.
      - times (string, default ""): comma-separated activation times in seconds
        relative to mount start, one per ADVANCE (state 1, state 2, ...).
        Blank or invalid entries use the default even rhythm. Values are
        clamped into the mount window and forced monotonic.
      - badge_state (number, default 1): index of the state the badges pop
        beside. Clamped to the states range.
      - badges (string, default ""): comma list of badge labels rendered after
        the badge_state chip; they pop in when that state activates. Empty
        disables badges. Up to 4.
      - accent (green | blue | violet, default green): active-chip accent.
        green -> --brand, blue -> --accent, violet -> --accent-2.
      - exit ("none" | "fade" | "up", default "none"): frame roots own
        transitions; the default hold-ends-the-film.

    Envelope, fixed IN with elastic HOLD (never gsap.timeScale()):
      IN    = chip cascade (~0.8s) then the advance schedule; the default
              rhythm spreads advances so the final state lands ~1s before the
              mount window closes.
      HOLD  = still after the last advance; nothing floats, nothing loops.
      OUT   = 0 when exit is "none"; else 0.45s fade / fade-up of the rail.

    Mount contract: elastic root, no data-width/data-height, styled via #root
    only, container-type: size, one paused timeline registered under the
    literal "state-chip-rail" key. Deterministic and seek-safe: fromTo
    entrances, attribute snaps via tl.set, no CSS transitions, no Date.now,
    no randomness. State dimming rides color (color-mix), never opacity, so it
    can never collide with the entrance opacity tweens.
  -->
  <html
    lang="en"
    data-composition-id="state-chip-rail"
    data-composition-duration="5"
    data-composition-variables='[
      { "id": "states", "type": "string", "role": "content", "label": "States", "description": "Comma-separated chip labels in machine order (2 to 8 states).", "default": "Queued,Reading,Drafting,Done" },
      { "id": "times", "type": "string", "role": "timing", "label": "Times", "description": "Comma-separated activation times in seconds, one per advance (state 1 onward). Blank entries use the default even rhythm.", "default": "" },
      { "id": "badge_state", "type": "number", "role": "content", "label": "Badge state", "description": "Index of the state the badges pop beside.", "default": 1, "min": 0, "max": 7, "step": 1 },
      { "id": "badges", "type": "string", "role": "content", "label": "Badges", "description": "Comma-separated badge labels shown beside the badge state. Empty disables.", "default": "" },
      { "id": "accent", "type": "enum", "role": "style", "label": "Accent", "description": "Accent token family used by the active chip.", "default": "green", "options": [{ "value": "green", "label": "Green" }, { "value": "blue", "label": "Blue" }, { "value": "violet", "label": "Violet" }] },
      { "id": "exit", "type": "enum", "role": "timing", "label": "Exit", "description": "How the rail leaves the frame. Default none: the hold ends the film.", "default": "none", "options": [{ "value": "none", "label": "None" }, { "value": "fade", "label": "Fade" }, { "value": "up", "label": "Up" }] }
    ]'
  >
    <head>
      <meta charset="UTF-8" />
      <title>State Chip Rail</title>
    </head>
    <body>
      <template>
        <div id="root" data-composition-id="state-chip-rail" data-duration="5" data-fps="30">
          <style>
            *,
            *::before,
            *::after {
              box-sizing: border-box;
            }

            #root {
              position: absolute;
              inset: 0;
              overflow: hidden;
              container-type: size;
              isolation: isolate;
              background: var(--bg, transparent);
              color: var(--fg, #f8fafc);
              font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
              pointer-events: none;
            }

            .scr-clip {
              position: absolute;
              inset: 0;
              display: grid;
              place-items: center;
              overflow: hidden;
            }

            .scr-rail {
              display: flex;
              align-items: center;
              justify-content: center;
              flex-wrap: nowrap;
              max-width: 94cqw;
              gap: var(--scr-gap, 0.8em);
              font-size: var(--scr-font, 2cqmin);
              will-change: transform, opacity;
            }

            .scr-chip,
            .scr-badge {
              display: flex;
              flex: 0 0 auto;
              align-items: center;
              gap: 0.55em;
              padding: 0.62em 1.05em;
              overflow: hidden;
              border: 0.16cqmin solid var(--border, #334155);
              border-radius: 100cqmax;
              font-weight: 500;
              line-height: 1;
              letter-spacing: 0.14em;
              text-transform: uppercase;
              white-space: nowrap;
              will-change: transform, opacity;
            }

            .scr-dot {
              flex: 0 0 auto;
              width: 0.5em;
              aspect-ratio: 1;
              border: 0.16cqmin solid var(--muted, #94a3b8);
              border-radius: 50%;
              background: transparent;
            }

            /* The snap machine: presentation is pure CSS on [data-state]; the
               timeline only flips the attribute. Dimming rides color, never
               opacity, so it cannot fight the entrance opacity tweens. */
            .scr-chip[data-state="pending"] {
              border-color: color-mix(in srgb, var(--border, #334155) 80%, transparent);
              background: transparent;
              color: color-mix(in srgb, var(--muted, #94a3b8) 78%, transparent);
            }

            .scr-chip[data-state="active"] {
              border-color: color-mix(in srgb, var(--scr-accent) 62%, var(--border, #334155));
              background: var(--surface, #131a24);
              color: var(--fg, #f8fafc);
            }
            .scr-chip[data-state="active"] .scr-dot {
              border-color: transparent;
              background: var(--scr-accent, var(--brand, #71f5a7));
            }

            .scr-chip[data-state="done"] {
              border-color: color-mix(in srgb, var(--border, #334155) 45%, transparent);
              background: transparent;
              color: color-mix(in srgb, var(--muted, #94a3b8) 42%, transparent);
            }
            .scr-chip[data-state="done"] .scr-dot {
              border-color: transparent;
              background: color-mix(in srgb, var(--scr-accent, #71f5a7) 38%, transparent);
            }

            .scr-badge {
              padding: 0.5em 0.9em;
              border-color: color-mix(in srgb, var(--border, #334155) 70%, transparent);
              color: color-mix(in srgb, var(--muted, #94a3b8) 88%, transparent);
              font-size: 0.82em;
            }
          </style>

          <div
            id="state-chip-rail-clip"
            class="scr-clip clip"
            data-start="0"
            data-duration="5"
            data-track-index="0"
          >
            <div class="scr-rail" role="list"></div>
          </div>

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

              // Literal id, never read off the DOM: the flattening step strips
              // data-composition-id from the mounted root (see toggle-flip).
              var compositionId = "state-chip-rail";
              var root = document.getElementById("root");
              var rail = root.querySelector(".scr-rail");
              var vars =
                window.__hyperframes && window.__hyperframes.getVariables
                  ? window.__hyperframes.getVariables()
                  : {};

              function commaList(value, max) {
                return String(value == null ? "" : value)
                  .split(",")
                  .map(function (token) {
                    return token.trim();
                  })
                  .filter(function (token) {
                    return token !== "";
                  })
                  .slice(0, max);
              }

              var states = commaList(vars.states, 8);
              if (states.length === 0) states = ["Queued", "Reading", "Drafting", "Done"];
              var count = states.length;

              var badges = commaList(vars.badges, 4);
              var badgeStateRaw = Math.round(Number(vars.badge_state));
              var badgeState = Number.isFinite(badgeStateRaw)
                ? Math.max(0, Math.min(count - 1, badgeStateRaw))
                : Math.min(1, count - 1);

              var exit = vars.exit === "fade" || vars.exit === "up" ? vars.exit : "none";

              // Each enum choice routes to a DIFFERENT contract token so the
              // variable stays meaningful under a theme.
              var accentColors = {
                green: "var(--brand, #71f5a7)",
                blue: "var(--accent, #61a8ff)",
                violet: "var(--accent-2, #c5a3ff)",
              };
              var accent = Object.prototype.hasOwnProperty.call(accentColors, vars.accent)
                ? vars.accent
                : "green";
              root.style.setProperty("--scr-accent", accentColors[accent]);

              // Build the rail: chips in machine order, badges directly after
              // the badge_state chip. State 0 is active from mount.
              var chips = states.map(function (label, index) {
                var chip = document.createElement("div");
                chip.className = "scr-chip";
                chip.setAttribute("role", "listitem");
                chip.setAttribute("data-state", index === 0 ? "active" : "pending");
                var dot = document.createElement("span");
                dot.className = "scr-dot";
                dot.setAttribute("aria-hidden", "true");
                var text = document.createElement("span");
                text.textContent = label;
                chip.appendChild(dot);
                chip.appendChild(text);
                return chip;
              });
              var badgeElements = badges.map(function (label) {
                var badge = document.createElement("div");
                badge.className = "scr-badge";
                badge.textContent = label;
                return badge;
              });
              chips.forEach(function (chip, index) {
                rail.appendChild(chip);
                if (index === badgeState) {
                  badgeElements.forEach(function (badge) {
                    rail.appendChild(badge);
                  });
                }
              });
              rail.setAttribute("aria-label", states.join(", "));

              // Deterministic fit: wide-tracked mono at ~0.76em per glyph, plus
              // chip padding, dot, and rail gaps, all measured in em.
              var railEm = (count - 1 + badges.length) * 0.8;
              states.forEach(function (label) {
                railEm += Array.from(label).length * 0.76 + 3.1;
              });
              badges.forEach(function (label) {
                railEm += (Array.from(label).length * 0.76 + 2.5) * 0.82;
              });
              var fontCqw = Math.min(2.4, 90 / Math.max(1, railEm));
              root.style.setProperty("--scr-font", "min(" + fontCqw.toFixed(3) + "cqw, 9cqh)");

              // RETIME RANGE: fixed IN cascade + advance schedule, elastic
              // still HOLD, optional OUT.
              var OUT_BASE = 0.45;
              var duration = Math.max(0.001, parseFloat(root.dataset.duration || "5"));
              var OUT = exit === "none" ? 0 : Math.min(OUT_BASE, duration * 0.2);
              var avail = duration - OUT;

              var CHIP = Math.min(0.45, Math.max(0.2, avail * 0.15));
              var entryGap = Math.min(0.08, Math.max(0.02, avail * 0.02));
              var entryEnd = 0.12 + entryGap * (count - 1) + CHIP;

              // Default rhythm: advances spread evenly from just after the
              // cascade to ~1s before the window closes.
              var firstAdvance = Math.min(Math.max(entryEnd + 0.15, 0.9), Math.max(0.2, avail - 0.6));
              var lastAdvance = Math.max(firstAdvance, avail - 1.0);
              var advanceCount = count - 1;
              var timesList = String(vars.times == null ? "" : vars.times).split(",");
              var previous = 0.15;
              var advances = [];
              for (var k = 0; k < advanceCount; k += 1) {
                var fallback =
                  advanceCount > 1
                    ? firstAdvance + (k * (lastAdvance - firstAdvance)) / (advanceCount - 1)
                    : firstAdvance;
                var cueText = (timesList[k] || "").trim();
                var cue = Number(cueText);
                var start = cueText !== "" && Number.isFinite(cue) ? cue : fallback;
                start = Math.max(0.15, Math.min(start, Math.max(0.15, avail - 0.45)));
                start = Math.max(start, previous + 0.05);
                previous = start;
                advances.push(start);
              }

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

              // IN: chips cascade into the rail. Explicit both-endpoints state
              // so any seek is exact. Badges hold at scale 0 until their pop
              // (they keep their layout slot, so the rail never reflows).
              chips.forEach(function (chip, index) {
                gsap.set(chip, { opacity: 0, y: "2.5cqh" });
                tl.fromTo(
                  chip,
                  { opacity: 0, y: "2.5cqh" },
                  { opacity: 1, y: "0cqh", duration: CHIP, ease: "power3.out" },
                  0.12 + entryGap * index,
                );
              });

              // The snap machine: pure attribute flips. GSAP records the prior
              // value on first render in timeline order, so backward seeks
              // restore pending / active exactly.
              advances.forEach(function (start, index) {
                var stateIndex = index + 1;
                tl.set(chips[stateIndex], { attr: { "data-state": "active" } }, start);
                tl.set(chips[stateIndex - 1], { attr: { "data-state": "done" } }, start);
              });

              // Badges pop beside their state as it activates.
              if (badgeElements.length > 0) {
                var popStart = badgeState === 0 ? entryEnd + 0.1 : advances[badgeState - 1] + 0.12;
                popStart = Math.min(popStart, Math.max(0.2, avail - 0.4));
                badgeElements.forEach(function (badge, index) {
                  gsap.set(badge, { opacity: 0, scale: 0 });
                  tl.fromTo(
                    badge,
                    { opacity: 0, scale: 0 },
                    { opacity: 1, scale: 1, duration: 0.4, ease: "power3.out" },
                    popStart + 0.14 * index,
                  );
                });
              }

              // HOLD: intentionally still. The resolved machine reads as a frame.

              // OUT: the whole rail leaves together.
              if (exit !== "none") {
                var OUT_START = duration - OUT;
                tl.to(rail, { opacity: 0, duration: OUT, ease: "power2.in" }, OUT_START);
                if (exit === "up") {
                  tl.to(rail, { y: "-3cqh", duration: OUT, ease: "power2.in" }, OUT_START);
                }
              }

              tl.seek(0);
              window.__timelines = window.__timelines || {};
              window.__timelines[compositionId] = tl;
            })();
          </script>
        </div>
      </template>
    </body>
  </html>
  ```
</Accordion>

Tagged `motion-primitive` `product-demo` `state-machine` `status` `chips` `rail`.

## 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

- [Swipe Rail](/catalog/components/swipe-rail.md)
- [Input Feedback](/catalog/components/input-feedback.md)
- [Iterating](/prompting/iterating.md)
