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
npm install @liiift-studio/speechtypeNext.js App Router: this library uses browser APIs. Add
"use client"to any component file that imports from it.
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>
</>
)
}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)
function handleSpeak() {
if (!ref.current) return
startSpeechType(ref.current, { activeWeight: 700, rate: 0.9 })
}
function handleStop() {
if (ref.current) removeSpeechType(ref.current)
}
return (
<>
<p ref={ref}>The quick brown fox jumps over the lazy dog.</p>
<button onClick={handleSpeak}>Speak</button>
<button onClick={handleStop}>Stop</button>
</>
)
}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.
```ts
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 HTMLimport type { SpeechTypeOptions } from '@liiift-studio/speechtype'
const opts: SpeechTypeOptions = {
activeTracking: 0.08,
activeWeight: 800,
inactiveOpacity: 0.3,
rate: 0.85,
}| 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 |
prepareSpeechType walks the element's child nodes and wraps each word in a <span class="st-word"> — without changing visual layout. applySpeechType then uses activeWordIndex to toggle st-active / st-inactive classes on each span. The active span receives letter-spacing, font-variation-settings, and CSS transitions via the class; the inactive spans get reduced opacity. All style changes are CSS class toggles — no inline style thrashing.
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.
| 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, st-active, st-inactive). |
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'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.