Getting started
Install
Use the package manager your project already uses:
npm install smooth-stream-text
# pnpm add smooth-stream-text
# yarn add smooth-stream-textThe package has no runtime dependencies. React (18 or newer) and Vue (3.2 or newer) are optional peer dependencies: nothing extra is installed, and you only need the framework you import from. It ships ES modules, CommonJS and TypeScript types for every entry point.
The one idea
Every integration works the same way. On each change you hand over two things:
- the whole text received so far for the current answer, and
- whether that answer is done, meaning the transport has closed it.
You get back the visible text: a prefix of what you handed over that grows at an even rate, and a phase of 'idle', 'streaming', 'settling' or 'done'.
Three rules follow from that, and they hold in every binding:
- Text that is already done the first time you hand it over is history. It appears at once, without pacing or animation, so a loaded conversation does not replay.
- Text that does not continue the previous text is a different answer. If it is already done, it is shown at once; if it is still streaming (a regenerated answer, say), it is paced from the start.
donecloses whichever answer is current. The rest of the backlog then drains to the end within about a second (twicesettleMs, 400 ms by default), even if the last chunk arrives afterdonewas set.
React
import { useSmoothText } from 'smooth-stream-text/react'
function Answer({ answer, streaming }: { answer: string; streaming: boolean }) {
const { text, phase } = useSmoothText(answer, { done: !streaming })
return <Markdown>{text}</Markdown>
}useSmoothText re-renders only when the visible text or the phase changes: at most once per animation frame while text is appearing. The entry point is marked 'use client', so it can be imported from Next.js App Router client components. More in the React guide.
Vue
<script setup lang="ts">
import { useSmoothStream } from 'smooth-stream-text/vue'
const props = defineProps<{ answer: string; streaming: boolean }>()
const { text } = useSmoothStream(() => props.answer, { done: () => !props.streaming })
</script>
<template>
<MyMarkdown :source="text" />
</template>More in the Vue and Nuxt guide.
Svelte
<script lang="ts">
import { smoothStream } from 'smooth-stream-text/svelte'
import { onDestroy } from 'svelte'
let { answer, streaming } = $props()
const smooth = smoothStream()
$effect(() => smooth.apply(answer, !streaming))
onDestroy(smooth.destroy)
</script>
<p>{$smooth.text}</p>More in the Svelte and SvelteKit guide.
Plain JavaScript
When the text is plain, the DOM handle does pacing and the fade-in reveal in one element:
import { createSmoothText } from 'smooth-stream-text/dom'
const answer = createSmoothText(document.querySelector('#answer')!)
for await (const delta of stream) answer.push(delta)
answer.end()The plain JavaScript guide also covers the <smooth-stream-text> Web Component, which works in Angular, Solid, Astro and any other framework that renders custom elements.
Without a DOM
The core is plain logic. It runs in Node 18 and newer, in workers and in tests:
import { createSmoothStream } from 'smooth-stream-text'
const stream = createSmoothStream()
stream.subscribe((state) => render(state.text))
for await (const delta of response) stream.push(delta)
stream.end()push takes deltas and setSource takes the growing full string; use whichever your transport gives you. See the API reference.
Settings that matter first
Defaults are chosen so that most chats need none. The ones worth knowing:
| Option | Default | What it changes |
|---|---|---|
targetLatencyMs | 180 | How far behind the live text the display runs. Lower feels more immediate, higher is smoother. |
unit | 'char' | 'word' adds only whole words, in every script. |
maxCharsPerSecond | 900 | Ceiling on the rate, so a burst is never dumped at once. |
settleMs | 400 | How long the tail takes to drain once the answer is done. |
All of them, and the reveal settings, are in the API reference.
Next
- Wire it into your stack: AI SDK, Next.js, Streamdown.
- Understand what pacing costs in time: latency and completion.
- See where the visible text may end: text boundaries.