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

# Thread Message Stack

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

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

## Install

<InstallCommand command="npx hyperframes add thread-message-stack" />

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 theme={null}
<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) for the rest.

## Source

<Accordion title={`thread-message-stack.html`}>
  ```html theme={null}
  <!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>
  ```
</Accordion>

Tagged `social` `conversation` `messages`.

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

- [Notification Stack](/catalog/components/notification-stack.md)
- [Undo, Redo & Patches](/sdk/guides/undo-redo-and-patches.md)
- [Keyframe Scrub Stack](/catalog/components/keyframe-scrub-stack.md)
