Storing Japanese pitch accent as one number (and rendering it in 15 lines of JS)

If you are building a flashcard app, a dictionary UI or anything else that teaches Japanese, sooner or later someone asks for pitch accent. Japanese pitch accent is not stress: syllables are not louder or longer, they are simply higher or lower, and the place where the pitch drops can change the meaning of a word. Here is a short explanation of Japanese pitch accent with audio examples if you want the linguistics first. This post is about the data side: how to store it compactly and draw it.

Why Japanese pitch accent needs only one number

Japanese dictionaries describe a word's accent with a single integer — the mora after which the pitch drops. In standard (Tokyo) Japanese everything else follows from that number:

  • 0 — no drop (heiban): first mora low, the rest high, and a following particle stays high;
  • 1 — drop after the first mora (atamadaka): high, then everything low;
  • n — the first mora is low, morae 2…n are high, then the pitch drops. If n equals the word length (odaka), the drop only shows up on the particle.

The classic triplet is はし:

Word Meaning Accent Pattern + particle
箸 chopsticks 1 H L – L
橋 bridge 2 L H – L
端 edge 0 L H – H

Note that 橋 and 端 sound identical on their own. The difference appears only when you say はしが or はしを — which is exactly why your data model should keep the particle in mind.

Counting morae for pitch accent

The accent number counts morae, not kana characters. Small ゃ ゅ ょ merge with the preceding kana (きょ is one mora), while っ, ん and the long-vowel mark ー each count as a full mora. That is easy to code:

const SMALL = new Set([...'ゃゅょぁぃぅぇぉャュョァィゥェォ']); function morae(kana) { const out = []; for (const ch of kana) { if (SMALL.has(ch) && out.length) out[out.length - 1] += ch; else out.push(ch); } return out; } morae('きょう'); // ['きょ', 'う'] morae('がっこう'); // ['が', 'っ', 'こ', 'う'] 

Rendering the pattern

With morae and the accent number, the high/low pattern is one expression:

function pitchPattern(kana, accent) { const m = morae(kana); const high = (i) => accent === 1 ? i === 0 : i > 0 && (accent === 0 || i < accent); const word = m.map((_, i) => (high(i) ? 'H' : 'L')).join(''); const particle = accent === 0 ? 'H' : 'L'; return `${word}-${particle}`; } pitchPattern('はし', 1); // 'HL-L' 箸 chopsticks pitchPattern('はし', 2); // 'LH-L' 橋 bridge pitchPattern('はし', 0); // 'LH-H' 端 edge pitchPattern('あなた', 2); // 'LHL-L' あなた you pitchPattern('たまご', 0); // 'LHH-H' 卵 egg 

From there you can draw an overline over the high morae, a small hook where the pitch drops, or a line chart — whatever fits your UI. Storing { kana: 'はし', accent: 2 } is far cheaper than storing a hand-made pattern for every word, and it never gets out of sync.

Caveats worth putting in your README

  • This is the Tokyo standard. Kansai (Osaka) accent works differently, so do not label it as "the" correct pronunciation.
  • Some words have two accepted accents; store an array if your source lists both.
  • Beginners rarely need to memorise numbers. Context usually tells 箸 and 橋 apart — the pattern is a listening aid, not a test.

What to show learners

In practice the most useful UI is not the number but the melody: a visual H/L line plus native audio, and a way to repeat after it. If you want to see how a learner-facing page presents Japanese pitch accent, compare the examples above with the table and FAQ on Japanese Zen — then decide how much of the theory your users actually need.