API reference
This page documents version 0.2.2, the current release on npm.
Entry points
| Import | Contains | Needs |
|---|---|---|
smooth-stream-text | The core: pacing, clocks, boundaries, source sync | Nothing. No DOM, runs in Node 18+ |
smooth-stream-text/dom | The reveal and the DOM handle, plus everything from the root | A DOM |
smooth-stream-text/react | Hooks and components, plus the DOM and root exports | React 18 or 19 |
smooth-stream-text/vue | Composable and components, plus the DOM and root exports | Vue 3.2+ |
smooth-stream-text/svelte | Store and actions, plus the DOM and root exports | Nothing from Svelte is imported |
smooth-stream-text/web-component | <smooth-stream-text>, plus the DOM and root exports | Custom elements |
smooth-stream-text/styles.css | The reveal stylesheet, for injectStyles: false |
Every entry ships ES modules, CommonJS and type declarations. The React entry starts with 'use client'. Importing the Web Component entry on a server does not throw.
Positions, lengths and rates count UTF-16 code units, the way JavaScript string lengths do, not visible characters or tokens.
Core
import { createSmoothStream } from 'smooth-stream-text'
const stream = createSmoothStream({ targetLatencyMs: 150 })createSmoothStream(options?): SmoothStream
Creates one stream. A stream can serve any number of answers: end() closes the current answer, and text that arrives later reopens it.
| Method | What it does |
|---|---|
push(delta) | Appends a delta. |
setSource(text) | Hands over the growing full text. Text that does not start with the current source counts as a new answer and starts over. |
end() | The producer closed the answer: the tail drains and the phase reaches done. |
reset(text?) | Starts over. With text, starts with it fully visible. For finished history, follow with end(), or the phase stays open until text arrives. |
flush() | Shows everything received so far, at once. Later input is paced again. |
pause() | Stops drawing without losing anything. Input may still arrive and waits in the buffer. |
resume() | Draws again from where it stopped. |
setOptions(partial) | Changes settings at runtime. A key passed as undefined returns to its default. clock is fixed at creation. |
getState() | The current SmoothState. |
subscribe(listener) | Calls listener(state) on every change. Returns an unsubscribe function. |
destroy() | Releases the stream for good. Later calls do nothing. |
push and setSource are interchangeable; use whichever your transport gives you.
SmoothState
| Field | Type | Meaning |
|---|---|---|
text | string | The visible prefix. Render this. |
cursor | number | How much of source is visible. |
source | string | Everything received for the current answer. |
pending | number | source.length - cursor. |
phase | StreamPhase | 'idle', 'streaming', 'settling' or 'done'. |
charsPerSecond | number | The controller's current rate. |
settling means the producer has stopped (closed, or silent past idleGraceMs) and the tail is draining. done means the answer is closed and fully visible.
Pacing options
| Option | Default | Meaning |
|---|---|---|
targetLatencyMs | 180 | How far behind the live text the display runs, in ms. The main feel setting. |
adaptMs | 250 | Time constant for smoothing the rate. Larger means lazier acceleration; 0 means none. |
minCharsPerSecond | 12 | Floor on the rate; 0 means none. |
maxCharsPerSecond | 900 | Ceiling on the rate. Keeps a burst from being dumped at once. |
settleMs | 400 | How long the tail takes to drain once the producer stops. The hard deadline is twice this. |
idleGraceMs | 200 | Silence tolerated on an open stream before the tail drains anyway. |
maxLatencyMs | 2000 | Above this projected lag the display jumps instead of catching up. |
maxFrameMs | 100 | Per-tick clamp on elapsed time, so one stalled frame cannot cause a jump. |
boundary | 'markdown' | Where the visible text may end: 'markdown', 'char', 'word' or a function. See text boundaries. |
unit | 'char' | 'char' or 'word': what appears as one piece. The reveal reads the same key. |
holdMs | 1500 | How long the producer must be silent before a held construct or a half-arrived word is released. |
holdChars | 120 | Most characters a boundary may hold back before releasing them anyway. |
clock | rAF | The time source. requestAnimationFrame where it exists, a 16 ms timer otherwise. |
A value that cannot work (NaN, a ceiling of zero or below, a floor above the ceiling) falls back to its default or is clamped, so the stream never freezes. resolvePacingOptions(options) returns the full, checked set; defaultPacing holds the defaults.
Clocks and testing
createManualClock(startMs?) returns a clock you advance by hand, so pacing can be tested without waiting:
import { createManualClock, createSmoothStream } from 'smooth-stream-text'
const clock = createManualClock(0)
const stream = createSmoothStream({ clock })
stream.push('hello world')
stream.end()
for (let i = 0; i < 100; i++) clock.advance(16)
stream.getState().text // 'hello world'A Clock is { now(): number; schedule(callback): cancel }. defaultClock is the built-in one. ManualClock adds advance(ms) and setNow(ms).
createSourceSync(target)
The rule every binding uses to turn a (text, done) pair, as a component receives it on each render, into stream calls. apply(text, done):
- the first pair that is already done is history:
reset(text)andend(); - text that continues the source is handed over and paced;
donecloses the answer, including when a chunk arrives afterdonewas set; - text that replaces the source is shown at once if done, or paced from the start if not.
Use it when you write a binding for another framework. SyncTarget is the shape it drives: getState, setSource, reset and end.
Lower-level helpers
These are exported for custom pipelines and are what the stream itself uses.
| Export | Purpose |
|---|---|
createBoundaryResolver(mode, unit?) | Returns { resolve(source, desired, closed, quiet?), reset() }, the largest allowed cut at or before desired. |
clampToGraphemeBoundary(text, index) | Moves an index back to the nearest grapheme cluster boundary. |
createPacerState(options, cursor?), stepPacer(state, input, options), effectiveTargetLatency(input, options) | The rate controller, one tick at a time. |
Types: BoundaryFn, BoundaryMode, BoundaryResolver, TextUnit, PacerInput, PacerState, SourceSync, SyncTarget, Clock, ManualClock, PacingOptions, ResolvedPacing, SmoothState, SmoothStream, StreamPhase.
Reveal options
The reveal fades newly visible text in. It is used by SmoothText, Reveal, createSmoothText, createReveal and the Web Component.
| Option | Default | Meaning |
|---|---|---|
unit | 'char' | What animates as one unit: 'char' or 'word'. Shared with pacing. |
durationMs | 380 | How long one unit takes to appear. |
staggerMs | 36 | Delay between neighbouring units of one batch. |
blurPx | 4 | Starting blur. |
translatePx | 3 | Starting upward shift. 0 keeps units display: inline, with no layout impact. |
easing | cubic-bezier(.22,.61,.36,1) | Timing function. |
maxWaveLagMs | 240 | Above this queue depth the stagger compresses, so a batch cannot queue a long wave. |
maxAnimatedUnits | 64 | Most units one update animates. Anything before them in a bigger batch appears at once. |
respectReducedMotion | true | Under prefers-reduced-motion no spans are created at all. false animates anyway. |
injectStyles | true | false skips the built-in stylesheet; import smooth-stream-text/styles.css or copy REVEAL_CSS instead. |
className | Extra class on every animating unit. |
The stylesheet is a constructed stylesheet in the cascade layer smooth-stream-text, adopted by the document or the shadow root the element is in, so a Content Security Policy without 'unsafe-inline' does not block it and any rule of yours wins. Visual settings are CSS custom properties on the host: --sst-duration, --sst-blur, --sst-translate, --sst-easing.
DOM
import { createSmoothText, createReveal } from 'smooth-stream-text/dom'createSmoothText(element, options?): SmoothTextHandle
Pacing and reveal in one element, for plain text. The handle owns the element's children; give it an element with no other content. options takes pacing options, reveal options, a shared timeline, a clock and initialText.
| Member | What it does |
|---|---|
push(delta), setText(text) | Hand over a delta, or the full growing text. |
end() | Closes the current answer. |
reset(text?) | Starts over, with text already fully shown. |
flush() | Shows everything received, at once. |
pause(), resume() | Stop and continue drawing, for an element that is hidden or detached. |
setOptions(partial) | Changes pacing or reveal settings at runtime. |
subscribe(listener) | The pacing state on every change. |
onSettled(listener) | Called once per answer when it is closed, fully paced and no unit is still animating. Returns an unsubscribe function. |
settled | Whether the current answer is completely on screen. |
state | The current pacing state. |
destroy() | Releases everything. The text stays on screen. |
createReveal(element, options?): RevealHandle
The reveal alone, for text you pace yourself. update(fullVisibleText) hands over the whole visible text; the diff against what is shown is computed inside, and only appended text animates. Also flush(), reset(text?), setOptions(partial), animating, onIdle(listener) and destroy().
createWaveTimeline(options) creates a timeline that several reveals can share, so the stagger stays continuous across paragraphs. Pass it as timeline.
Also exported: defaultReveal, resolveRevealOptions, splitPieces, REVEAL_CSS, WORD_CLASS ('sst-word'), FLAT_CLASS ('sst-flat'), FORCE_MOTION_CLASS ('sst-motion'), ensureStyles(target), applyRevealVars(element, options), prefersReducedMotion(), and the types Piece, ResolvedReveal, RevealDeps, RevealHandle, RevealOptions, WaveTimeline, SmoothTextHandle, SmoothTextOptions.
React
import { useSmoothText, useSmoothStream, useSmoothStreamController, SmoothText, Reveal, RevealProvider, useReveal } from 'smooth-stream-text/react'| Export | Signature | Returns |
|---|---|---|
useSmoothText | (source, options?: PacingOptions & { done?: boolean }) | { text, phase, flush, reset, setOptions }. Re-renders only when text or phase changes. |
useSmoothStream | same | The full SmoothState plus flush, reset, setOptions. |
useSmoothStreamController | (options?: PacingOptions) | The full state plus push, setSource, end, flush, reset, setOptions. |
SmoothText | props: text, done?, as?, className?, unitClassName?, onSettled?, every pacing and reveal option, any HTML attribute | A host element that paces and reveals its text. |
RevealProvider | props: every reveal option, resetKey? | Shares one wave timeline with the Reveals below it. Renders no DOM. |
Reveal | props: children: string, as?, initialText?, any HTML attribute | Animates appended text in its host element. |
useReveal | () | The surrounding provider's { options, timeline }. |
Types: SmoothStreamControls, SmoothStreamController, SmoothTextState, SmoothTextProps, RevealProps, RevealProviderProps, RevealContextValue. The React entry also re-exports everything from the DOM and root entries.
Vue
import { useSmoothStream, SmoothText, Reveal, RevealProvider } from 'smooth-stream-text/vue'| Export | Signature | Returns |
|---|---|---|
useSmoothStream | (source: Ref<string> | (() => string), options?: PacingOptions & { done?: Ref<boolean> | (() => boolean) | boolean }) | { state, text, phase, flush, reset, pause, resume, setOptions }; text and phase are computed refs. |
SmoothText | props: text, done, as (default 'div'), className, options; emits settled | Paces and reveals plain text. Also the default export. |
RevealProvider | props: options, resetKey | Shares one wave with the Reveals in its slot. |
Reveal | props: text, as (default 'span'), className, options | Animates appended text. |
Pacing starts once the component is mounted, so server rendering schedules nothing; text that is already done is in the server output. Inside <KeepAlive> everything pauses while deactivated. The Vue entry re-exports the DOM and root entries.
Svelte
import { smoothStream, smoothText, reveal } from 'smooth-stream-text/svelte'| Export | Kind | What it does |
|---|---|---|
smoothStream(options?) | readable store of SmoothState | Adds apply(text, done), push, setSource, end, flush, reset, pause, resume, setOptions, destroy. |
smoothText | action | use:smoothText={{ text, done, onSettled, ...options }} paces and reveals into the element. |
reveal | action | use:reveal={{ text, ...revealOptions }} animates appended text in the element. |
Nothing is imported from Svelte, so these work in Svelte 4 and 5. Actions do not run on the server. Types: SvelteAction, SmoothTextActionOptions, RevealActionOptions, SmoothStreamStore.
Web Component
import 'smooth-stream-text/web-component'Importing the module defines <smooth-stream-text>. defineSmoothStreamTextElement(tag?) defines it under another name; SmoothStreamTextElement is the class.
| Attribute | Option |
|---|---|
text | the text (also a property) |
done | closes the answer (also a property) |
unit | unit |
duration | durationMs |
stagger | staggerMs |
blur | blurPx |
translate | translatePx |
target-latency | targetLatencyMs |
min-cps | minCharsPerSecond |
max-cps | maxCharsPerSecond |
boundary | boundary |
Properties: text, done, options (any pacing or reveal option; removing an attribute brings back the option or default underneath it) and the read-only state. Methods: push(delta), end(), flush(), reset(text?).
The element dispatches sst:done (bubbling) once per answer, when it is completely on screen. The stream belongs to the element: moving it or taking it out of the page and back pauses and resumes it, and text pushed before the element is connected is kept.