A BMI Calculator That Shows Both JASSO and WHO Categories, Because They Disagree
BMI 25 is "Overweight" according to the WHO and "Obesity class 1" according to the Japan Society for the Study of Obesity. Same number, very different emotional weight. Most BMI calculators only show one. I built one that shows both side by side — and explains why the difference exists.
Every online BMI calculator tells you "Your BMI is 24.5 — Normal" or "Your BMI is 25.0 — Obese," in one voice. But BMI interpretation isn't universal. Japan's JASSO (Japan Society for the Study of Obesity) uses stricter thresholds than the WHO, based on epidemiological evidence that Asian populations accumulate visceral fat at lower BMI levels. Showing only one can seriously misrepresent reality.
🔗 Live demo: https://sen.ltd/portfolio/bmi-calc/
📦 GitHub: https://github.com/sen-ltd/bmi-calc
Enter height (cm) and weight (kg), get BMI + both category systems + an ideal-weight target based on BMI 22 (statistically associated with the lowest disease incidence for Japanese adults). Vanilla JS, zero deps, 80 lines of logic, 11 tests.
calculateBmi returns a rich result object
The calculation is trivial (kg / m²), but the return shape packs five useful derived values:
export function calculateBmi(heightCm, weightKg) {
if (!Number.isFinite(heightCm) || heightCm <= 0) return { error: 'invalid height' }
if (!Number.isFinite(weightKg) || weightKg <= 0) return { error: 'invalid weight' }
const heightM = heightCm / 100
const bmi = weightKg / (heightM * heightM)
return {
bmi: Math.round(bmi * 10) / 10,
heightCm,
weightKg,
heightM,
idealBmi22Weight: Math.round(22 * heightM * heightM * 10) / 10,
categoryJp: categoryJp(bmi),
categoryWho: categoryWho(bmi),
}
}
Math.round(bmi * 10) / 10 gives you a number rounded to one decimal. toFixed(1) is tempting but returns a string, which is inconvenient if you want to do any comparison downstream.
The idealBmi22Weight field uses BMI 22 because population studies in Japan consistently show BMI 22 as the nadir of morbidity incidence for the Japanese adult population. The WHO's "ideal" range is 21-23, so 22 is a good compromise.
Guard clauses handle the boring cases: NaN, Infinity, negative, zero. Without the <= 0 check, dividing by zero produces Infinity in the BMI value and everything downstream silently breaks.
JASSO categories are stricter than WHO
Japan's system uses "obesity" starting at BMI 25:
export function categoryJp(bmi) {
if (bmi < 18.5) return { id: 'under', ja: '低体重', en: 'Underweight' }
if (bmi < 25) return { id: 'normal', ja: '普通体重', en: 'Normal' }
if (bmi < 30) return { id: 'obese1', ja: '肥満(1度)', en: 'Obesity class 1' }
if (bmi < 35) return { id: 'obese2', ja: '肥満(2度)', en: 'Obesity class 2' }
if (bmi < 40) return { id: 'obese3', ja: '肥満(3度)', en: 'Obesity class 3' }
return { id: 'obese4', ja: '肥満(4度)', en: 'Obesity class 4' }
}
The WHO treats BMI 25-30 as a distinct "overweight" category before reaching "obese":
export function categoryWho(bmi) {
if (bmi < 16) return { id: 'severe-under', ja: '高度やせ', en: 'Severely underweight' }
if (bmi < 18.5) return { id: 'under', ja: 'やせ', en: 'Underweight' }
if (bmi < 25) return { id: 'normal', ja: '標準', en: 'Normal' }
if (bmi < 30) return { id: 'over', ja: '過体重', en: 'Overweight' }
if (bmi < 35) return { id: 'obese1', ja: '肥満 I', en: 'Obese I' }
if (bmi < 40) return { id: 'obese2', ja: '肥満 II', en: 'Obese II' }
return { id: 'obese3', ja: '肥満 III', en: 'Obese III' }
}
The difference matters most at BMI 25, which is JASSO "Obesity class 1" but WHO "Overweight." These carry very different psychological weights. The WHO classification gives you a soft warning; the JASSO classification jumps straight to "obese." Showing both is a way of letting the user weigh the medical evidence without pretending one authority is canonical.
The reason Japan is stricter isn't arbitrary. Large epidemiological studies have shown that Asian populations — Japanese, Chinese, South Asian — tend to accumulate visceral adipose tissue and develop metabolic syndrome at lower BMIs than European populations. BMI 25 for a Japanese adult and BMI 25 for a European adult represent different metabolic health pictures.
Category as {id, ja, en} object, not just a string
Each category returns an object with three fields:
-
idis stable and machine-readable for CSS/color/icon lookup (.category-obese1) -
jaandenare human-facing labels
.category-normal { color: var(--ok); }
.category-over { color: var(--warn); }
.category-obese1 { color: var(--warn-strong); }
.category-obese2, .category-obese3, .category-obese4 { color: var(--danger); }
Having id separate from localized text means UI code never has to do ja.includes('肥満')-style string matching for logic — that's a fragile pattern that breaks the moment you add a locale or change a label.
Tests
11 cases on node --test. The critical ones are boundary tests on both sides of BMI 25 for both classification systems:
test('BMI 22 returns exact 22 for 170cm and 63.58kg', () => {
const r = calculateBmi(170, 63.58)
assert.equal(r.bmi, 22)
})
test('JASSO boundary: BMI 24.9 is normal', () => {
const r = calculateBmi(170, 71.94)
assert.equal(r.categoryJp.id, 'normal')
})
test('JASSO boundary: BMI 25.0 is obese1', () => {
const r = calculateBmi(170, 72.25)
assert.equal(r.categoryJp.id, 'obese1')
})
test('WHO BMI 25.0 is overweight (not obese)', () => {
const r = calculateBmi(170, 72.25)
assert.equal(r.categoryWho.id, 'over')
})
test('idealBmi22Weight for 160cm is 56.3', () => {
const r = calculateBmi(160, 50)
assert.equal(r.idealBmi22Weight, 56.3)
})
test('zero height returns error', () => {
assert.ok(calculateBmi(0, 60).error)
})
Testing the 24.9 → 25.0 transition and comparing both classifications at BMI 25.0 would catch anyone who mixed up < vs <= on either implementation. Without the paired tests, one-sided bugs stay invisible.
Medical disclaimer
BMI is a population-level screening tool, not a diagnosis. It doesn't distinguish muscle from fat, it's less meaningful at extremes of height, and a specific number is not medical advice. Talk to a doctor or registered dietitian before making health decisions based on these numbers.
Series
This is entry #16 in my 100+ public portfolio series.
- 📦 Repo: https://github.com/sen-ltd/bmi-calc
- 🌐 Live: https://sen.ltd/portfolio/bmi-calc/
- 🏢 Company: https://sen.ltd/
Planned future companions: basal metabolic rate, daily caloric needs (Harris-Benedict).

Top comments (0)