# StreamingText

> Renders a growing string as a live token stream: the appended delta lands tinted and settles, with an optional block cursor while the response is in flight.

- Package: `fancy-ui-svelte` (Svelte 5 + Tailwind CSS v4)
- Category: AI Chat
- Tags: ai, chat, streaming, text, reasoning
- Docs: https://fancy-ui.rama.app/docs/components/streaming-text
- Source: https://github.com/RamaHerbin/fancy-ui/tree/main/src/lib/fancy-ui/streaming-text

## Import

```svelte
<script lang="ts">
  import { StreamingText } from 'fancy-ui-svelte';
</script>
```

Setup (once per project): `npm install fancy-ui-svelte`, then add `@import "fancy-ui-svelte/tailwind.css";` after `@import "tailwindcss";` in your main CSS file. Full guide: https://fancy-ui.rama.app/llms.txt

React: `import { StreamingText } from 'fancy-ui-react';` (same prop names; `class` becomes `className`, snippets become `ReactNode`).

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `text` (required) | `string` | — | The accumulated text so far, not the latest delta; reassign as chunks arrive |
| `streaming` | `boolean` | `false` | Show a soft block cursor after the last character |
| `markdown` | `boolean` | `false` | Render as markdown instead of the tinted plain-text stream; disables the delta tint |
| `settleMs` | `number` | `350` | How long a newly arrived chunk stays tinted in ms, plain mode only |
| `tintColor` | `string` | — | Colour a chunk fades from, and the cursor's fill; sets --ft-tint-color |
| `onComplete` | `() => void` | — | Called once when streaming goes from true to false |

## Examples

### Basic Usage

```svelte
<script lang="ts">
	import { onMount } from "svelte";
	import { StreamingText } from "fancy-ui-svelte";

	const source =
		"A model answers one token at a time, so the surface it lands on should show that. Each chunk arrives tinted and settles into the paragraph a moment later, which makes the seam between what was already there and what just came in legible without moving any of the text around.";

	const words = source.split(" ");

	let text = $state("");
	let streaming = $state(false);

	onMount(() => {
		// A looping demo that keeps re-animating is exactly what reduced motion is
		// asking us not to do, so it gets the finished answer instead.
		if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
			text = source;
			return;
		}

		let timer: ReturnType<typeof setTimeout>;
		let i = 0;

		function tick() {
			if (i < words.length) {
				// The whole text so far, never the delta — that is the contract.
				text = i === 0 ? words[0] : `${text} ${words[i]}`;
				i += 1;
				timer = setTimeout(tick, 90);
				return;
			}
			streaming = false;
			timer = setTimeout(restart, 2400);
		}

		function restart() {
			text = "";
			i = 0;
			streaming = true;
			timer = setTimeout(tick, 160);
		}

		restart();
		return () => clearTimeout(timer);
	});
</script>

<div class="bg-card w-full max-w-xl rounded-lg border p-5">
	<p class="text-muted-foreground mb-3 text-xs font-medium tracking-wide uppercase">Assistant</p>
	<StreamingText {text} {streaming} class="text-sm leading-relaxed" tintColor="#6366f1" />
</div>
```

### Markdown Streaming

```svelte
<script lang="ts">
	import { onMount } from "svelte";
	import { StreamingText } from "fancy-ui-svelte";

	const source = `**Markdown mode** re-parses the whole document on every chunk, so structure appears the moment it becomes valid:

- a bullet resolves as soon as its line is complete
- inline \`code\` and **emphasis** snap in mid-sentence

\`\`\`ts
const res = await fetch("/api/chat");
\`\`\`

There is no delta tint here — only plain mode can wrap new text in a span of its own.`;

	const words = source.split(" ");

	let text = $state("");
	let streaming = $state(false);

	onMount(() => {
		if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
			text = source;
			return;
		}

		let timer: ReturnType<typeof setTimeout>;
		let i = 0;

		function tick() {
			if (i < words.length) {
				text = i === 0 ? words[0] : `${text} ${words[i]}`;
				i += 1;
				timer = setTimeout(tick, 90);
				return;
			}
			streaming = false;
			timer = setTimeout(restart, 2400);
		}

		function restart() {
			text = "";
			i = 0;
			streaming = true;
			timer = setTimeout(tick, 160);
		}

		restart();
		return () => clearTimeout(timer);
	});
</script>

<div class="bg-card w-full max-w-xl rounded-lg border p-5">
	<p class="text-muted-foreground mb-3 text-xs font-medium tracking-wide uppercase">Assistant</p>
	<StreamingText {text} {streaming} markdown class="text-sm" />
</div>
```
