Files
hyperframes/docs/catalog/components/titlecard-lockup.mdx
T
Miguel Ángel 9734578e60 feat(registry): bring back the video-primitive moves (#3169)
Restores the 208 catalog items reverted after their previews 404'd in
production, this time on the payload mechanism rather than the .html files
that caused the outage.

The generator no longer writes a preview document to docs/public. That writer,
and the machinery under it, existed only to produce files the docs host
discards, so it is gone rather than bypassed. Items now embed the composition
itself via a payload, which is what the previous change already does for the
items that were already in the catalog.

The variables explorer is parked, not restored: it drove its preview through
the same unpublished .html path, so it would have shown an empty frame. Items
that declare variables get the live player plus the static variables table, and
reconnecting the explorer to payloads is a follow-up.
2026-08-10 18:46:03 -04:00

392 lines
16 KiB
Plaintext

---
title: "Titlecard Lockup"
description: "The calm breather titlecard: an optional mono kicker fades up, the wordmark settles dead-center with one restrained move, a hairline rule draws left to right, a mono label fades beneath, and the lockup holds truly still."
---
import { InstallCommand } from "/snippets/install-command.jsx";
<iframe
className="w-full aspect-video rounded-xl border-0 bg-zinc-100 dark:bg-zinc-800"
title="titlecard-lockup 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/titlecard-lockup.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/titlecard-lockup.png");document.body.appendChild(p)});<\/script></body></html>`}
/>
## Install
<InstallCommand command="npx hyperframes add titlecard-lockup" />
That writes one file: `compositions/components/titlecard-lockup.html`.
## Paste it into your composition
Open `compositions/components/titlecard-lockup.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 |
| --- | --- | --- | --- |
| `wordmark` | `HYPERFRAMES` | string | Display wordmark that settles dead-center. |
| `label` | `WRITE HTML. RENDER VIDEO.` | string | Mono caption under the rule. Empty hides it. |
| `kicker` | `INTRODUCING` | string | Small mono label above the wordmark. Empty hides it. |
| `rule` | `show` | `show`, `hide` | Hairline rule drawn left to right under the wordmark. |
| `accent` | `green` | `green`, `blue`, `violet` | Contract color carried by the hairline rule. |
| `exit` | `none` | `none`, `fade`, `up` | Optional departure. Frame roots own transitions, so none is the default. |
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
<div
data-composition-id="titlecard-lockup"
data-composition-src="compositions/components/titlecard-lockup.html"
data-variable-values='{"wordmark":"HYPERFRAMES","label":"WRITE HTML. RENDER VIDEO.","kicker":"INTRODUCING","rule":"show","accent":"green","exit":"none"}'
></div>
```
## Source
<Accordion title={`titlecard-lockup.html`}>
```html
<!doctype html>
<!--
titlecard-lockup: HyperFrames video primitive (intros and reveals / breather)
The calm breather titlecard. An optional mono kicker fades up, the wordmark
settles dead-center with ONE restrained move (scale 0.96 to 1, smooth
ease-out), a hairline rule draws left to right beneath it, a mono label
fades in under the rule, then the lockup holds truly still. Low motion is
the payload: no second development phase, no spring chains, and the hold
is dead still. The stillness is the confidence.
Variables:
- wordmark (string): the centered display wordmark.
- label (string): mono caption under the rule. Empty hides it.
- kicker (string): small mono label above the wordmark. Empty hides it.
- rule (show | hide): the hairline rule and its draw.
- accent (green | blue | violet): contract color carried by the rule.
- exit (none | fade | up, default none): optional departure.
Envelope: fixed IN, optional OUT, elastic HOLD only.
IN_BASE = 1.80s kicker, wordmark settle, rule draw, label, in sequence
HOLD = max(0, D - IN - OUT), truly still: no drift, no breath
OUT_BASE = 0.50s when exit is fade or up, otherwise 0
If D is shorter than IN_BASE + OUT_BASE the phases compress together.
Longer mounts stretch HOLD only. The timeline is never time-scaled.
The rule draw uses getTotalLength dash measurement (never the pathLength
attribute, never non-scaling-stroke on a dashed path).
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 literal
titlecard-lockup key.
-->
<html
lang="en"
data-composition-id="titlecard-lockup"
data-composition-duration="4"
data-composition-variables='[
{ "id": "wordmark", "type": "string", "role": "content", "label": "Wordmark", "description": "Display wordmark that settles dead-center.", "default": "HYPERFRAMES" },
{ "id": "label", "type": "string", "role": "content", "label": "Label", "description": "Mono caption under the rule. Empty hides it.", "default": "WRITE HTML. RENDER VIDEO." },
{ "id": "kicker", "type": "string", "role": "content", "label": "Kicker", "description": "Small mono label above the wordmark. Empty hides it.", "default": "INTRODUCING" },
{ "id": "rule", "type": "enum", "role": "style", "label": "Rule", "description": "Hairline rule drawn left to right under the wordmark.", "default": "show", "options": [{ "value": "show", "label": "Show" }, { "value": "hide", "label": "Hide" }] },
{ "id": "accent", "type": "enum", "role": "style", "label": "Accent", "description": "Contract color carried by the hairline rule.", "default": "green", "options": [{ "value": "green", "label": "Green" }, { "value": "blue", "label": "Blue" }, { "value": "violet", "label": "Violet" }] },
{ "id": "exit", "type": "enum", "role": "style", "label": "Exit", "description": "Optional departure. Frame roots own transitions, so none is the default.", "default": "none", "options": [{ "value": "none", "label": "None" }, { "value": "fade", "label": "Fade" }, { "value": "up", "label": "Up" }] }
]'
>
<head>
<meta charset="UTF-8" />
<title>Titlecard Lockup</title>
</head>
<body>
<template>
<div id="root" data-composition-id="titlecard-lockup" 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;
color: var(--fg, #f8fafc);
font-family: var(--font-display, Inter, system-ui, sans-serif);
pointer-events: none;
}
#root [hidden] {
display: none !important;
}
.tl-clip {
position: absolute;
inset: 0;
display: grid;
place-items: center;
overflow: hidden;
background: var(--bg, #0b0c0e);
}
.tl-lockup {
display: flex;
flex-direction: column;
align-items: center;
max-width: 92cqw;
text-align: center;
will-change: transform, opacity;
}
.tl-kicker {
display: block;
margin-bottom: var(--space-2, 3.4cqh);
/* Light-safe ink derived from the host's own fg/bg instead of
--muted (whose fallback is dark-tuned and fails contrast on
light hosts). 62% fg clears 4.5:1 on both dark and light. */
color: color-mix(in srgb, var(--fg, #f8fafc) 62%, var(--bg, #0b0c0e));
font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
font-size: 2.6cqmin;
font-weight: 600;
line-height: 1;
letter-spacing: 0.3em;
text-indent: 0.3em; /* balances the trailing tracking so the line optically centers */
text-transform: uppercase;
will-change: transform, opacity;
}
.tl-wordmark {
display: block;
max-width: 92cqw;
color: var(--fg, #f8fafc);
font-family: var(--font-display, Inter, system-ui, sans-serif);
font-size: var(--tl-wordmark-size, min(14cqw, 22cqh));
font-weight: 600;
line-height: 1;
letter-spacing: -0.03em;
white-space: nowrap;
transform-origin: 50% 50%;
will-change: transform, opacity;
}
.tl-rule-svg {
display: block;
width: 34cqw;
height: max(1px, 0.2cqmin);
margin-top: var(--space-2, 4cqh);
}
.tl-rule-svg line {
stroke: color-mix(
in srgb,
var(--tl-accent, var(--brand, #71f5a7)) 60%,
var(--fg, #f8fafc)
);
stroke-width: 2;
stroke-opacity: 0.62;
}
.tl-label {
display: block;
margin-top: var(--space-2, 3.2cqh);
/* Same fg/bg-derived ink as the kicker: contrast-safe on any host. */
color: color-mix(in srgb, var(--fg, #f8fafc) 62%, var(--bg, #0b0c0e));
font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
font-size: 2.2cqmin;
font-weight: 600;
line-height: 1.4;
letter-spacing: 0.34em;
text-indent: 0.34em;
text-transform: uppercase;
will-change: transform, opacity;
}
</style>
<div
id="titlecard-lockup-clip"
class="tl-clip clip"
data-start="0"
data-duration="4"
data-track-index="0"
>
<div class="tl-lockup">
<span class="tl-kicker"></span>
<span class="tl-wordmark"></span>
<svg
class="tl-rule-svg"
viewBox="0 0 560 2"
preserveAspectRatio="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<line class="tl-rule" x1="0" y1="1" x2="560" y2="1" />
</svg>
<span class="tl-label"></span>
</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 lockup = root.querySelector(".tl-lockup");
var kicker = root.querySelector(".tl-kicker");
var wordmark = root.querySelector(".tl-wordmark");
var ruleSvg = root.querySelector(".tl-rule-svg");
var ruleLine = root.querySelector(".tl-rule");
var label = root.querySelector(".tl-label");
var vars =
window.__hyperframes && window.__hyperframes.getVariables
? window.__hyperframes.getVariables()
: {};
function readText(value, fallback) {
if (value == null) return fallback;
return String(value).trim().replace(/\s+/g, " ");
}
var wordmarkText = readText(vars.wordmark, "HYPERFRAMES");
var labelText = readText(vars.label, "WRITE HTML. RENDER VIDEO.");
var kickerText = readText(vars.kicker, "INTRODUCING");
var ruleShown = vars.rule !== "hide";
var accentColors = {
green: "var(--brand, #71f5a7)",
blue: "var(--accent, #61a8ff)",
violet: "var(--accent-2, #c5a3ff)",
};
var accent = Object.prototype.hasOwnProperty.call(accentColors, vars.accent)
? vars.accent
: "green";
var exit = vars.exit === "fade" || vars.exit === "up" ? vars.exit : "none";
wordmark.textContent = wordmarkText;
lockup.setAttribute("aria-label", wordmarkText);
root.style.setProperty("--tl-accent", accentColors[accent]);
if (kickerText) kicker.textContent = kickerText;
else kicker.setAttribute("hidden", "hidden");
if (labelText) label.textContent = labelText;
else label.setAttribute("hidden", "hidden");
if (!ruleShown) ruleSvg.setAttribute("hidden", "hidden");
// Deterministic character-count fit keeps the wordmark on one line
// with air on both sides before the template is laid out. The 0.78
// advance estimate is conservative so wide themed display faces
// (serifs) still fit inside the frame.
var characters = Math.max(1, Array.from(wordmarkText).length);
var fittedCqw = Math.min(12, 84 / (characters * 0.78));
root.style.setProperty(
"--tl-wordmark-size",
"min(" + fittedCqw.toFixed(3) + "cqw, 22cqh)",
);
var IN_BASE = 1.8;
var OUT_BASE = exit === "none" ? 0 : 0.5;
var parsedDuration = Number.parseFloat(root.dataset.duration);
var duration = Number.isFinite(parsedDuration) ? Math.max(0.001, parsedDuration) : 4;
var totalBase = IN_BASE + OUT_BASE;
var phaseScale = duration < totalBase ? duration / totalBase : 1;
var IN = IN_BASE * phaseScale;
var OUT = OUT_BASE * phaseScale;
var HOLD = Math.max(0, duration - IN - OUT);
var OUT_START = IN + HOLD;
var KICKER_IN = 0.45 * phaseScale;
var WORD_AT = 0.25 * phaseScale;
var WORD_IN = 0.85 * phaseScale;
var RULE_AT = 0.9 * phaseScale;
var RULE_IN = 0.55 * phaseScale;
var LABEL_AT = 1.3 * phaseScale;
var LABEL_IN = 0.5 * phaseScale;
var tl = gsap.timeline({ paused: true });
// Optional kicker fades up first. An empty kicker leaves a beat of
// quiet instead of reflowing the schedule.
if (kickerText) {
tl.fromTo(
kicker,
{ autoAlpha: 0, y: "2.2cqh" },
{ autoAlpha: 1, y: "0cqh", duration: KICKER_IN, ease: "power2.out" },
0,
);
}
// The ONE move: the wordmark settles dead-center, scale 0.96 to 1,
// smooth ease-out, entrance half only, no bounce.
tl.fromTo(
wordmark,
{ autoAlpha: 0, scale: 0.96 },
{ autoAlpha: 1, scale: 1, duration: WORD_IN, ease: "power3.out" },
WORD_AT,
);
// Hairline rule draws left to right via measured-length dash.
if (ruleShown) {
var ruleLength = ruleLine.getTotalLength();
ruleLine.style.strokeDasharray = String(ruleLength);
ruleLine.style.strokeDashoffset = String(ruleLength);
tl.fromTo(
ruleLine,
{ strokeDashoffset: ruleLength },
{ strokeDashoffset: 0, duration: RULE_IN, ease: "power2.out" },
RULE_AT,
);
}
// Mono label fades up beneath the rule.
if (labelText) {
tl.fromTo(
label,
{ autoAlpha: 0, y: "1.8cqh" },
{ autoAlpha: 1, y: "0cqh", duration: LABEL_IN, ease: "power2.out" },
LABEL_AT,
);
}
// TRUE hold: dead still. No drift, no breath; sub-perceptual
// motion reads as noise at 1080p, and the stillness is the point.
// Optional exit. Frame roots own transitions; none is the default
// and leaves the held lockup as the final frame.
if (exit !== "none") {
var exitProps = { autoAlpha: 0, duration: OUT, ease: "power2.in" };
if (exit === "up") exitProps.y = "-2.4cqh";
tl.to(lockup, exitProps, OUT_START);
}
tl.seek(0);
window.__timelines = window.__timelines || {};
window.__timelines["titlecard-lockup"] = tl;
})();
</script>
</div>
</template>
</body>
</html>
```
</Accordion>
{/* hf:generated-footer */}
Tagged `motion-primitive` `typography` `title-card` `lockup` `breather` `orient`.
## Related topics
- [Browse the complete Catalog](/catalog)
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
- [Build a richer composition](/go-further)