Install
From a CDN, no build step:
<script type="module"
src="https://cdn.jsdelivr.net/npm/@jailson/say-my-name@0"></script>
<say-my-name audio="/audio/my-name.opus" respell="zhah-EEL-sown">
Jailson
</say-my-name>
Or as a package:
npm install @jailson/say-my-name
import '@jailson/say-my-name';
How it looks
Just a play button
Ask
<say-my-name lang="pt-BR">Jailson</say-my-name>
With a respelling, inline
<say-my-name respell="zhah-EEL-sown" lang="pt-BR">
Jailson
</say-my-name>
Dictionary style, with IPA
<say-my-name respell="zhah-EEL-sown" ipa="ʒaˈiwsõ"
lang="pt-BR" display="ruby">
Jailson
</say-my-name>
More than one right answer
A name can have a native pronunciation and the one you answer to locally. Both are correct. Most tools make you pick one; this one doesn't.
<say-my-name display="ruby">
Jailson
<script type="application/json">
[{"label": "Portuguese", "lang": "pt-BR",
"respell": "zhah-EEL-sown", "ipa": "ʒaˈiwsõ"},
{"label": "How I'm called here", "lang": "en-CA",
"respell": "JAY-ill-sun"}]
</script>
</say-my-name>
About the voice
The examples above use live browser synthesis, because a demo page has no audio of your voice. On your own site you'd supply a file. Three things are worth knowing about relying on the browser instead:
-
Browsers can't be told how to pronounce a word. SSML
<phoneme>is unsupported across engines, and on macOS the markup gets read aloud as literal text. Your IPA cannot reach the synthesizer. - The voice is whatever the visitor's device has. Different on Windows, macOS, Android, and iOS — and absent entirely if they have no voice pack for your language.
- Synthesis fails hardest on unusual names — precisely the names that need a pronunciation widget in the first place.
So live synthesis is the weakest of the options, and the component is careful about it: a synthesized button draws a dashed sound wave and says so in its accessible label, and if the browser has no voice for your language it renders no button at all rather than mispronouncing you confidently.
A recording is the most faithful answer — but it isn't the only respectable
one. A clean, isolated sample of your voice on a public page is exactly what
voice-cloning models want, and declining to publish one is reasonable. Generating a
clip from a text-to-speech tool, marking it synthetic, and shipping that
is a perfectly good outcome. What matters is that a listener is never misled about
which one they are hearing.
Attributes
| Attribute | Default | What it does |
|---|---|---|
audio | — | URL of your recording. Loaded only when someone presses play. |
respell | — | Dictionary-style respelling for humans, e.g. zhah-EEL-sown. |
ipa | — | IPA, with or without slashes. Hidden from screen readers, which read the symbols as gibberish. |
lang | — | BCP 47 tag. Picks the speech voice; the button hides if no voice matches. |
tts | fallback |
off · fallback (only when there's no recording) · on (both, as separate buttons). |
tts-text | the name | What the synthesizer is actually given. Set it when the spelled name comes out wrong. |
synthetic | absent | Marks an audio file as a synthesized voice, so its button is labelled like live synthesis. |
voice | best match | Preferred voice name, e.g. Luciana. |
rate | 0.9 |
Speech rate. Names land better slightly slower than default. |
display | inline |
none · inline · ruby (respelling above the name, IPA below) · tooltip. |
show | both |
both · respell · ipa. Which written forms appear at all. |
name | element text | Only needed if the element has no text of its own. |
Styling
Custom properties, or ::part() for full control:
say-my-name {
--smn-color: #6b46c1;
--smn-phonetic-color: #666;
--smn-phonetic-size: 0.8em;
--smn-button-size: 1em;
--smn-focus: 2px solid #6b46c1;
}
say-my-name::part(button) { border-radius: 999px; }
say-my-name::part(ipa) { font-family: 'Doulos SIL', serif; }
Accessibility
- A real
<button>, reachable by keyboard, with a focus ring that inherits your page's. - Labels read as sentences: “Hear how to pronounce Jailson — Portuguese (synthesized voice)”.
- IPA is
aria-hidden; screen readers announce the button label instead of spelling out symbols. - The playing animation respects
prefers-reduced-motion. - Without JavaScript the name is still just text — the component enhances, it doesn't replace.
- Failures are announced politely rather than silently swallowed.
Frameworks
It's a custom element, so it works anywhere HTML does. React 19+ passes props to
custom elements natively. Vue needs
compilerOptions.isCustomElement. Svelte and Astro need nothing.