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

# Thread Message Stack

> A source-owned editable conversation stack with arbitrary incoming and outgoing messages

# Thread Message Stack

A source-owned editable conversation stack with arbitrary incoming and outgoing messages

**Thread Message Stack**

```html thread-message-stack.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Thread Message Stack</title>
  </head>
  <body>
    <template id="tms-template">
      <div
        id="tms-root"
        data-composition-id="thread-message-stack"
        data-start="0"
        data-duration="8"
        data-width="1920"
        data-height="1080"
      >
        <style>
          #tms-root,
          #tms-root * {
            box-sizing: border-box;
          }

          #tms-root {
            position: relative;
            width: 1920px;
            height: 1080px;
            overflow: hidden;
            container-type: size;
            isolation: isolate;
            background: #2e241a;
            color: #ffffff;
            font-family: -apple-system, "Helvetica Neue", "Segoe UI", sans-serif;
            --tms-out-fill: #2d89e1;
            --tms-out-ink: #ffffff;
            --tms-in-fill: rgba(240, 240, 245, 0.82);
            --tms-in-ink: #101216;
          }

          #tms-roll,
          #tms-scrim,
          #tms-view {
            position: absolute;
            inset: 0;
          }

          #tms-roll {
            overflow: hidden;
            background:
              radial-gradient(circle at 18% 20%, #f0d3a6 0, transparent 31%),
              radial-gradient(circle at 75% 18%, #e8a86a 0, transparent 34%),
              radial-gradient(circle at 62% 74%, #2e6a8c 0, transparent 32%),
              radial-gradient(circle at 18% 82%, #6b4a7a 0, transparent 35%), #2e241a;
            transform: scale(1.12);
            will-change: transform;
          }

          #tms-scrim {
            background:
              linear-gradient(
                to bottom,
                transparent 0,
                rgba(0, 0, 0, 0.18) 42%,
                rgba(0, 0, 0, 0.38) 100%
              ),
              linear-gradient(to right, rgba(0, 0, 0, 0.32), rgba(0, 0, 0, 0) 78%);
          }

          #tms-view {
            overflow: hidden;
            -webkit-mask-image: linear-gradient(to bottom, transparent 5%, #000 24%, #000 100%);
            mask-image: linear-gradient(to bottom, transparent 5%, #000 24%, #000 100%);
          }

          #tms-column {
            position: absolute;
            inset: 0;
          }

          .tms-row {
            position: absolute;
            right: 6%;
            bottom: 10%;
            left: 6%;
            display: flex;
            flex-direction: column;
            opacity: 0;
            will-change: transform, opacity;
          }

          .tms-row[data-side="incoming"] {
            align-items: flex-start;
          }

          .tms-row[data-side="outgoing"] {
            align-items: flex-end;
          }

          .tms-sender {
            min-height: 30px;
            padding: 0 18px 8px;
            color: #ffffff;
            font-size: 24px;
            font-weight: 700;
            line-height: 1;
            text-shadow: 0 3px 12px rgba(0, 0, 0, 0.72);
          }

          .tms-bubble {
            max-width: min(72%, 920px);
            padding: 24px 38px;
            border-radius: 42px;
            font-size: 42px;
            font-weight: 500;
            letter-spacing: -0.015em;
            line-height: 1.3;
            overflow-wrap: anywhere;
            white-space: pre-wrap;
          }

          .tms-row[data-side="incoming"] .tms-bubble {
            color: var(--tms-in-ink);
            background:
              linear-gradient(180deg, rgba(255, 255, 255, 0.14), rgba(0, 0, 0, 0.05)),
              var(--tms-in-fill);
            box-shadow: 0 12px 34px rgba(0, 0, 0, 0.3);
            transform-origin: 0 100%;
          }

          .tms-row[data-side="outgoing"] .tms-bubble {
            color: var(--tms-out-ink);
            background:
              linear-gradient(180deg, rgba(255, 255, 255, 0.2), rgba(0, 0, 0, 0.17)),
              var(--tms-out-fill);
            box-shadow: 0 12px 38px rgba(0, 0, 0, 0.38);
            transform-origin: 100% 100%;
          }

          #tms-driver {
            position: absolute;
            width: 1px;
            height: 1px;
            opacity: 0;
            pointer-events: none;
          }
        </style>

        <div id="tms-roll" data-layout-ignore></div>
        <div id="tms-scrim" data-layout-ignore></div>
        <div id="tms-view"><div id="tms-column"></div></div>
        <div
          id="tms-driver"
          class="clip"
          data-start="0"
          data-duration="8"
          data-track-index="0"
        ></div>

        <div hidden data-hf-primitive-data>
          { "messages": [ { "side": "incoming", "text": "wait, that is actually wild", "sender":
          "Northbeam support" }, { "side": "outgoing", "text": "can you fix it?" }, { "side":
          "incoming", "text": "way ahead of you" }, { "side": "outgoing", "text": "you are unreal"
          }, { "side": "incoming", "text": "he owes you dinner for this one" }, { "side":
          "outgoing", "text": "already booked" } ], "stagger": 0.86, "hold": 1 }
        </div>
        <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
        <script>
          (function () {
            "use strict";
            window.__timelines = window.__timelines || {};
            var root = document.getElementById("tms-root");
            var column = document.getElementById("tms-column");
            var roll = document.getElementById("tms-roll");
            var data = JSON.parse(
              root
                .querySelector("[data-hf-primitive-data]")
                .textContent.replace(/\s*[\r\n]+\s*/g, " "),
            );
            var messages = data.messages;
            var duration = Number(root.dataset.duration || 8);
            var available = Math.max(0.2, duration - Math.max(0, Number(data.hold || 0)) - 0.35);
            var requestedStagger = Math.max(0, Number(data.stagger == null ? 0.72 : data.stagger));
            var stagger =
              messages.length <= 1 ? 0 : Math.min(requestedStagger, available / messages.length);
            var start = 0.25;
            var rows = [];

            messages.forEach(function (message, index) {
              var row = document.createElement("div");
              row.id = "tms-message-" + index;
              row.className = "tms-row";
              row.dataset.side = message.side;
              if (message.sender !== undefined) {
                var sender = document.createElement("div");
                sender.className = "tms-sender";
                sender.textContent = message.sender;
                row.appendChild(sender);
              }
              var bubble = document.createElement("div");
              bubble.className = "tms-bubble";
              bubble.textContent = message.text;
              row.appendChild(bubble);
              column.appendChild(row);
              rows.push(row);
            });

            var heights = rows.map(function (row) {
              return Math.max(118, row.offsetHeight || 118) + 22;
            });
            var tl = gsap.timeline({ paused: true });
            tl.fromTo(
              roll,
              { x: 0, y: 0, scale: 1.12 },
              { x: -54, y: -22, scale: 1.18, duration: duration, ease: "sine.inOut" },
              0,
            );

            rows.forEach(function (row, index) {
              var at = start + index * stagger;
              var direction = row.dataset.side === "incoming" ? -1 : 1;
              tl.fromTo(
                row,
                { x: direction * 34, y: 0, scale: 0.94, opacity: 0 },
                { x: 0, y: 0, scale: 1, opacity: 1, duration: 0.28, ease: "back.out(1.45)" },
                at,
              );
              var offset = 0;
              for (var previous = index - 1; previous >= 0; previous -= 1) {
                offset += heights[previous + 1];
                tl.to(rows[previous], { y: -offset, duration: 0.24, ease: "power3.out" }, at);
              }
            });

            window.__timelines["thread-message-stack"] = tl;
          })();
        </script>
      </div>
    </template>
  </body>
</html>
```

Copy npx hyperframes add thread-message-stack to the clipboard

That writes one file: `compositions/thread-message-stack.html`.

## Add it to your video

It runs for 8 seconds at 1920×1080. Paste this into your composition:

```html index.html
<div
  data-composition-id="thread-message-stack"
  data-composition-src="compositions/thread-message-stack.html"
  data-start="0"
  data-duration="8"
  data-track-index="1"
  data-width="1920"
  data-height="1080"
></div>
```

Move it in time with `data-start`. Put it on a different timeline row with
`data-track-index`. See [data attributes](/concepts/data-attributes.md) for the rest.

{/* hf:generated-footer */}

Tagged `social` `conversation` `messages`.

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