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

@liiift-studio/speechtype

v1.0.8

Published

Typography that follows your voice — per-word typographic emphasis synced to Web Speech API boundary events

Downloads

944

Readme

speechType

npm License: MIT part of liiift type-tools

Typography that follows your voice — per-word typographic emphasis synced to Web Speech API boundary events. Each spoken word gets wider tracking, heavier weight, and larger optical size; the rest of the text recedes. A read-along effect grounded in typographic logic, not arbitrary highlight colours.

speechtype.com · npm · GitHub

TypeScript · Zero dependencies · React + Vanilla JS


Install

npm install @liiift-studio/speechtype

Usage

Next.js App Router: this library uses browser APIs. Add "use client" to any component file that imports from it.

React component (controlled)

SpeechTypeText is a controlled component — you manage a SpeechSynthesisUtterance yourself, track which word is active in state, and pass the index as a prop. This pattern gives you full control over voice, timing, and UI.

"use client"
import { SpeechTypeText } from '@liiift-studio/speechtype'
import { useState, useCallback } from 'react'

const TEXT = 'The quick brown fox jumps over the lazy dog.'

export default function Demo() {
  const [activeWordIndex, setActiveWordIndex] = useState(-1)

  const handleSpeak = useCallback(() => {
    const utterance = new SpeechSynthesisUtterance(TEXT)
    utterance.onboundary = (e) => {
      if (e.name === 'word') {
        const wordIndex = TEXT.slice(0, e.charIndex).trim().split(/\s+/).filter(Boolean).length
        setActiveWordIndex(wordIndex)
      }
    }
    utterance.onend = () => setActiveWordIndex(-1)
    speechSynthesis.speak(utterance)
  }, [])

  return (
    <>
      <SpeechTypeText activeWordIndex={activeWordIndex} activeWeight={700} inactiveOpacity={0.45}>
        {TEXT}
      </SpeechTypeText>
      <button onClick={handleSpeak}>Speak</button>
    </>
  )
}

React — imperative (startSpeechType)

For a simpler setup, skip SpeechTypeText and let startSpeechType manage everything directly on a plain element ref:

"use client"
import { useRef } from 'react'
import { startSpeechType, removeSpeechType } from '@liiift-studio/speechtype'

export default function Demo() {
  const ref = useRef<HTMLParagraphElement>(null)
  // stop() cancels speech and resets emphasis but keeps spans in place.
  // removeSpeechType() does a full teardown — cancels speech AND restores original HTML.
  // Call stop() for pause/stop controls; call removeSpeechType() only on unmount or full reset.
  const stopRef = useRef<(() => void) | null>(null)

  function handleSpeak() {
    if (!ref.current) return
    stopRef.current?.()  // cancel any in-progress speech first
    stopRef.current = startSpeechType(ref.current, { activeWeight: 700, rate: 0.9 })
  }

  function handleStop() {
    stopRef.current?.()
    stopRef.current = null
  }

  return (
    <>
      <p ref={ref}>The quick brown fox jumps over the lazy dog.</p>
      <button onClick={handleSpeak}>Speak</button>
      <button onClick={handleStop}>Stop</button>
    </>
  )
}

React hook

useSpeechType is the low-level hook behind SpeechTypeText. Use it when you need the controlled pattern but want to render your own element:

"use client"
import { useSpeechType } from '@liiift-studio/speechtype'
import { useRef, useState, useCallback } from 'react'

export default function Demo() {
  const ref = useRef<HTMLParagraphElement>(null)
  const [activeWordIndex, setActiveWordIndex] = useState(-1)

  useSpeechType(ref, activeWordIndex, { activeWeight: 700 })

  return <p ref={ref}>The quick brown fox jumps over the lazy dog.</p>
}

Vanilla JS

startSpeechType is the all-in-one entry point for vanilla use. It wraps the words in spans, starts the Web Speech API, updates the emphasis on each boundary event, and returns a stop function.

import { startSpeechType, removeSpeechType } from '@liiift-studio/speechtype'

const el = document.querySelector('p')
const stop = startSpeechType(el, {
  activeWeight: 700,
  activeTracking: 0.06,
  rate: 0.9,
})

// Later — stop speech and restore original HTML:
stop()
removeSpeechType(el)

For more control, use the lower-level functions:

import { prepareSpeechType, applySpeechType, removeSpeechType } from '@liiift-studio/speechtype'

const el = document.querySelector('p')
prepareSpeechType(el)               // wraps each word in a span

applySpeechType(el, 3)              // emphasise word at index 3
applySpeechType(el, -1)             // clear emphasis

removeSpeechType(el)                // restore original HTML

TypeScript

import type { SpeechTypeOptions } from '@liiift-studio/speechtype'

const opts: SpeechTypeOptions = {
  activeTracking: 0.08,
  activeWeight: 800,
  inactiveOpacity: 0.3,
  rate: 0.85,
}

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | activeTracking | number | 0.06 | Letter-spacing on the active (currently spoken) word, in em | | activeWeight | number | 700 | wght axis value on the active word | | activeOpsz | number | 24 | opsz axis value on the active word | | inactiveOpacity | number | 0.45 | Opacity of inactive (not currently spoken) words | | transitionMs | number | 80 | CSS transition duration in ms for style changes | | rate | number | 0.9 | Speech rate (0.1–10). Passed to SpeechSynthesisUtterance | | pitch | number | 1 | Speech pitch (0–2). Passed to SpeechSynthesisUtterance | | volume | number | 1 | Speech volume (0–1). Passed to SpeechSynthesisUtterance |


How it works

prepareSpeechType reads the element's text content and wraps each word in a <span class="st-word"> — without changing visual layout. Note: inline child elements (<em>, <strong>, <a>, etc.) are flattened to plain text during wrapping. applySpeechType then writes font-variation-settings, letter-spacing, and opacity as inline styles directly on each span (no CSS class toggles). The active span gets wider tracking, heavier weight, and larger optical size; inactive spans get reduced opacity. CSS transitions on those properties are set once by prepareSpeechType.

startSpeechType wires a SpeechSynthesisUtterance to the browser's Web Speech API, listens for boundary events, maps the character offset to a word index, and calls applySpeechType on each event. It returns a stop function that cancels synthesis and removes all emphasis.

Browser support: Web Speech API is supported in Chrome, Edge, and Safari. Firefox requires a flag. startSpeechType falls back silently in environments without speechSynthesis.


API reference

| Export | Description | |--------|-------------| | prepareSpeechType(el, options?) | Wraps each word in a span. Call once before applySpeechType. | | applySpeechType(el, activeIndex, options?) | Emphasises word at activeIndex. Pass -1 to clear. | | startSpeechType(el, options?) | All-in-one: prepares spans, starts Web Speech API, returns stop(). | | removeSpeechType(el) | Cancels synthesis and restores original HTML. | | getCleanHTML(el) | Returns element HTML with all injected spans removed. | | useSpeechType | React hook: (ref, activeWordIndex, options?) | | SpeechTypeText | React component. Controlled via activeWordIndex prop. Forwards ref. | | SpeechTypeOptions | TypeScript interface for all options. | | SPEECH_CLASSES | CSS class names injected by the algorithm (st-word). |


Next.js

SpeechTypeText, useSpeechType, and startSpeechType all require a browser environment. Add "use client" to any component that imports them:

"use client"
import { SpeechTypeText } from '@liiift-studio/speechtype'

Dev notes

next in root devDependencies

package.json at the repo root lists next as a devDependency. This is a Vercel detection workaround — not a real dependency of the npm package. Vercel's build system inspects the root package.json to detect the framework; without next present it falls back to a static build and skips the Next.js pipeline, breaking the /site subdirectory deploy.

The package itself has zero runtime dependencies. Do not remove this entry.