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

# Streaming Text

> Words arrive the way an AI answer streams - in uneven bursts on a measured token rhythm, each word landing grey and inking to full colour - rather than the even tick of a typewriter

# Streaming Text

Words arrive the way an AI answer streams - in uneven bursts on a measured token rhythm, each word landing grey and inking to full colour - rather than the even tick of a typewriter

**Streaming Text**

```html streaming-text.html theme={null}
<!--
  Streaming Text - AI-answer token-flow primitive.

  Paste the markup, CSS and script into a composition. Use the timeline
  integration note at the bottom as the starting point for deterministic GSAP
  animation.

  This is not a typewriter. A typewriter is even; an AI answer arrives in
  bursts - two words together, a pause, three more. The GAPS table in the
  integration note is that rhythm, measured word-by-word from reference
  footage of a real streamed answer, and each word lands grey and inks to
  full colour over 0.27s, the way fresh tokens read before the eye settles.

  Wrap each word in a span with class "w". The script only styles; the
  timeline recipe reveals.

  Variables. The script reads each one, falls back to the declared default on
  anything missing or unrecognised, and writes the result as a custom property
  the CSS below consumes.

    - size (18 to 64, default 27, in px): type size.
    - tone (dark-on-light | light-on-dark, default dark-on-light): the ink
      pair. Fresh-token grey is chosen to sit between the two.

  On every default the result matches the measured reference: 27px ink on a
  light chat ground.
-->

<div
  data-hf-ui-root
  class="hf-ui-streaming-text"
  aria-label="Streamed answer text"
  data-composition-variables='[
    { "id": "size", "type": "number", "role": "style", "label": "Size", "description": "Type size of the streamed words.", "default": 27, "min": 18, "max": 64, "step": 1, "unit": "px" },
    { "id": "tone", "type": "enum", "role": "style", "label": "Tone", "description": "Ink pair: dark words on a light ground, or light words on a dark ground.", "default": "dark-on-light", "options": [{ "value": "dark-on-light", "label": "Dark on light" }, { "value": "light-on-dark", "label": "Light on dark" }] }
  ]'
>
  <p class="hf-st-p">
    <span class="w">You</span> <span class="w">don&#39;t</span> <span class="w">need</span>
    <span class="w">a</span> <span class="w">video</span> <span class="w">editor.</span>
    <span class="w">HyperFrames</span> <span class="w">renders</span> <span class="w">the</span>
    <span class="w">HTML</span> <span class="w">you</span> <span class="w">already</span>
    <span class="w">write</span> <span class="w">into</span> <span class="w">pixel-perfect,</span>
    <span class="w">deterministic</span> <span class="w">video.</span>
  </p>
</div>

<style>
  /* hf-ui:tokens:start */
  /* prettier-ignore */
  [data-hf-ui-root] {
    --_hf-ui-accent: var(--hf-ui-accent, #3ce6ac);
    --_hf-ui-action: var(--hf-ui-action, #eeeeee);
    --_hf-ui-action-contrast: var(--hf-ui-action-contrast, #111111);
    --_hf-ui-border: var(--hf-ui-border, #2a2a2a);
    --_hf-ui-border-strong: var(--hf-ui-border-strong, #383838);
    --_hf-ui-canvas: var(--hf-ui-canvas, #0a0a0a);
    --_hf-ui-control-border: var(--hf-ui-control-border, #686868);
    --_hf-ui-control-border-hover: var(--hf-ui-control-border-hover, #777777);
    --_hf-ui-control-height: var(--hf-ui-control-height, 40px);
    --_hf-ui-danger: var(--hf-ui-danger, #ff7878);
    --_hf-ui-data-1: var(--hf-ui-data-1, #3ce6ac);
    --_hf-ui-data-2: var(--hf-ui-data-2, #63a5ff);
    --_hf-ui-data-3: var(--hf-ui-data-3, #e9ab55);
    --_hf-ui-data-4: var(--hf-ui-data-4, #a78bfa);
    --_hf-ui-data-5: var(--hf-ui-data-5, #ff8a75);
    --_hf-ui-duration-drawer-enter: var(--hf-ui-duration-drawer-enter, 360ms);
    --_hf-ui-duration-drawer-exit: var(--hf-ui-duration-drawer-exit, 240ms);
    --_hf-ui-duration-modal-enter: var(--hf-ui-duration-modal-enter, 280ms);
    --_hf-ui-duration-modal-exit: var(--hf-ui-duration-modal-exit, 180ms);
    --_hf-ui-duration-overlay-enter: var(--hf-ui-duration-overlay-enter, 210ms);
    --_hf-ui-duration-overlay-exit: var(--hf-ui-duration-overlay-exit, 140ms);
    --_hf-ui-duration-press: var(--hf-ui-duration-press, 120ms);
    --_hf-ui-duration-state: var(--hf-ui-duration-state, 160ms);
    --_hf-ui-ease-enter: var(--hf-ui-ease-enter, cubic-bezier(0.32, 0.72, 0, 1));
    --_hf-ui-ease-exit: var(--hf-ui-ease-exit, cubic-bezier(0.23, 1, 0.32, 1));
    --_hf-ui-ease-response: var(--hf-ui-ease-response, cubic-bezier(0.23, 1, 0.32, 1));
    --_hf-ui-font-mono: var(--hf-ui-font-mono, ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace);
    --_hf-ui-font-sans: var(--hf-ui-font-sans, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
    --_hf-ui-hit-target: var(--hf-ui-hit-target, 44px);
    --_hf-ui-radius-control: var(--hf-ui-radius-control, 5px);
    --_hf-ui-radius-overlay: var(--hf-ui-radius-overlay, 10px);
    --_hf-ui-radius-surface: var(--hf-ui-radius-surface, 7px);
    --_hf-ui-shadow-contact: var(--hf-ui-shadow-contact, 0 8px 18px rgba(0, 0, 0, 0.38));
    --_hf-ui-shadow-inset: var(--hf-ui-shadow-inset, inset 0 1px 0 rgba(255, 255, 255, 0.035));
    --_hf-ui-shadow-modal: var(--hf-ui-shadow-modal, 0 18px 46px rgba(0, 0, 0, 0.52));
    --_hf-ui-space-1: var(--hf-ui-space-1, 4px);
    --_hf-ui-space-12: var(--hf-ui-space-12, 48px);
    --_hf-ui-space-16: var(--hf-ui-space-16, 64px);
    --_hf-ui-space-2: var(--hf-ui-space-2, 8px);
    --_hf-ui-space-3: var(--hf-ui-space-3, 12px);
    --_hf-ui-space-4: var(--hf-ui-space-4, 16px);
    --_hf-ui-space-6: var(--hf-ui-space-6, 24px);
    --_hf-ui-space-8: var(--hf-ui-space-8, 32px);
    --_hf-ui-surface: var(--hf-ui-surface, #141414);
    --_hf-ui-surface-inset: var(--hf-ui-surface-inset, #101010);
    --_hf-ui-surface-raised: var(--hf-ui-surface-raised, #1a1a1a);
    --_hf-ui-text: var(--hf-ui-text, #e5e5e5);
    --_hf-ui-text-faint: var(--hf-ui-text-faint, #7e7e7e);
    --_hf-ui-text-muted: var(--hf-ui-text-muted, #929292);
    --_hf-ui-text-strong: var(--hf-ui-text-strong, #f3f3f3);
    --_hf-ui-warning: var(--hf-ui-warning, #e9ab55);
    --_hf-ui-weight-medium: var(--hf-ui-weight-medium, 500);
    --_hf-ui-weight-regular: var(--hf-ui-weight-regular, 400);
    --_hf-ui-weight-semibold: var(--hf-ui-weight-semibold, 600);
    box-sizing: border-box;
    color-scheme: dark;
    font-family: var(--_hf-ui-font-sans);
  }
  /* prettier-ignore */
  [data-hf-ui-root] *,
  [data-hf-ui-root] *::before,
  [data-hf-ui-root] *::after {
    box-sizing: border-box;
  }
  /* prettier-ignore */
  [data-hf-theme="light"] [data-hf-ui-root],
  [data-hf-ui-root][data-hf-theme="light"] {
    --_hf-ui-accent: var(--hf-ui-accent, #087a57);
    --_hf-ui-action: var(--hf-ui-action, #171717);
    --_hf-ui-action-contrast: var(--hf-ui-action-contrast, #ffffff);
    --_hf-ui-border: var(--hf-ui-border, #e0dfdb);
    --_hf-ui-border-strong: var(--hf-ui-border-strong, #cbc9c3);
    --_hf-ui-canvas: var(--hf-ui-canvas, #f6f5f1);
    --_hf-ui-control-border: var(--hf-ui-control-border, #888888);
    --_hf-ui-control-border-hover: var(--hf-ui-control-border-hover, #6f6e68);
    --_hf-ui-danger: var(--hf-ui-danger, #a61b1b);
    --_hf-ui-data-1: var(--hf-ui-data-1, #087a57);
    --_hf-ui-data-2: var(--hf-ui-data-2, #245ea8);
    --_hf-ui-data-3: var(--hf-ui-data-3, #8a4b00);
    --_hf-ui-data-4: var(--hf-ui-data-4, #6d3fc0);
    --_hf-ui-data-5: var(--hf-ui-data-5, #a83d2d);
    --_hf-ui-shadow-contact: var(--hf-ui-shadow-contact, 0 7px 16px rgba(26, 24, 20, 0.13));
    --_hf-ui-shadow-inset: var(--hf-ui-shadow-inset, inset 0 1px 0 rgba(255, 255, 255, 0.78));
    --_hf-ui-shadow-modal: var(--hf-ui-shadow-modal, 0 18px 46px rgba(26, 24, 20, 0.18));
    --_hf-ui-surface: var(--hf-ui-surface, #ffffff);
    --_hf-ui-surface-inset: var(--hf-ui-surface-inset, #f0efeb);
    --_hf-ui-surface-raised: var(--hf-ui-surface-raised, #ffffff);
    --_hf-ui-text: var(--hf-ui-text, #171717);
    --_hf-ui-text-faint: var(--hf-ui-text-faint, #6f6e68);
    --_hf-ui-text-muted: var(--hf-ui-text-muted, #5f5f5a);
    --_hf-ui-text-strong: var(--hf-ui-text-strong, #0a0a0a);
    --_hf-ui-warning: var(--hf-ui-warning, #8a4b00);
    color-scheme: light;
  }
  /* prettier-ignore */
  [data-hf-ui-root]:focus-visible {
    outline: 2px solid var(--_hf-ui-accent);
    outline-offset: 3px;
  }
  /* prettier-ignore */
  [data-hf-rendering="true"][data-hf-ui-root],
  [data-hf-rendering="true"] [data-hf-ui-root],
  [data-hf-rendering="true"][data-hf-ui-root] *,
  [data-hf-rendering="true"] [data-hf-ui-root] *,
  [data-hf-rendering="true"][data-hf-ui-root]::before,
  [data-hf-rendering="true"][data-hf-ui-root]::after,
  [data-hf-rendering="true"] [data-hf-ui-root]::before,
  [data-hf-rendering="true"] [data-hf-ui-root]::after,
  [data-hf-rendering="true"][data-hf-ui-root] *::before,
  [data-hf-rendering="true"][data-hf-ui-root] *::after,
  [data-hf-rendering="true"] [data-hf-ui-root] *::before,
  [data-hf-rendering="true"] [data-hf-ui-root] *::after {
    animation-duration: 0s !important;
    animation-delay: 0s !important;
    transition-duration: 0s !important;
    transition-delay: 0s !important;
  }
  /* prettier-ignore */
  @media (prefers-reduced-motion: reduce) {
    [data-hf-ui-root],
    [data-hf-ui-root] *,
    [data-hf-ui-root] *::before,
    [data-hf-ui-root] *::after {
      animation-duration: 0s !important;
      animation-delay: 0s !important;
      transition-duration: 0s !important;
      transition-delay: 0s !important;
      scroll-behavior: auto !important;
    }
  }
  /* hf-ui:tokens:end */

  .hf-ui-streaming-text {
    font-size: var(--hf-st-size, 27px);
    color: var(--hf-st-ink, #141414);
    font-family: var(--_hf-ui-font-sans);
  }
  .hf-st-p {
    margin: 0;
    line-height: 1.43;
    letter-spacing: 0.046em;
    max-width: 24.5em;
  }
  .hf-ui-streaming-text .w {
    opacity: 0;
  }
</style>

<script>
  (function () {
    "use strict";

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

    // Unrecognised overrides return to their declared defaults.
    function pick(table, value, fallback) {
      return Object.prototype.hasOwnProperty.call(table, value) ? value : fallback;
    }

    function clampNumber(value, min, max, fallback) {
      var n = typeof value === "number" ? value : parseFloat(value);
      if (!isFinite(n)) {
        return fallback;
      }
      return Math.min(max, Math.max(min, n));
    }

    var tones = {
      "dark-on-light": { ink: "#141414", fresh: "#767676" },
      "light-on-dark": { ink: "#f3f3f3", fresh: "#5f5f5f" },
    };

    var size = clampNumber(vars.size, 18, 64, 27);
    var tone = tones[pick(tones, vars.tone, "dark-on-light")];

    var roots = document.querySelectorAll(".hf-ui-streaming-text");
    for (var i = 0; i < roots.length; i += 1) {
      roots[i].style.setProperty("--hf-st-size", size + "px");
      roots[i].style.setProperty("--hf-st-ink", tone.ink);
      roots[i].style.setProperty("--hf-st-fresh", tone.fresh);
    }
  })();
</script>

<!--
  Timeline integration - GAPS is the measured token rhythm (seconds between
  consecutive words, zeros are words that arrive together). Cycle it for any
  word count; the ramp inks each word from fresh grey to full colour.

  var GAPS = [
    0.167, 0.100, 0.000, 0.367, 0.000, 0.167, 0.133, 0.100, 0.100, 0.133,
    0.167, 0.133, 0.000, 0.233, 0.167, 0.100, 0.000, 0.133, 0.133, 0.100,
    0.167, 0.133, 0.100, 0.133, 0.167, 0.100, 0.133, 0.100, 0.133,
  ];
  var t = startTime;
  gsap.utils.toArray('.hf-ui-streaming-text .w').forEach(function (w, i) {
    if (i > 0) t += GAPS[i % GAPS.length];
    tl.set(w, { opacity: 1 }, t);
    tl.fromTo(w, { color: '#767676' }, { color: 'var(--hf-st-ink)', duration: 0.2667, ease: 'none' }, t);
  });
-->
```

Copy npx hyperframes add streaming-text to the clipboard

That writes one file: `compositions/components/streaming-text.html`.

## Paste it into your composition

Open `compositions/components/streaming-text.html` and copy what is inside into your own composition.
The file opens with a comment header that walks you through it.

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 |
| - | - | - | - |
| `size` | `27` | 18px to 64px, step 1px | Type size of the streamed words. |
| `tone` | `dark-on-light` | `dark-on-light`, `light-on-dark` | Ink pair: dark words on a light ground, or light words on a dark ground. |

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 theme={null}
<div
  data-composition-id="streaming-text"
  data-composition-src="compositions/components/streaming-text.html"
  data-variable-values='{"size":27,"tone":"dark-on-light"}'
></div>
```

Tagged `video-primitive` `text-effect` `ai` `chat`.

Created by Miao Yang.

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


## Related topics

- [Morph Text](/catalog/components/morph-text.md)
- [Text Shimmer](/catalog/components/text-shimmer.md)
- [Text Stagger](/catalog/components/text-stagger.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.