Learn how to implement Morse code encoding and decoding in JavaScript with a simple, dependency-free approach. This guide covers mapping, edge cases, audio playback using the Web Audio API, and building a lightweight browser UI for real-time interaction.

How to Encode and Decode Morse Code in JavaScript

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.


Sponsors