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

# Stagger Cascade

> A responsive grid of tile cards that fades and travels into place with an evenly spaced per-item GSAP stagger. The ordered cascade is the only visual mechanic.

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

## Install

<InstallCommand command="npx hyperframes add stagger-cascade" />

That writes one file: `compositions/components/stagger-cascade.html`.

## Paste it into your composition

Open `compositions/components/stagger-cascade.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                                           |
| ----------- | ------- | ----------------------------- | ------------------------------------------------------ |
| `itemCount` | `6`     | 3 to 12, step 1               | Number of tiles in the responsive grid.                |
| `stagger`   | `60`    | 20ms to 150ms, step 1ms       | Delay between consecutive tile entrances.              |
| `direction` | `up`    | `up`, `down`, `left`, `right` | Direction each tile travels into its resting position. |

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="stagger-cascade"
  data-composition-src="compositions/components/stagger-cascade.html"
  data-variable-values='{"itemCount":6,"stagger":60,"direction":"up"}'
></div>
```

## Source

<Accordion title={`stagger-cascade.html`}>
  ```html theme={null}
  <!doctype html>
  <!--
    stagger-cascade: HyperFrames video primitive (effects / burst / exhibit)

    Concept: a responsive grid of plain tile cards enters in DOM order with one
    visible, evenly spaced GSAP stagger. Each tile fades from opacity 0 to 1 and
    travels along the selected axis from a container-relative offset to rest.
    One mechanic, one job: exhibit a group through a readable cascade.

    Compiled-from evidence: production census (launches-motion-vocabulary.md),
    where stagger cascades appear in 13 of 18 launches and observed steps center
    around 0.04 to 0.08 seconds, with 0.14 to 0.18 seconds used for deliberate
    item-by-item reveals.

    Use when: a set of cards, features, rows, or related objects needs to arrive
    as an ordered group without adding a competing hero motion.

    Variables (declared in data-composition-variables below):
      - itemCount (number, default 6, range 3 to 12): tiles in the grid.
      - stagger (number, default 60, range 20 to 150 ms): delay per tile.
      - direction (up | down | left | right, default up): travel direction into
        the resting grid. Up starts below, left starts to the right, and so on.

    Envelope (fixed IN/OUT, elastic HOLD only, never gsap.timeScale()):
      IN_BASE  = 2.40s  cascade budget, including the slowest valid stagger.
      HOLD     = elastic = max(0, D - (IN_BASE + OUT_BASE)); deliberately still
                 so the ordered entrance remains the only mechanic.
      OUT_BASE = 0.40s  clean group fade using --ease-exit (power2.in).
      If D < IN_BASE + OUT_BASE, IN and OUT scale down together so IN + OUT == D
      and HOLD == 0. The stagger step scales only in that compressed envelope.

    Sync point: none. The mechanic distributes arrivals across the IN phase, so
    there is no single fixed cue that represents the cascade honestly.

    Sound cue: none. A distributed cascade has no meaningful single-frame sound
    event, so this primitive does not dispatch hf:sfx.

    Mount contract: this file is a MOUNTABLE SUB-COMPOSITION. A host loads it via
    data-composition-src, and the runtime clones only <template> contents. Styles,
    markup, GSAP, and timeline registration therefore live inside <template>.
    #root has position:absolute; inset:0; container-type:size and no data-width or
    data-height, so the host owns the box. The composition id is hardcoded as
    "stagger-cascade" because mount flattening strips it from the live root.
    Variables come from window.__hyperframes.getVariables(), which supplies the
    declared defaults merged with host data-variable-values overrides.
  -->
  <html
    lang="en"
    data-composition-variables='[
      { "id": "itemCount", "type": "number", "role": "layout", "label": "Item count", "description": "Number of tiles in the responsive grid.", "default": 6, "min": 3, "max": 12, "step": 1 },
      { "id": "stagger", "type": "number", "role": "motion", "label": "Stagger", "description": "Delay between consecutive tile entrances.", "default": 60, "min": 20, "max": 150, "step": 1, "unit": "ms" },
      { "id": "direction", "type": "enum", "role": "motion", "label": "Direction", "description": "Direction each tile travels into its resting position.", "default": "up", "options": [{ "value": "up", "label": "Up" }, { "value": "down", "label": "Down" }, { "value": "left", "label": "Left" }, { "value": "right", "label": "Right" }] }
    ]'
  >
    <head>
      <meta charset="UTF-8" />
      <title>Stagger Cascade</title>
      <!-- Metadata only. The mount loader reads variable declarations from
           <html>, then discards everything outside <template>. -->
    </head>
    <body>
      <template>
        <div
          id="root"
          data-composition-id="stagger-cascade"
          data-start="0"
          data-duration="4"
          data-fps="30"
        >
          <style>
            *,
            *::before,
            *::after {
              box-sizing: border-box;
            }

            /* Root fills the host box. Every internal measurement uses cqw/cqh,
               and every painted color comes from the host theme contract. */
            #root {
              position: absolute;
              inset: 0;
              container-type: size;
              isolation: isolate;
              overflow: hidden;
              color: var(--fg);
              font-family: var(--font-body);
            }

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

            .sc-grid {
              width: 82cqw;
              height: 70cqh;
              display: grid;
              grid-template-columns: repeat(var(--sc-columns), minmax(0, 1fr));
              grid-auto-rows: minmax(0, 1fr);
              gap: min(2.2cqw, 3.8cqh);
            }

            .sc-tile {
              position: relative;
              min-width: 0;
              min-height: 0;
              display: grid;
              place-items: center;
              overflow: hidden;
              border: 0.16cqw solid var(--border);
              border-radius: min(2.4cqw, 4cqh);
              background: color-mix(in srgb, var(--surface) 92%, var(--brand));
              color: var(--fg);
            }

            .sc-tile::after {
              content: "";
              position: absolute;
              left: 14%;
              right: 14%;
              bottom: 12%;
              height: min(0.55cqw, 0.95cqh);
              border-radius: 50cqw;
              background: var(--brand);
              opacity: 0.72;
            }

            .sc-tile-index {
              display: block;
              font-family: var(--font-display);
              font-size: min(5cqw, 8cqh);
              font-weight: 700;
              line-height: 1;
              color: var(--fg);
            }
          </style>

          <div
            id="stagger-cascade-clip"
            class="sc-clip clip"
            data-start="0"
            data-duration="4"
            data-track-index="0"
          >
            <div class="sc-grid" aria-label="Cascading tile grid"></div>
          </div>

          <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 compositionId = "stagger-cascade";
              var grid = root.querySelector(".sc-grid");

              // EDIT ZONE: declarations above and clamped reads below are the
              // single source of truth for the primitive's three public knobs.
              var vars =
                window.__hyperframes && window.__hyperframes.getVariables
                  ? window.__hyperframes.getVariables()
                  : {};

              var rawItemCount = Number(vars.itemCount);
              var itemCount = Number.isFinite(rawItemCount) ? Math.round(rawItemCount) : 6;
              itemCount = Math.max(3, Math.min(12, itemCount));

              var rawStagger = Number(vars.stagger);
              var staggerMs = Number.isFinite(rawStagger) ? rawStagger : 60;
              staggerMs = Math.max(20, Math.min(150, staggerMs));

              // INVARIANT: only these four mappings can reach GSAP. Each option
              // has a distinct axis or sign at preview size.
              var offsets = {
                up: { x: "0cqw", y: "12cqh" },
                down: { x: "0cqw", y: "-12cqh" },
                left: { x: "8cqw", y: "0cqh" },
                right: { x: "-8cqw", y: "0cqh" },
              };
              var offset = Object.prototype.hasOwnProperty.call(offsets, vars.direction)
                ? offsets[vars.direction]
                : offsets.up;

              // itemCount owns both DOM count and the responsive grid shape.
              var fragment = document.createDocumentFragment();
              for (var index = 0; index < itemCount; index += 1) {
                var tile = document.createElement("div");
                var number = document.createElement("span");
                tile.className = "sc-tile";
                tile.setAttribute("aria-hidden", "true");
                number.className = "sc-tile-index";
                number.textContent = String(index + 1).padStart(2, "0");
                tile.appendChild(number);
                fragment.appendChild(tile);
              }
              grid.replaceChildren(fragment);
              grid.style.setProperty(
                "--sc-columns",
                String(Math.min(4, Math.ceil(Math.sqrt(itemCount)))),
              );

              var tiles = root.querySelectorAll(".sc-tile");

              // RETIME RANGE: change these four base values together. IN_BASE is
              // sized so 12 items at 150ms plus the entrance tween finish exactly
              // at its boundary. HOLD alone absorbs longer host durations.
              var IN_BASE = 2.4;
              var OUT_BASE = 0.4;
              var CASCADE_START_BASE = 0.1;
              var ENTRY_DURATION_BASE = 0.65;

              var duration = Math.max(0.001, parseFloat(root.dataset.duration || "4"));
              var totalBase = IN_BASE + OUT_BASE;
              var scale = duration < totalBase ? duration / totalBase : 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 CASCADE_START = CASCADE_START_BASE * scale;
              var ENTRY_DURATION = ENTRY_DURATION_BASE * scale;
              var STAGGER_SECONDS = (staggerMs / 1000) * scale;

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

              // IN: the sole mechanic. Explicit endpoints keep mounted re-seeks
              // deterministic. power2.out is --ease-standard.
              timeline.fromTo(
                tiles,
                { opacity: 0, x: offset.x, y: offset.y },
                {
                  opacity: 1,
                  x: "0cqw",
                  y: "0cqh",
                  duration: ENTRY_DURATION,
                  ease: "power2.out",
                  stagger: { each: STAGGER_SECONDS, from: "start" },
                },
                CASCADE_START,
              );

              // HOLD: deliberately unanimated. OUT: one clean group fade with
              // power2.in, the GSAP form of --ease-exit.
              timeline.to(grid, { opacity: 0, duration: OUT, ease: "power2.in" }, OUT_START);

              timeline.seek(0);

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

Tagged `effects` `grid` `stagger` `entrance` `exhibit`.

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

- [Text Stagger](/catalog/components/text-stagger.md)
- [Stagger Lattice](/catalog/components/stagger-lattice.md)
- [Logo Brand Close](/catalog/components/logo-brand-close.md)
