---
title: "StreamText"
description: "Text that streams in word by word at a steady pace."
category: AiReasoning
status: beta
since: 0.6.3
tags: [ai, streaming, text, reveal, caret]
syncStatus: reviewed
verified: true
---

## Installation

:::TuffCodeBlock{lang="bash"}
---
code: |
  pnpm add @talex-touch/tuffex
---
:::

:::TuffCodeBlock{lang="typescript"}
---
code: |
  import { TxStreamText } from '@talex-touch/tuffex/stream-text'
  import '@talex-touch/tuffex/stream-text/style.css'
  import '@talex-touch/tuffex/inline-citation/style.css' // citation chips
  import '@talex-touch/tuffex/base.css' // once per app
---
:::

## Usage

### Streaming
Pass everything received so far as `content`, and keep `streaming` on while the source is live.
:::TuffDemoWrapper{demo="StreamTextStreamTextDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  const reply = ref('')
  const busy = ref(true)

  for await (const delta of stream)
    reply.value += delta
  busy.value = false
  </script>

  <template>
    <p>
      <TxStreamText :content="reply" :streaming="busy" />
    </p>
  </template>
---
:::

### Reveal Presets
`reveal` sets how a word enters. With complete content, `replay()` plays it again and `reserve` keeps the layout still.
:::TuffDemoWrapper{demo="StreamTextPresetsDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import type { TxStreamTextInstance } from '@talex-touch/tuffex/stream-text'

  const text = ref<TxStreamTextInstance | null>(null)
  </script>

  <template>
    <TxStreamText ref="text" :content="answer" reveal="blur" reserve />
    <TxButton @click="text?.replay()">Replay</TxButton>
  </template>
---
:::

### Slots and State
`content` also takes a list of runs: text with marks and links, citations, and custom runs.
:::TuffDemoWrapper{demo="StreamTextSlotsDemo" code-lang="vue"}
---
code: |
  <template>
    <TxStreamText :content="runs" @state-change="state = $event" @cite="open">
      <template #caret="{ state }">
        <span class="my-caret" :data-state="state" />
      </template>
      <template #citation="{ source, index }">
        <sup>{{ index }}</sup>
      </template>
      <template #inline="{ name, props }">
        <MyDelta v-if="name === 'delta'" :value="props.value" />
      </template>
    </TxStreamText>
  </template>
---
:::

### Best Practices

- Pass the whole text so far, not the latest delta.
- Keep `streaming` true until the source has really finished; leave it off for complete answers (history, replays) and use `replay()` to play them.
- Turn on `reserve` when complete text plays back in a layout that must not move, such as a card or a bubble.
- Copy and share the full text you hold: the display trails the source by up to `maxLagMs`.
- The component is not a live region; wrap the conversation in `role="log"` to announce replies.

## API Reference

### Props

| Name | Type | Default | Description |
|------|------|---------|-------------|
| `content` | `string \| StreamInline[]` | — | Everything received so far. Required. |
| `streaming` | `boolean` | `false` | The source is live; a possibly unfinished last word waits for what follows or 200ms of quiet. |
| `wordMs` | `number` | `24` | Releases one word every `wordMs`. |
| `maxLagMs` | `number` | `600` | No word shows later than this after it arrives. |
| `drainMs` | `number` | `320` | Once `streaming` turns false, releases the rest within this. |
| `pauseMs` | `number` | `400` | Reports `paused` after this long without a new word. |
| `reveal` | `'aurora' \| 'hue' \| 'blur' \| 'languid' \| 'none'` | `'aurora'` | How a word enters; see `StreamRevealPreset`. |
| `caret` | `boolean` | `true` | Shows the caret while live; switched off, it goes at once. |
| `reserve` | `boolean` | `false` | Holds the final layout with an invisible copy during playback; ignored while `streaming`. |
| `paced` | `boolean` | `true` | `false` shows each word as it arrives, still animated; `TxStreamElement` passes it to pace parts itself. |
| `appear` | `boolean` | `false` | Content present at mount enters too. Read at mount. |
| `tag` | `string` | `'span'` | Root element. |
| `locale` | `string` | `'zh'` | Locale for `Intl.Segmenter` word segmentation. |

### Events

| Name | Payload | Description |
|------|---------|-------------|
| `state-change` | `(state: StreamState)` | Fires when the stream changes state. |
| `done` | — | Fires once per play-through, when the last word shows and the source has finished. |
| `cite` | `(source: AiSourceItem)` | Fires when a citation chip opens; the chip never navigates. |

### Slots

| Name | Scope | Description |
|------|-------|-------------|
| `caret` | `{ state }` | Replaces the Tuff caret; rendered only while live. |
| `citation` | `{ source, label, index }` | Replaces the citation chip; `index` is the marker's number. |
| `inline` | `{ name, props }` | Renders `{ type: 'custom' }` runs. |

### Exposed Methods

| Name | Type | Description |
|------|------|-------------|
| `state` | `StreamState` | The current stream state. |
| `replay` | `() => void` | Plays the whole content again from the first word, at `wordMs`. |
| `skip` | `() => void` | Shows everything now, without entrances. |

### Types

:::TuffCodeBlock{lang="typescript"}
---
code: |
  type StreamState = 'idle' | 'streaming' | 'paused' | 'draining' | 'done'

  type StreamRevealPreset =
    | 'aurora' // fades out of a 4px blur through the blue–violet–pink sweep (default)
    | 'hue' // the sweep alone
    | 'blur' // the blur alone
    | 'languid' // rises slowly out of an 8px blur
    | 'none' // shows each word as it is released

  type StreamInline =
    | { type: 'text', text: string, marks?: ('strong' | 'em' | 'del' | 'code')[], href?: string }
    | { type: 'citation', source: AiSourceItem, label?: string, index?: number }
    | { type: 'custom', name: string, props?: Record<string, unknown> }
---
:::

### CSS Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `--tx-stream-reveal-1` / `-2` / `-3` | blue / violet / pink, per theme | The sweep's color stops; all equal the ink in high-contrast themes. |
| `--tx-stream-caret-start` / `--tx-stream-caret-end` | `#199ffe` / `#810dc6` | The caret gradient, from the Tuff logo. |
| `--tx-stream-reveal-duration` | the preset's | Written onto the root from `reveal`; hosts do not set it. |

## Overview

- Words go out every `wordMs`; a burst speeds the release so each word shows within `maxLagMs` of arriving. Once the source stops, the rest drains within `drainMs`.
- States: `idle` → `streaming` → `paused` (no new word for `pauseMs`) → `draining` (source finished, backlog still going out) → `done`; the caret retracts on `done`.
- Only new text enters: content present at mount shows at once, and a rewrite re-releases from the first changed word.
- Text splits by word with `Intl.Segmenter`, punctuation staying with its word; without it, Latin splits on spaces and CJK by character.
- `href` renders only for `http(s)`, `mailto`, `tel`, relative, query, and hash URLs; `//host` stays text. Links carry `rel="noopener noreferrer"`.
- Under reduced motion, nothing enters or is paced and the caret rests; server rendering outputs plain text; the root carries `aria-busy="true"` while live.
- The caret and the `reserve` copy are hidden from assistive technology (the copy is also `inert`); the text is not a live region, so wrap the conversation in `role="log"` to announce replies.

## Technologies

- The pacer `use-stream-pacer.ts` and the `stream-reveal-*` mixins in `style/mixins.scss` are shared by the stream family.
- Motion follows the streaming text of kobra.systems (`hue`) and Beautiful UI (`blur`); the default `aurora` combines both. The caret is drawn from the Tuff logo.
- Source: `packages/tuffex/packages/components/src/stream-text/`.

<TuffDocSourceLink />

## Use cases

- An AI answer arriving token by token, in a chat, a panel, or a notification.
- Replaying a stored answer or a scripted demo at a readable pace, without moving the layout.
- A short generated sentence with sources, where each citation lands on its claim.

## Related components

- [StreamMarkdown](./stream-markdown.en.mdc) renders whole Markdown documents as they stream, with the same presets and caret.
- [CodeStream](./code-stream.en.mdc) streams code.
- [InlineCitation](./inline-citation.en.mdc) is the default citation chip.
- [Sources](./sources.en.mdc) lists the sources at the end of an answer.
