Install
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: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>
data-start. Put it on a different timeline row with
data-track-index. See data attributes for the rest.
Source
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>
social conversation messages.