You don’t run into Morse code often in modern apps. Until you do.
Maybe you’re building a puzzle game. Maybe a side project with audio signals. Or you just want a small encoding system that doesn’t depend on language libraries.
Morse code fits. It’s simple. It’s predictable. And it maps cleanly to JavaScript.
When This Works:
- Normal text input (letters and numbers)
- Clean Morse input (single spaces, correct / usage)
- Encoding and decoding basic strings
- Clicking a button to trigger audio playback
Let’s build it.
Step 1: Define the Morse map
Start with a plain object. Each character maps to dots and dashes.
const MORSE_CODE = {
A: ".-",
B: "-...",
C: "-.-.",
D: "-..",
E: ".",
F: "..-.",
G: "--.",
H: "....",
I: "..",
J: ".---",
K: "-.-",
L: ".-..",
M: "--",
N: "-.",
O: "---",
P: ".--.",
Q: "--.-",
R: ".-.",
S: "...",
T: "-",
U: "..-",
V: "...-",
W: ".--",
X: "-..-",
Y: "-.--",
Z: "--..",
"1": ".----",
"2": "..---",
"3": "...--",
"4": "....-",
"5": ".....",
"6": "-....",
"7": "--...",
"8": "---..",
"9": "----.",
"0": "-----",
" ": "/"
};
Keep it uppercase. That avoids extra checks later.
Step 2: Encode text → Morse
Now convert a string into Morse code.
function encodeToMorse(text) {
return text
.toUpperCase()
.split("")
.map((char) => MORSE_CODE[char] || "")
.join(" ");
}
Example: encodeToMorse("HELLO WORLD");
Output: .... . .-.. .-.. --- / .-- --- .-. .-.. -..
Simple. No dependencies. Runs anywhere.
Step 3: Decode Morse → text
You need a reversed map.
const REVERSED_MORSE = Object.fromEntries(
Object.entries(MORSE_CODE).map(([key, value]) => [value, key])
);
Now decode:
function decodeFromMorse(morse) {
return morse
.split(" ")
.map((code) => {
if (code === "/") return " ";
return REVERSED_MORSE[code] || "";
})
.join("");
}
Example:
decodeFromMorse(".... . .-.. .-.. --- / .-- --- .-. .-.. -..");
Output:
HELLO WORLD
Done.
Step 4: Handle edge cases
Real input is messy. Expect it.
A few quick fixes:
- Unknown characters → skip or replace with ?
- Multiple spaces → normalize input
- Lowercase input → already handled with toUpperCase()
Here’s a safer version:
function encodeSafe(text) {
return text
.toUpperCase()
.split("")
.map((char) => MORSE_CODE[char] || "?")
.join(" ");
}
Not perfect. Good enough for most cases.
Step 5: Add audio output (optional)
Now it gets more interesting.
You can play Morse code using the Web Audio API.
Basic idea:
- Dot = short beep
- Dash = longer beep
- Space = silence
Quick example:
function playMorse(morse) {
const context = new AudioContext();
const unit = 0.1; // seconds (dot duration)
let time = context.currentTime;
// Split into words first (by "/"), then letters (by " ")
const words = morse.split("/");
words.forEach((word, wordIndex) => {
const letters = word.trim().split(" ");
letters.forEach((letter, letterIndex) => {
// Play each symbol in the letter
letter.split("").forEach((symbol) => {
if (symbol === ".") {
beep(context, time, unit);
time += unit * 2; // dot + 1 unit silence
} else if (symbol === "-") {
beep(context, time, unit * 3);
time += unit * 4; // dash + 1 unit silence
}
});
// Space between letters: 3 units total
// (but we already have 1 from last symbol, so add 2)
if (letterIndex < letters.length - 1) {
time += unit * 2;
}
});
// Space between words: 7 units total
// (add 4 more if not last word)
if (wordIndex < words.length - 1) {
time += unit * 4;
}
});
}
function beep(ctx, start, duration) {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
gain.gain.setValueAtTime(0.1, start); // Lower volume to protect ears
osc.frequency.value = 600;
osc.start(start);
osc.stop(start + duration);
}
Rough, but it works.
You can tweak timing, frequency, spacing. Depends on your use case.
Step 6: Support punctuation and custom characters
Right now, your map handles letters and numbers. That’s limiting.
Add common punctuation:
const EXTRA_MORSE = {
".": ".-.-.-",
",": "--..--",
"?": "..--..",
"!": "-.-.--",
":": "---...",
"'": ".----.",
"-": "-....-",
"/": "-..-.",
"@": ".--.-."
};
Object.assign(MORSE_CODE, EXTRA_MORSE);
Now your encoder handles real sentences, not just basic input.
Small change. Big difference.
Step 7: Normalize input properly
Users won’t give you clean input. Expect messy strings.
Handle:
- Extra spaces
- Line breaks
- Mixed casing
function normalizeInput(text) {
return text
.replace(/\s+/g, " ")
.trim()
.toUpperCase();
}
Then use it:
function encodeNormalized(text) {
return normalizeInput(text)
.split("")
.map((char) => MORSE_CODE[char] || "?")
.join(" ");
}
This avoids weird output issues early.
Step 8: Add timing control for audio playback
The basic audio function works, but it’s rigid.
Make timing configurable:
function playMorseAdvanced(morse, speed = 0.1) {
const context = new AudioContext();
let time = context.currentTime;
// Standard Morse timing (dot = 1 unit)
// - Space between dots/dashes in same letter: 1 unit (handled by beep timing)
// - Space between letters: 3 units
// - Space between words: 7 units
const words = morse.split("/");
words.forEach((word, wordIndex) => {
const letters = word.trim().split(" ");
letters.forEach((letter, letterIndex) => {
const symbols = letter.split("");
symbols.forEach((symbol) => {
if (symbol === ".") {
beep(context, time, speed);
time += speed * 2; // dot + 1 unit space
} else if (symbol === "-") {
beep(context, time, speed * 3);
time += speed * 4; // dash + 1 unit space
}
});
// Space between letters (add 2 more units to reach total of 3)
if (letterIndex < letters.length - 1) {
time += speed * 2;
}
});
// Space between words (add 4 more units to reach total of 7)
if (wordIndex < words.length - 1) {
time += speed * 4;
}
});
}
Now you can speed it up or slow it down depending on your use case.
Note: Both playMorse and playMorseAdvanced require the beep function from Step 5.
Step 9: Build a quick browser UI
You don’t need a framework for this.
Basic HTML:
<textarea id="input"></textarea>
<button onclick="runEncode()">Encode</button>
<button onclick="runDecode()">Decode</button>
<pre id="output"></pre>
JavaScript:
function runEncode() {
const input = document.getElementById("input").value;
document.getElementById("output").textContent =
encodeToMorse(input);
}
function runDecode() {
const input = document.getElementById("input").value;
document.getElementById("output").textContent =
decodeFromMorse(input);
}
That’s enough for testing locally.
No build tools. No setup.
Step 10: Add real-time encoding (optional)
If you want a better UX, update output on typing:
document
.getElementById("input")
.addEventListener("input", (e) => {
document.getElementById("output").textContent =
encodeToMorse(e.target.value);
});
Feels instant. Much better experience.
If you want to validate against a trusted external source, you can paste your encoded output into a free Morse code translator.
Step 11: Common mistakes to avoid
A few things that break quickly:
- Mixing / and spaces incorrectly
- Not handling unknown characters
- Forgetting to normalize input
- Hardcoding timing values
Test edge cases early.
Try:
- Empty input
- Symbols
- Multiple spaces
- Long strings
You’ll catch issues fast.
Step 12: Wrap it in a utility module
Don’t leave it scattered.
Create a small module:
export const Morse = {
encode: encodeToMorse,
decode: decodeFromMorse,
play: playMorse
};
Now you can import it anywhere:
import { Morse } from "./morse.js";
Morse.encode("test");
Morse.decode("...");
Clean. Reusable..
Where this actually helps
You’re probably not building a Morse app.
But this pattern shows up in real projects:
- Encoding systems for games
- Signal-based puzzles
- Accessibility tools (audio or vibration signals)
- Learning apps
Also, it’s a good exercise in mapping, transformation, and timing control in JavaScript.
Small problem. Clear logic.
Final note
Morse code looks old. The implementation isn’t.
It’s just string mapping, timing, and basic data structures. Nothing fancy.
And that’s why it works so well in code.