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.