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

# Scroll Feed

> A loop-friendly column of varied skeleton post cards scrolls upward with subtle motion trails and an agitated pace.

# Scroll Feed

A loop-friendly column of varied skeleton post cards scrolls upward with subtle motion trails and an agitated pace.

**Scroll Feed**

```html scroll-feed.html theme={null}
<!doctype html>
<!--
  scroll-feed: HyperFrames video primitive (effects / agitation / loop)

  Concept: a self-scrolling column of skeleton post cards recycles without
  a visible seam. The default register is a doomscroll: distinct thumb
  flicks (fast start, power3 decay to a settle) with a read pause between
  them, one flick per card. Two low-opacity copies trail the main column to
  suggest motion blur while deterministic index math varies every card.

  Variables:
    - speed (doom | frantic, default doom): changes the card pitch, so the
      column covers more distance during the same duration in frantic mode.
    - card_count (number, 4 to 10, default 6): cards in one repeated cycle.
    - cues (string, default ""): comma-separated seconds (from mount start);
      each cue advances the feed by exactly one card pitch (stepped rhythm).
      Empty synthesizes evenly spaced flicks, one per card, that still cover
      exactly one full cycle.
    - exit (none | fade | up, default "none"): optional departure over a
      short reserved tail.

  Envelope: IN = 0s, HOLD = D minus any exit tail, OUT = 0s or the exit
  tail. HOLD owns the complete scroll and is the only elastic phase. A
  longer mount stretches HOLD directly, never through timeScale. With no
  cues and exit none, zero-length IN and OUT keep the first and final
  frames loop-compatible and each column moves exactly one complete card
  cycle. Cues or an enabled exit trade that loop guarantee for the cue
  rhythm or the departure.

  Determinism: cards are created synchronously. Their dimensions vary only
  through index arithmetic. No clocks, random values, timers, or CSS
  animations participate in rendering.

  Mount contract: the runtime clones only this template. #root fills the
  host box, establishes the container query basis, has no data-width or
  data-height, and registers one paused timeline under the hardcoded
  scroll-feed id.
-->
<html
  lang="en"
  data-composition-id="scroll-feed"
  data-composition-duration="4"
  data-composition-variables='[
    { "id": "speed", "type": "enum", "role": "timing", "label": "Scroll speed", "description": "Pace of the upward feed movement.", "default": "doom", "options": [{ "value": "doom", "label": "Doom" }, { "value": "frantic", "label": "Frantic" }] },
    { "id": "card_count", "type": "number", "role": "content", "label": "Card count", "description": "Number of skeleton posts in one repeated feed cycle.", "default": 6, "min": 4, "max": 10, "step": 1 },
    { "id": "cues", "type": "string", "role": "timing", "label": "Cues", "description": "Comma-separated seconds (from mount start); each cue advances the feed by one card. Empty synthesizes evenly spaced flicks, one per card.", "default": "" },
    { "id": "exit", "type": "enum", "role": "timing", "label": "Exit", "description": "Optional departure. Default none: the feed scrolls until the frame cuts.", "default": "none", "options": [{ "value": "none", "label": "None" }, { "value": "fade", "label": "Fade" }, { "value": "up", "label": "Up" }] }
  ]'
>
  <head>
    <meta charset="UTF-8" />
    <title>Scroll Feed</title>
  </head>
  <body>
    <template>
      <div id="root" data-composition-id="scroll-feed" data-duration="4" data-fps="30">
        <style>
          *,
          *::before,
          *::after {
            box-sizing: border-box;
          }

          #root {
            position: absolute;
            inset: 0;
            overflow: hidden;
            container-type: size;
            isolation: isolate;
            background: linear-gradient(180deg, var(--bg, #171b23) 0%, var(--bg, #0f1218) 100%);
            color: var(--fg, #d8dee9);
            font-family: var(--font-body, Inter, system-ui, sans-serif);
            pointer-events: none;
          }

          .sf-viewport {
            position: absolute;
            inset: 0;
            overflow: hidden;
            /* Soft fade at both feed edges so cards never clip hard mid-media. */
            mask-image: linear-gradient(
              180deg,
              transparent 0,
              #000 7cqh,
              #000 calc(100% - 7cqh),
              transparent 100%
            );
          }

          .sf-column {
            position: absolute;
            top: -20cqh;
            left: 12cqw;
            width: 76cqw;
            will-change: transform;
          }

          .sf-cycle {
            display: flex;
            flex-direction: column;
          }

          .sf-card {
            flex: 0 0 var(--sf-card-h);
            width: 76cqw;
            height: var(--sf-card-h);
            margin-bottom: var(--sf-gap);
            padding: var(--space-2, 2.3cqh) var(--space-2, 2.4cqw);
            overflow: hidden;
            border: 0.12cqw solid var(--border, rgba(160, 174, 192, 0.2));
            border-radius: var(--radius, 2.2cqw);
            /* Second stop derives from --surface so the gradient survives a
               host theme instead of collapsing to a flat fill. */
            background: linear-gradient(
              145deg,
              var(--surface, #29303b) 0%,
              color-mix(in srgb, var(--surface, #29303b) 76%, var(--bg, #0f1218)) 100%
            );
            box-shadow: 0 1.8cqh 4cqh rgba(0, 0, 0, 0.28);
          }

          .sf-card-header {
            display: flex;
            align-items: center;
            gap: var(--space-1, 1.4cqw);
            height: 4.4cqh;
            margin-bottom: var(--space-1, 1.5cqh);
          }

          .sf-avatar {
            flex: 0 0 auto;
            width: var(--sf-avatar);
            height: var(--sf-avatar);
            border-radius: 50%;
            background: var(--muted, #758094);
          }

          .sf-identity {
            display: flex;
            flex: 1 1 auto;
            min-width: 0;
            flex-direction: column;
            gap: var(--space-1, 0.65cqh);
          }

          .sf-line,
          .sf-meta,
          .sf-action {
            display: block;
            border-radius: 99cqw;
          }

          .sf-line {
            height: 0.85cqh;
            background: var(--muted, #7d8798);
          }

          .sf-line-primary {
            width: var(--sf-primary-w);
          }

          .sf-line-secondary {
            width: var(--sf-secondary-w);
            opacity: 0.62;
          }

          .sf-meta {
            flex: 0 0 auto;
            width: var(--sf-meta-w);
            height: 0.75cqh;
            background: var(--muted, #667184);
            opacity: 0.72;
          }

          .sf-media {
            position: relative;
            height: calc(var(--sf-card-h) - 12.2cqh);
            min-height: 6cqh;
            overflow: hidden;
            border-radius: var(--radius, 1.4cqw);
            /* The media block gets its own derivation (lifted toward --fg)
               so card and media stay distinct under any --surface. */
            background: linear-gradient(
              135deg,
              color-mix(in srgb, var(--surface, #29303b) 88%, var(--fg, #d8dee9)) 0%,
              color-mix(in srgb, var(--surface, #29303b) 90%, var(--bg, #0f1218)) 100%
            );
          }

          .sf-media::before {
            position: absolute;
            top: var(--sf-media-y);
            left: var(--sf-media-x);
            width: 18cqw;
            height: 18cqw;
            border-radius: 50%;
            background: var(--muted, rgba(130, 143, 160, 0.16));
            content: "";
          }

          .sf-card-footer {
            display: flex;
            align-items: center;
            gap: var(--space-2, 2.4cqw);
            height: 2cqh;
            margin-top: var(--space-1, 1.35cqh);
          }

          .sf-action {
            width: var(--sf-action-w);
            height: 0.8cqh;
            background: var(--muted, #717c8e);
            opacity: 0.72;
          }

          .sf-blur {
            z-index: 0;
            opacity: 0.1;
            filter: blur(0.28cqh);
          }

          .sf-blur-a {
            top: -19.2cqh;
            left: 11.55cqw;
          }

          .sf-blur-b {
            top: -20.8cqh;
            left: 12.45cqw;
          }

          .sf-main {
            z-index: 1;
          }
        </style>

        <div
          id="scroll-feed-clip"
          class="sf-viewport clip"
          data-start="0"
          data-duration="4"
          data-track-index="0"
        >
          <div class="sf-column sf-blur sf-blur-a" aria-hidden="true"></div>
          <div class="sf-column sf-blur sf-blur-b" aria-hidden="true"></div>
          <div class="sf-column sf-main" aria-hidden="true"></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 mainColumn = root.querySelector(".sf-main");
            var blurA = root.querySelector(".sf-blur-a");
            var blurB = root.querySelector(".sf-blur-b");
            var columns = root.querySelectorAll(".sf-column");
            var vars =
              window.__hyperframes && window.__hyperframes.getVariables
                ? window.__hyperframes.getVariables()
                : {};

            var speed = vars.speed === "frantic" ? "frantic" : "doom";
            var countValue = Number(vars.card_count);
            var cardCount = Number.isFinite(countValue)
              ? Math.max(4, Math.min(10, Math.round(countValue)))
              : 6;
            var exit = vars.exit === "fade" || vars.exit === "up" ? vars.exit : "none";
            var cuesValue = vars.cues == null ? "" : String(vars.cues);
            var cues = cuesValue
              .split(",")
              .reduce(function (result, token) {
                var trimmed = token.trim();
                if (trimmed === "") return result;
                var seconds = Number(trimmed);
                if (!Number.isFinite(seconds) || seconds < 0) return result;
                result.push(seconds);
                return result;
              }, [])
              .sort(function (a, b) {
                return a - b;
              });

            // Frantic mode travels farther in the same HOLD while still
            // completing exactly one full cycle at the final frame.
            var cardHeight = speed === "frantic" ? 32 : 24;
            var gap = speed === "frantic" ? 4 : 3;
            var cycleDistance = cardCount * (cardHeight + gap);
            root.style.setProperty("--sf-card-h", cardHeight + "cqh");
            root.style.setProperty("--sf-gap", gap + "cqh");

            var cycle = document.createElement("div");
            cycle.className = "sf-cycle";

            for (var index = 0; index < cardCount; index += 1) {
              var card = document.createElement("article");
              card.className = "sf-card";
              card.style.setProperty("--sf-avatar", 3.2 + (index % 3) * 0.45 + "cqh");
              card.style.setProperty("--sf-primary-w", 18 + ((index * 11) % 24) + "cqw");
              card.style.setProperty("--sf-secondary-w", 12 + ((index * 7) % 19) + "cqw");
              card.style.setProperty("--sf-meta-w", 7 + ((index * 5) % 9) + "cqw");
              card.style.setProperty("--sf-media-x", 5 + ((index * 17) % 48) + "cqw");
              card.style.setProperty("--sf-media-y", -8 + ((index * 13) % 10) + "cqh");
              card.style.setProperty("--sf-action-w", 7 + ((index * 3) % 8) + "cqw");
              card.innerHTML =
                '<header class="sf-card-header"><span class="sf-avatar"></span><span class="sf-identity"><span class="sf-line sf-line-primary"></span><span class="sf-line sf-line-secondary"></span></span><span class="sf-meta"></span></header><div class="sf-media"></div><footer class="sf-card-footer"><span class="sf-action"></span><span class="sf-action"></span><span class="sf-action"></span></footer>';
              cycle.appendChild(card);
            }

            // Three identical cycles keep the viewport covered before, during,
            // and after the exact one-cycle translation. The blur trails clone
            // this single deterministic source instead of recomputing cards.
            mainColumn.appendChild(cycle);
            mainColumn.appendChild(cycle.cloneNode(true));
            mainColumn.appendChild(cycle.cloneNode(true));
            blurA.innerHTML = mainColumn.innerHTML;
            blurB.innerHTML = mainColumn.innerHTML;

            // IN stays zero. Exit none keeps OUT at zero for a clean loop;
            // an enabled exit reserves a short tail after the scroll.
            var duration = Math.max(0.001, parseFloat(root.dataset.duration || "4"));
            var EXIT = exit === "none" ? 0 : Math.min(0.4, duration * 0.15);
            var SCROLL_END = Math.max(0.001, duration - EXIT);
            var finalY = -cycleDistance;

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

            // Stepped rhythm: each step advances the feed by exactly one
            // card pitch (thumb flick: fast start, power3 decay, settle).
            // Three stacked cycles keep the viewport covered through two
            // full cycles of steps. Without cues the default doomscroll
            // synthesizes one evenly spaced flick per card, so the column
            // still travels exactly one full cycle by SCROLL_END and the
            // first and final frames stay loop-compatible.
            var pitch = cardHeight + gap;
            var steps = [];
            var autoFlick = cues.length === 0;
            if (autoFlick) {
              for (var step = 0; step < cardCount; step += 1) {
                steps.push((SCROLL_END / cardCount) * step);
              }
            } else {
              cues.forEach(function (cueAt) {
                if (steps.length >= cardCount * 2) return;
                var at = Math.min(SCROLL_END, cueAt);
                if (steps.length === 0 || at > steps[steps.length - 1] + 0.01) {
                  steps.push(at);
                }
              });
            }
            finalY = -pitch * steps.length;
            steps.forEach(function (cueAt, index) {
              var next = index + 1 < steps.length ? steps[index + 1] : SCROLL_END;
              // Auto flicks keep a longer rest so each card gets a read;
              // explicit cues keep the documented cue-mode timing.
              var stepDuration = autoFlick
                ? Math.max(0.05, Math.min(0.45, (next - cueAt) * 0.6))
                : Math.max(0.05, Math.min(0.5, (next - cueAt) * 0.8, duration - cueAt));
              var target = { y: -pitch * (index + 1) + "cqh" };
              if (index === 0) {
                tl.fromTo(
                  columns,
                  { y: "0cqh" },
                  {
                    y: target.y,
                    duration: stepDuration,
                    ease: "power3.out",
                  },
                  cueAt,
                );
              } else {
                tl.to(columns, { y: target.y, duration: stepDuration, ease: "power3.out" }, cueAt);
              }
            });

            // Optional departure; exit none scrolls until the frame cuts.
            if (exit === "fade") {
              tl.to(columns, { opacity: 0, duration: EXIT, ease: "power2.in" }, SCROLL_END);
            } else if (exit === "up") {
              tl.to(columns, { opacity: 0, duration: EXIT, ease: "power2.in" }, SCROLL_END);
              tl.to(
                columns,
                { y: finalY - 10 + "cqh", duration: EXIT, ease: "power2.in" },
                SCROLL_END,
              );
            }

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

Copy npx hyperframes add scroll-feed to the clipboard

That writes one file: `compositions/components/scroll-feed.html`.

## Paste it into your composition

Open `compositions/components/scroll-feed.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 |
| - | - | - | - |
| `speed` | `doom` | `doom`, `frantic` | Pace of the upward feed movement. |
| `card_count` | `6` | 4 to 10, step 1 | Number of skeleton posts in one repeated feed cycle. |
| `cues` | \`\` | string | Comma-separated seconds (from mount start); each cue advances the feed by one card. Empty synthesizes evenly spaced flicks, one per card. |
| `exit` | `none` | `none`, `fade`, `up` | Optional departure. Default none: the feed scrolls until the frame cuts. |

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="scroll-feed"
  data-composition-src="compositions/components/scroll-feed.html"
  data-variable-values='{"speed":"doom","card_count":6,"cues":"","exit":"none"}'
></div>
```

Tagged `motion-primitive` `effects` `scroll` `feed` `loop` `agitation`.

## Related topics

* [Browse the complete Catalog](/catalog.md)
* [Add assets and Catalog items in the browser editor](/studio/assets-and-blocks.md)
* [Put blocks together into one video](/concepts/compositions.md)


## Related topics

- [Scroll Camera Story](/catalog/components/scroll-camera-story.md)
- [Text, captions and code on screen](/prompting/captions-catalog.md)
- [Input Feedback](/catalog/components/input-feedback.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.