Iris Cocreative · Lab ← All experiments

Iris Cocreative · Component

Listen Hear

A read-along voice for any page. One line of code turns your writing into warm, natural speech — and highlights each sentence and word as it reads, so people can follow along with their eyes and ears at once.

Listen Hear is the text-to-speech widget we built for Iris Cocreative work — it powers the read-along experience on our research notes and client proposals. It sits quietly at the top of an article as a small “Listen to this page” button. Press it and a calm player floats up from the bottom of the screen, reads the page in a lifelike voice, and gently scrolls and highlights along so the listener never loses their place.

It’s built on Speechify’s API, wrapped in a single small script with no dependencies. Audio can be pre-generated once and served as static files, which means visitors don’t need an account, an API key, or any setup — they just press play.

Hear it now — the Beatrice voice

Good writing deserves to be heard. When words are read aloud and highlighted in step, a page becomes something you can follow with your eyes and your ears at the same time — calmer, clearer, and open to far more people.

This is the real widget voice — Beatrice, generated with Speechify — playing a pre-rendered clip with word-synced highlighting. Press “Listen to this page” above to hear the whole page read the same way.

Why it matters: accessibility

Audio isn’t a nice-to-have bolted onto the side of a page — for a lot of people it’s the difference between reading something and giving up on it. A read-along voice widens who can actually use what you publish.

It also moves you toward the WCAG spirit of giving content in more than one perceivable form. A visitor who can’t comfortably read your page can still fully receive it — and that’s the whole point of accessible design: the same content, open to more people, with dignity rather than a stripped-down “accessible version” off to the side.

A quiet inclusion. Nobody has to declare a need or find a special mode. The button sits there for everyone; whoever wants it, uses it. That’s inclusion built into the ordinary path, not tacked on.

Beyond accessibility

The same widget earns its place for reasons that have nothing to do with disability. Voice simply meets people where a lot of life already happens.

Multitasking & commutes

Let people “read” your proposal, article, or docs while they walk, drive, cook, or work out — turning dead time into attention.

Longer, deeper engagement

Audio keeps people on the page. A ten-minute read they’d skim becomes a ten-minute listen they finish — and dwell time is real signal.

Proposals that land

A client can listen to your pitch on the way to a meeting instead of squinting at a PDF. It feels considered, modern, and premium.

Screen relief

Give readers a break from more screen time. Some of your best writing is better received with eyes closed.

Retention

Hearing and seeing a point together helps it stick — useful for learning material, onboarding, and reference docs.

A crafted touch

A warm, well-tuned voice signals care. It quietly says this site was made by people who sweat the details.

How it works

The widget scans the page’s main <article>, breaking it into blocks — each heading, paragraph, and list item becomes its own short audio segment, played in sequence with natural pauses between them. As each segment plays, word-level timing data from Speechify drives the highlight, lighting up the active sentence softly and the current word more strongly, and scrolling to keep the reader’s place (while politely getting out of the way if the visitor scrolls themselves).

There are two ways it can source audio:

If a cached block’s text no longer matches the page — say a paragraph was edited — the widget notices and regenerates that block live rather than playing stale audio, so the voice always matches the words on screen.

Developer reference · not narrated

Add it to a site

The widget needs just two things on the page: your readable content wrapped in an <article> element, and the script. That’s it — the “Listen to this page” button appears automatically after your first heading.

Any HTML site

<!-- Your content -->
<article>
  <h1>Your headline</h1>
  <p>Your writing…</p>
</article>

<!-- The widget, once before </body> -->
<script src="https://lab.iriscocreative.com/listen-hear/read-aloud.js" defer></script>

Webflow

In Webflow, wrap your rich-text or content section so it renders inside an <article> tag (set an HTML Embed with <article> … or use a component whose tag you can control), then add the script in Page Settings → Custom Code → Before </body> tag:

<script src="https://lab.iriscocreative.com/listen-hear/read-aloud.js" defer></script>

If your content isn’t inside an <article>, add a tiny embed just before the script that promotes your main wrapper — or point the widget at a different container by configuring it (see below).

Single-page apps (React, Vue, etc.)

Load the script after your content has rendered, and re-initialise on route changes. The simplest path is to import the file and call it once the article is in the DOM; because the widget keys everything off the live DOM and a page slug, each route can have its own cached audio folder.

One requirement. The widget reads whatever lives inside the first <article> on the page. Headings, paragraphs, and list items are spoken; navigation, captions marked class="lede", and anything inside data-lh-skip are skipped. Wrap only what you want read.

Customise it

Configure the widget by setting a window.ListenHear object before the script loads, or with data-* attributes on the <article>. Everything is optional.

<script>
  window.ListenHear = {
    slug:  'my-page',        // which tts-cache/<slug>/ folder to use
    voice: 'beatrice_32',    // preferred Speechify voice
    label: 'Listen to this article',
    speeds: [1, 1.25, 1.5, 2],  // speed-toggle cycle
    mount: '#listen-btn'       // put the button in a specific element
  };
</script>
<script src="…/listen-hear/read-aloud.js" defer></script>
OptionDefaultWhat it does
slugpage filenameWhich tts-cache/<slug>/ folder holds this page’s pre-generated audio. Also settable as data-lh-slug.
voicebeatrice_32Preferred Speechify voice, resolved against the account’s voice list. Also data-lh-voice.
modelsimba-3.2Speechify model used for live generation.
labelListen to this pageText on the launcher button (a duration estimate is appended).
speeds1, 1.25, 1.5, 2, 0.75Playback speeds the speed button cycles through.
stopHeadingsrelated, sourcesHeadings after which reading stops (e.g. link lists you don’t want spoken).
mountafter the H1CSS selector for where to place the launcher button.
cacheBasetts-cacheBase path to the cache folder, if you host audio elsewhere.

Branding & colours

The player reads CSS custom properties, so you can theme it entirely from your own stylesheet — no need to edit the script. Set any of these on :root (or a parent):

:root {
  --lh-accent: #279DAF;   /* play button, highlight, progress */
  --lh-panel:  #FDFBF5;   /* bar & button background */
  --lh-ink:    #1E2430;   /* text colour */
  --lh-line:   #E2DCCB;   /* borders */
  --lh-track:  #EDE7D8;   /* progress track */
  --lh-font:   Montserrat, system-ui, sans-serif;
}

Skipping content

Add data-lh-skip to any element (or a wrapper) to keep it out of the narration — pull-quotes, code samples, figure captions, or a whole reference section like this one. Elements with class="lede" and anything inside a <nav> are skipped automatically.

Generating the audio cache

For a public page, pre-generate the audio once so visitors need no key. The included Node script (tts-generate.mjs) calls Speechify for every block and writes a tts-cache/<slug>/ folder next to your HTML.

# from the listen-hear folder, with a Speechify key available
# (SPEECHIFY_API_KEY env var, or speechify-key.txt — never committed)

npm install jsdom

# dry run — list the blocks that would be read, no API calls
node tts-generate.mjs --html my-page.html --slug my-page --plan

# generate everything (skips blocks already cached & unchanged)
node tts-generate.mjs --html my-page.html --slug my-page --voice beatrice_32

Commit the resulting tts-cache/<slug>/ folder alongside the page. Re-run after editing copy — unchanged blocks are reused, so you only pay for what changed. Because the file is deployed on GitHub Pages, the audio is served straight from lab.iriscocreative.com with the rest of the site.

Cost & privacy. Speechify bills per character at generation time only — a cached page costs nothing to play, however many people listen. The API key lives only at build time (or, for live pages, in the individual visitor’s own browser); it’s never shipped in the page or committed to the repo.