npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

use-smooth-text

v0.1.1

Published

Smoothly reveal streamed (LLM) text in React — backend-agnostic, zero-dependency.

Readme

use-smooth-text

Stream LLM responses into React without the jitter.

The same streamed text rendered two ways: raw on the left (jumping in bursts) and through use-smooth-text on the right (revealed smoothly).

When a model streams a reply, the text doesn't trickle in one character at a time. It arrives in clumps: a few words, then a pause, then a whole sentence at once. Render that straight to the screen and it stutters. The chat apps you actually like to use hide this — the words glide in at a steady, readable pace. That smoothness is faked on top of bursty data, and this hook does the faking for you.

The popular way to get it, the Vercel AI SDK's smoothStream, runs on the server, and only if that server is JavaScript. If your backend is Python, Go, Rust, or just something you wrote yourself, you're out of luck. use-smooth-text runs in the browser on plain strings, so it doesn't care where the text came from or how it got there. No dependencies, one hook.

Install

npm install use-smooth-text

You'll need React 18 or newer — it's a peer dependency.

Quick start

Give the hook the text you've collected so far, and render what it hands back:

import { useEffect, useState } from 'react'
import { useSmoothText } from 'use-smooth-text'

function Reply({ stream }: { stream: AsyncIterable<string> }) {
  const [full, setFull] = useState('')
  const [done, setDone] = useState(false)

  // Collect chunks however you got them — fetch, SSE, a WebSocket, whatever.
  useEffect(() => {
    let cancelled = false
    ;(async () => {
      for await (const chunk of stream) {
        if (cancelled) return
        setFull((prev) => prev + chunk)
      }
      setDone(true)
    })()
    return () => { cancelled = true }
  }, [stream])

  const { text, isComplete } = useSmoothText(full, { done })
  return <p aria-live={isComplete ? 'off' : 'polite'}>{text}</p>
}

That's the whole idea: you keep appending to a string, the hook gives you back a smoothly-growing slice of it.

One thing to watch: if you reuse the same component across conversations, clear full and done when the stream changes. The simplest fix is to give the component a key={messageId} so React remounts it for each new reply.

What it returns

useSmoothText(fullText, options?) takes the text you've accumulated so far and returns a smoothly-revealed slice of it, plus a little state to drive your UI.

Options

| Option | Type | Default | What it does | |---|---|---|---| | granularity | 'char' \| 'word' | 'char' | Reveal a grapheme at a time (smoothest) or a word at a time (reads more naturally and splits markdown less often). | | charsPerSecond | number | 60 | How fast it reveals. It'll speed up on its own to catch up after a big burst. | | done | boolean | false | Flip this when the stream ends so the hook drains the rest and reports completion. |

Returns

| Field | Type | What it's for | |---|---|---| | text | string | The slice of fullText revealed so far — render this. | | isAnimating | boolean | true while there's still text left to reveal. | | isComplete | boolean | true once done is set and everything has been shown. | | flush | () => void | Skip the rest of the animation and show everything now. |

A few things worth knowing

It works with any backend because it only ever sees plain strings — it never talks to your model, your server, or your transport. Whatever puts text in the browser is your business.

It won't mangle your text. Reveal happens on real character boundaries via Intl.Segmenter, so it never splits an emoji, a ZWJ sequence, or a surrogate pair, and it handles CJK and right-to-left scripts like Arabic and Hebrew correctly. "One character at a time" is a lie in most of the world's writing systems, and this hook knows it.

It paces itself. When it falls behind a burst it speeds up, then eases off as it catches up, so it lands roughly when the stream does instead of lagging forever.

It respects prefers-reduced-motion. If the user has asked for less motion, the text just appears, no animation.

It's safe to server-render. On the server it renders nothing and never touches window, so there's no hydration mismatch in Next.js, Remix, or anything else doing SSR.

What it doesn't do (yet)

I'd rather be upfront about the edges than have you discover them:

  • It smooths the visual reveal. It doesn't change how many packets your server sends or how fast they arrive — that's the price of doing this in the browser, and it's also what makes it work everywhere.
  • Feed it human-readable text, not tool calls or JSON. You don't want to type out a function-call payload one character at a time.
  • The input is append-only. It expects the string to keep growing; a non-prefix change is treated as a brand-new message and starts over (no support for editing text that's already streamed in).
  • v0 is controlled-input only. You hold the string and pass it in. Imperative push() and stream-consuming adapters are on the list.
  • It doesn't parse markdown. For streaming markdown, pair it with a renderer that completes partial syntax (like streamdown); granularity: 'word' also cuts down on mid-token flicker.

How it compares

| | use-smooth-text | smoothStream (AI SDK) | @llm-ui/react | |---|:-:|:-:|:-:| | Runs in | browser | JS server only | browser | | Works with any backend | yes | no (needs a JS server) | yes | | Zero dependencies | yes | n/a | no | | Actively maintained | yes | yes | no (quiet since 2024) |

If you're already all-in on the Vercel AI SDK with a Node backend, smoothStream is right there and you don't need this. This is for everyone else.

License

MIT