say-my-name
A web component that shows people how to pronounce your name.
An embeddable web component for name pronunciation — your own recording, a phonetic spelling, and speech synthesis as the fallback. Zero dependencies, nothing hosted, nothing leaves the page.
The problem
Every existing way to put a name pronunciation on a web page makes you depend on someone else’s server for a 40 KB recording of your own voice. NameCoach is account-gated enterprise SaaS. NameDrop is a hosted link. The WordPress and Drupal plugins are locked to those CMSs. Vocalizer.js, the closest open-source equivalent, has been unmaintained since 2016 and fetches a stranger’s recording of your name from a third-party API.
The gap is narrow and specific: a static site has nowhere to put a pronunciation widget that doesn’t involve signing up for something. This is one HTML tag that works on a static site, hosts nothing, and asks for nothing.
Approach and methodology
Five constraints were fixed before the first line of the element was written, and they are what the design is actually made of:
- Zero runtime dependencies. Dev dependencies are fine; shipped ones are not.
- Under 5 KB gzipped for the bundle a visitor downloads.
- No network calls, ever — not for audio hosting, not for analytics, not for a pronunciation API.
- Progressive enhancement. With JavaScript off, the visitor still sees the name as ordinary text.
- Never imply a synthesized voice is a recording.
The fifth is the one that shapes the most code. A synthesized button draws a dashed sound
wave instead of a solid one and appends “(synthesized voice)” to its accessible label; a
recording marked synthetic gets the same treatment, because a generated file is a
synthesizer no matter when it was generated. When the browser has no voice for the language
at all, the button is not rendered — better no button than one that says a name confidently
in the wrong accent.
The second constraint is not a slogan either. The component gets dropped onto other people’s pages, so the size check is a build step that fails rather than a number in a README.
Architecture
The element is a custom element with a shadow root, styled from the outside through custom
properties (--smn-color, --smn-focus, and a handful of sizes) and ::part() hooks —
name, respell, ipa, controls, button. Everything else is encapsulated.
Three pieces are worth naming:
A name can have more than one right answer. A Brazilian name in an English-speaking
country has at least two legitimate pronunciations: the native one, and the one you actually
answer to locally. Both are correct, and most tools make you pick one. Multiple pronunciations
are declared in a <script type="application/json"> block inside the element — a tag browsers
never render, so if the component fails to load the visitor still just sees the name. Each one
gets its own labelled button, and the written form follows whichever is playing.
Two written forms, not one. A dictionary-style respelling (zhah-EEL-sown) is the reading
aid; IPA (ʒaˈiwsõ) is the notation. The ruby display mode frames the name with the
respelling above and the IPA below, the way a dictionary entry does. The IPA is aria-hidden,
because read aloud the symbols come out as disconnected punctuation.
The licence boundary is a runtime boundary. The component is MIT and contains no GPL code.
The authoring studio is GPLv3, because it bundles eSpeak NG
to guess IPA from a spelling — and eSpeak NG is GPLv3. The studio is a separate program that
happens to load the component, which is why the component’s licence is unaffected. The engine
itself is 18 MB and is not committed to the repository at all: the build stages it out of
node_modules, and the studio fetches it only when a visitor asks for a suggestion. The same
build generates the studio’s language list from the engine, so the 133 languages it offers
can never drift from what is actually installed.
The studio does three things for audio — record in the browser, load a file made elsewhere, or
synthesize one locally with eSpeak and tick synthetic for you. Nothing is uploaded in any of
the three. Choosing the voice at authoring time rather than playback time is the point:
unlike browser speech synthesis, the voice baked into the file is the voice every visitor
hears.
Results
What’s real: published on npm as @jailson/say-my-name, MIT, with a demo and the studio on
GitHub Pages. The bundle a visitor downloads is 4,466 bytes gzipped — 87% of its own 5 KB
budget — with no runtime dependencies at all.
It is also in use on this site. The heading at the top of the homepage is the component, which is the honest test of whether it survives contact with a real design system: a shadow root means a host page’s focus ring, contrast rules and heading semantics all stop at the boundary and have to be handed back in deliberately.
That integration is also where the sharper entries in Limitations below came from. Every one of them was found by putting the component on a real page and listening to the result — not by reading its own documentation, which is a fair description of how much a component like this can tell you about itself.
Limitations
- Browsers cannot be told how to pronounce a word. SSML
<phoneme>is unsupported across engines, and on macOS the markup is read aloud as literal text rather than stripped. The IPA cannot reach the synthesizer, which is why a recording is the answer and synthesis is only the net underneath it. - The synthesizer needs its own spelling, and finding it is trial and error. Because the
IPA cannot reach the engine, the only lever left is the text you hand it — which is what the
tts-textattribute is for. What that means in practice is sharper than the attribute makes it sound: getting a usable “Jailson” out of a Brazilian Portuguese engine means feeding itJaílson, with an acute accent the name does not have. Unaccented,aireads as a diphthong and the stress lands on JAIL-son. The accent moves it, and the language’s own rules do the rest — Brazilian Portuguese vocalizes the codalto [w] without being asked. None of that is discoverable from documentation; it came out of generating variants and comparing them. An author can end up maintaining three spellings of one name: the one the page displays, the respelling a human reads, and the one the machine is fed. - The studio’s suggested IPA is a draft, not a transcription. eSpeak is a
grapheme-to-phoneme engine, and its output carries its own accent. On this name it produced
ŋ— a released velar nasal, as in English song — where Brazilian Portuguese has a nasalized vowel and no velar contact at all, plus a secondary stress the word does not carry and a flattenedrwhere the language has a tap. The studio is explicit that every field is a starting point the author edits. This is what that warning costs if it is ignored: a transcription that tells an IPA reader to say the name with the exact foreign ending the component exists to prevent. - Synthesis fails hardest on unusual names — exactly the names that need a pronunciation widget — and the voice is whatever the visitor’s device happens to have, different on Windows, macOS, Android and iOS.
- The respelling generator is approximate by design — and has one bug that is not by
design. It maps IPA to letters an English reader can sound out, trading precision for
legibility, and its output is explicitly a starting point the author edits rather than a
transcription. The gap between the two is easy to see on this name: the function turns
ʒaˈiwsõintozhah-EEW-sohng, and the published respelling iszhah-EEL-sown— a human keeping theLthe spelling shows and refusing the English-ngending. The bug is that it takes one word at a time: given a full name it drops the space, soʒaˈiwsõ ˈbɾitucomes back aszhah-eew-sohngb-REE-too, two words fused at the seam. Run it per word and rejoin. - IPA is notation, not a reading aid. A visitor who cannot read it gets nothing from that written form; the respelling is the half that serves them, and it is optional.
- The studio costs 18 MB on first use. That is eSpeak NG, fetched only on request and remembered per browser afterwards, but it is a real download and the studio says so while it runs.
Work in progress
Published at 0.1.0. The component’s API — attributes, parts, custom properties, and the
say-my-name:play event — is what a second release would have to keep stable, and it has not
yet been through anyone else’s page to find out where it is wrong.