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

@tapestry-ui/dictation

v0.2.0

Published

Speak instead of typing. A headless Preact hook over the browser's SpeechRecognition, with the iPhone quirks handled and a provider shape you can swap for a server transcriber.

Readme

@tapestry-ui/dictation

Speak instead of typing. A utility, not a component — it renders nothing.

Part of Tapestry UI.

npm i @tapestry-ui/dictation
const dictation = useBrowserDictation({
  lang: locale.value,                    // dictate in whatever tongue the app speaks
  onResult: (text) => setDraft(text),    // one utterance, committed
  onFault: setTrouble,
});

{dictation.supported && (
  <button aria-pressed={dictation.listening}
          onClick={() => dictation.listening ? dictation.cancel() : dictation.start()}>
    {dictation.listening ? 'Stop' : 'Speak'}
  </button>
)}
{dictation.listening && <p>{dictation.partial}</p>}

Know where the audio goes

On-device first, by default. Chrome 139+ exposes SpeechRecognition.available({ processLocally: true }) and install(), and this hook uses them: if a language pack is there it recognises locally, if one is downloadable it fetches it (preparing is true while it does), and only otherwise does it fall back to the cloud. Pass preferOnDevice: false to skip that.

That default earns itself twice. The audio never leaves the machine — and it keeps working when the cloud recogniser is unreachable, which is what a bare network fault usually means.

Where it does fall back: Safari routes to Apple, Chrome to Google, and nothing in the API tells you which happened.

If that is the wrong posture for your data, do not use this hook. Implement the Dictation interface against a transcriber you control — record with MediaRecorder, send the audio to your own server, return the text. The interface is the point of the package; the browser-backed hook is just the implementation that costs nothing.

Three things this exists to handle

It is never continuous, and that is not a preference. With continuous = true, Safari on iOS holds the microphone open and never delivers a final result. So this is single-shot: one utterance, then it stops itself.

It prefers the unprefixed name. Chrome now has SpeechRecognition natively as well as webkitSpeechRecognition. If you are stubbing it in a test, stub the one the hook will actually find — recogniserClass() is exported so you can ask.

Faults are distinguished, because only one is actionable.

| fault | means | |---|---| | unsupported | no recogniser in this browser — hide the button, don't disable it | | denied | permission refused. The only one a person can fix. | | no-microphone | permission given, no device | | silence | it listened, nothing was said | | network | the recogniser was unreachable — a blocker, a VPN, a Chromium fork with no API key, or a regional outage. Not the speaker's fault and rarely fixable by them. | | failed | anything else the browser reported |

"Something went wrong" would hide the one message worth showing.

The shape

interface Dictation {
  readonly supported: boolean;
  readonly listening: boolean;
  readonly preparing: boolean;   // fetching an on-device language pack
  readonly partial: string;      // heard so far, before it commits
  readonly fault: DictationFault | null;
  start(): void;
  cancel(): void;                // abandon this utterance, deliver nothing
}

A fault clears on the next start(). Unmounting mid-utterance releases the microphone. faultFor, transcriptOf and recogniserClass are exported and pure — that is where the logic lives and where the tests point.

Don't send on someone's behalf

The hook delivers text. It does not submit anything. A recogniser deciding someone has finished talking is not the same as them deciding, and a voice feature that fires off a half-sentence loses trust on its second use. Put the transcript in the box and let them press the button.

@tapestry-ui/ask does exactly that: pass it a dictation provider and a prefill, and a dictated sentence lands in the composer, editable, unsent.

License

MIT © Brandon Minton