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

# Zoom Through Transition

> A transition that zooms through a focal card into the next scene.

# Zoom Through Transition

A transition that zooms through a focal card into the next scene.

**Zoom Through Transition**

```html zoom-through-transition.html
<!doctype html>
<!--
  Zoom Through Transition - a scene push that wipes the incoming scene through
  the outgoing one while the outgoing one drifts back and blurs.

  Mount contract: MOUNTABLE SUB-COMPOSITION. The runtime clones only <template>
  contents; #root fills the host box (inset: 0, container-type: size), and the
  file registers one paused timeline under the literal "zoom-through-transition"
  key. Variables come from window.__hyperframes.getVariables().

  This file owns its motion. It previously shipped as a fragment whose only
  executable timeline lived in demo.html, so an installed copy rendered the
  finished pose and never moved.

  Variables (declared in data-composition-variables below):
    - from-label (string, default "Before"): caption on the outgoing scene.
    - to-label (string, default "Zoom Through Transition"): caption on the
      incoming scene, clipped out of frame until the wipe brings it in.
    - depth (close | standard | far, default standard): multiplies how far both
      scenes travel and how hard the outgoing one blurs. Every input it
      multiplies is zero at rest, so it collapses to the shipped pose there. It
      deliberately leaves the two scale tracks alone: the timeline drives those
      from their own start values, which a multiplier would distort.
    - accent (blue | green | violet, default blue): ink of the ring that marks
      the incoming scene. Both scenes are monochrome placeholder material; the
      ring is the one accent-coloured element. blue rides --accent, green rides
      --brand, violet rides --accent-2, and each falls back to a neutral
      hairline so an unthemed composition renders in black and white.

  Envelope: the push starts at 0.35s (clamped to fit shorter durations), runs
  0.9s, then holds the incoming scene for whatever is left.
-->
<html
  lang="en"
  data-composition-id="zoom-through-transition"
  data-composition-duration="3"
  data-composition-variables='[
    { "id": "from-label", "type": "string", "role": "content", "label": "Outgoing label", "description": "Caption on the outgoing scene, the one on screen when the transition starts.", "default": "Before" },
    { "id": "to-label", "type": "string", "role": "content", "label": "Incoming label", "description": "Caption on the incoming scene, the one the timeline wipes into frame.", "default": "Zoom Through Transition" },
    { "id": "depth", "type": "enum", "role": "motion", "label": "Depth", "description": "Multiplies how far both scenes travel and how hard the outgoing one blurs, so the push covers a shorter or longer run in the same time.", "default": "standard", "options": [{ "value": "close", "label": "Close" }, { "value": "standard", "label": "Standard" }, { "value": "far", "label": "Far" }] },
    { "id": "accent", "type": "enum", "role": "style", "label": "Accent", "description": "Theme token for the ring marking the incoming scene. Blue rides --accent, green --brand, violet --accent-2; each falls back to a neutral hairline.", "default": "blue", "options": [{ "value": "blue", "label": "Blue" }, { "value": "green", "label": "Green" }, { "value": "violet", "label": "Violet" }] }
  ]'
>
  <head>
    <meta charset="UTF-8" />
    <title>Zoom Through Transition</title>
  </head>
  <body>
    <template>
      <div id="root" data-composition-id="zoom-through-transition" data-duration="3" data-fps="30">
        <style>
          *,
          *::before,
          *::after {
            box-sizing: border-box;
          }

          /* Root fills the host-owned box; the scenes measure against it. */
          #root {
            position: absolute;
            inset: 0;
            container-type: size;
            isolation: isolate;
            overflow: hidden;
            background: #111827;
          }

          .hf-catalog-zoom-through-transition {
            --hf-accent: var(--accent, rgba(255, 255, 255, 0.28));
            --hf-l1: rgba(255, 255, 255, 0.72);
            --hf-l3: rgba(255, 255, 255, 0.18);
            --hf-l4: rgba(255, 255, 255, 0.08);
            --hf-hair: rgba(255, 255, 255, 0.14);
            --hf-ink: #111827;
            --hf-muted: #6b7280;
            --hf-surface: #ffffff;
            color: var(--hf-ink);
            font-family: Inter, system-ui, sans-serif;
          }
          .hf-catalog-zoom-through-transition {
            position: absolute;
            inset: 0;
            overflow: hidden;
            background: #111827;
          }
          .hf-catalog-zoom-through-transition .scene {
            position: absolute;
            inset: 0;
            display: grid;
            place-items: center;
            font-weight: 900;
            font-size: 13.5cqmin;
            letter-spacing: -0.05em;
            color: #fff;
          }
          .hf-catalog-zoom-through-transition .from {
            background:
              repeating-linear-gradient(
                90deg,
                rgba(255, 255, 255, 0.04) 0 1px,
                transparent 1px 4px
              ),
              var(--hf-l4);
            transform: translate(
                calc(var(--hf-from-x, 0px) * var(--hf-depth, 1)),
                calc(var(--hf-from-y, 0px) * var(--hf-depth, 1))
              )
              scale(var(--hf-from-scale, 1));
            filter: blur(calc(var(--hf-from-blur, 0px) * var(--hf-depth, 1)));
          }
          .hf-catalog-zoom-through-transition .to {
            background:
              repeating-linear-gradient(
                90deg,
                rgba(255, 255, 255, 0.05) 0 1px,
                transparent 1px 4px
              ),
              var(--hf-l3);
            box-shadow: inset 0 0 0 2px var(--hf-accent);
            transform: translate(
                calc(var(--hf-to-x, 0px) * var(--hf-depth, 1)),
                calc(var(--hf-to-y, 0px) * var(--hf-depth, 1))
              )
              scale(var(--hf-to-scale, 1));
            clip-path: inset(
              var(--hf-clip-top, 0%) var(--hf-clip-right, 100%) var(--hf-clip-bottom, 0%)
                var(--hf-clip-left, 0%)
            );
          }
          .hf-catalog-zoom-through-transition .fx {
            position: absolute;
            inset: 0;
            background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.75), transparent);
            mix-blend-mode: screen;
            transform: translateX(var(--hf-fx-x, -110%));
          }
          .hf-catalog-zoom-through-transition .rgb {
            position: absolute;
            inset: 0;
            pointer-events: none;
            opacity: var(--hf-rgb-opacity, 0);
            background: linear-gradient(
              90deg,
              rgba(239, 68, 68, 0.35),
              transparent,
              rgba(34, 211, 238, 0.35)
            );
            transform: translateX(var(--hf-rgb-x, 0px));
          }
        </style>

        <div class="hf-catalog-zoom-through-transition">
          >
          <div class="scene from">Before</div>
          <div class="scene to">Zoom Through Transition</div>
          <div class="fx"></div>
          <div class="rgb"></div>
        </div>

        <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
        <script>
          (function () {
            "use strict";

            var vars =
              window.__hyperframes && window.__hyperframes.getVariables
                ? window.__hyperframes.getVariables()
                : {};

            // Unrecognised overrides return to their declared defaults.
            var depths = { close: 0.55, standard: 1, far: 1.7 };
            // Each choice routes to a different contract token so the variable stays
            // meaningful under a theme rather than collapsing into one themed accent.
            var HAIRLINE = "rgba(255, 255, 255, 0.28)";
            var accents = {
              blue: "var(--accent, " + HAIRLINE + ")",
              green: "var(--brand, " + HAIRLINE + ")",
              violet: "var(--accent-2, " + HAIRLINE + ")",
            };

            function pick(table, value, fallback) {
              return Object.prototype.hasOwnProperty.call(table, value) ? value : fallback;
            }

            function label(value) {
              return typeof value === "string" && value !== "" ? value : null;
            }

            var depth = depths[pick(depths, vars.depth, "standard")];
            var accent = accents[pick(accents, vars.accent, "blue")];
            var fromLabel = label(vars["from-label"]);
            var toLabel = label(vars["to-label"]);

            var roots = document.querySelectorAll(".hf-catalog-zoom-through-transition");
            var mountRoot = document.getElementById("root");
            for (var i = 0; i < roots.length; i += 1) {
              var root = roots[i];
              root.style.setProperty("--hf-depth", String(depth));
              root.style.setProperty("--hf-accent", accent);

              var fromScene = root.querySelector(".from");
              var toScene = root.querySelector(".to");
              if (fromLabel !== null && fromScene) {
                fromScene.textContent = fromLabel;
              }
              if (toLabel !== null && toScene) {
                toScene.textContent = toLabel;
              }
            }

            // The motion used to live only in this file's demo page, so an installed
            // copy rendered the finished pose and never moved. The timeline belongs
            // with the markup it drives: build it here, paused, and register it under
            // this composition's own id so the runtime seeks it frame by frame.
            if (!mountRoot || !window.gsap) return;

            var SELF = ".hf-catalog-zoom-through-transition";
            var duration = Math.max(0.001, parseFloat(mountRoot.dataset.duration || "3"));
            // The push reads as a transition only with a beat of the outgoing scene
            // before it. Clamped so the wipe always lands inside the duration.
            var PUSH = 0.9;
            var START = Math.max(0, Math.min(0.35, duration - PUSH));

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

            tl.fromTo(
              SELF + " .to",
              { "--hf-clip-right": "100%", "--hf-to-x": "60px", "--hf-to-scale": 1.04 },
              {
                "--hf-clip-right": "0%",
                "--hf-to-x": "0px",
                "--hf-to-scale": 1,
                duration: PUSH,
                ease: "power3.inOut",
              },
              START,
            );
            tl.fromTo(
              SELF + " .from",
              { "--hf-from-x": "0px", "--hf-from-scale": 1, "--hf-from-blur": "0px" },
              {
                "--hf-from-x": "-70px",
                "--hf-from-scale": 0.96,
                "--hf-from-blur": "5px",
                duration: PUSH,
                ease: "power3.inOut",
              },
              START,
            );
            tl.fromTo(
              SELF + " .fx",
              { "--hf-fx-x": "-110%" },
              { "--hf-fx-x": "110%", duration: 0.58, ease: "power2.inOut" },
              START + 0.18,
            );
            // repeat/yoyo resolves from absolute time on every seek, so the strobe is
            // reproducible at any frame rather than accumulated across ticks.
            tl.fromTo(
              SELF + " .rgb",
              { "--hf-rgb-opacity": 0, "--hf-rgb-x": "-16px" },
              {
                "--hf-rgb-opacity": 1,
                "--hf-rgb-x": "16px",
                repeat: 3,
                yoyo: true,
                duration: 0.07,
                ease: "steps(1)",
              },
              START + 0.32,
            );

            // HOLD: the incoming scene rests for whatever is left of the duration.
            tl.set({}, {}, duration);

            tl.seek(0);

            window.__timelines = window.__timelines || {};
            window.__timelines["zoom-through-transition"] = tl;
          })();
        </script>
      </div>
    </template>
  </body>
</html>
```

Copy npx hyperframes add zoom-through-transition to the clipboard

That writes one file: `compositions/components/zoom-through-transition.html`.

## Paste it into your composition

Open `compositions/components/zoom-through-transition.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 |
| - | - | - | - |
| `from-label` | `Before` | string | Caption on the outgoing scene, the one on screen when the transition starts. |
| `to-label` | `Zoom Through Transition` | string | Caption on the incoming scene, the one the timeline wipes into frame. |
| `depth` | `standard` | `close`, `standard`, `far` | Multiplies how far both scenes travel and how hard the outgoing one blurs, so the push covers a shorter or longer run in the same time. |
| `accent` | `blue` | `blue`, `green`, `violet` | Theme token for the ring marking the incoming scene. Blue rides --accent, green --brand, violet --accent-2; each falls back to a neutral hairline. |

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="zoom-through-transition"
  data-composition-src="compositions/components/zoom-through-transition.html"
  data-variable-values='{"from-label":"Before","to-label":"Zoom Through Transition","depth":"standard","accent":"blue"}'
></div>
```

{/* hf:generated-footer */}

Tagged `transition-primitive`.

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