What You'll Build
By the end of this tutorial, you'll have a working BMI calculator — a real, usable tool that:
- Accepts height (in cm) and weight (in kg) from the user
- Calculates BMI using the formula:
weight ÷ (height in meters)² - Displays the BMI number and its category (Underweight, Normal, Overweight, Obese)
- Colour-codes the result based on the category
- Updates instantly as you type — no submit button needed
- Handles invalid input gracefully
What You'll Learn
- HTML: Building a form with labels, inputs, and result containers
- CSS: Card layout, input styling, focus states, colour-coded results
- JavaScript: Reading input values, arithmetic, conditional logic, live event handling
- Pattern: The "input → calculate → display" pattern that powers every calculator app
Before you start: This tutorial assumes you know basic HTML tags and can write a JavaScript function. If you're brand new, take 20 minutes through our HTML course first — you'll get more from this project.
Set Up the HTML Structure
Create a new file called index.html. This is the skeleton — a card containing two input fields and a result area.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>BMI Calculator</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="card"> <h1>⚖️ BMI Calculator</h1> <p class="sub">Enter your height and weight</p> <div class="field"> <label for="height">Height (cm)</label> <input type="number" id="height" placeholder="170"> </div> <div class="field"> <label for="weight">Weight (kg)</label> <input type="number" id="weight" placeholder="70"> </div> <div class="result" id="result"> <div class="num" id="num">—</div> <div class="cat" id="cat">Enter values above</div> </div> </div> <script src="script.js"></script> </body> </html>
Why this structure? Each element has a purpose:
.card— the container that holds everything, so we can style it as a unit#heightand#weight— the inputs we'll read from JavaScript#num— where the BMI number displays#cat— where the category text displays#result— the container that changes colour based on the category
Style It with CSS
Create a new file called style.css in the same folder. This makes the calculator look clean and modern.
/* Reset defaults so spacing is predictable */ * { box-sizing: border-box; margin: 0; padding: 0; } /* Center the card on the page */ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f1f5f9; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 20px; color: #0f172a; } /* The white card */ .card { background: #ffffff; border-radius: 16px; padding: 28px; max-width: 380px; width: 100%; box-shadow: 0 10px 40px rgba(15, 23, 42, 0.1); } h1 { font-size: 1.4rem; margin-bottom: 4px; } .sub { color: #64748b; font-size: 0.85rem; margin-bottom: 20px; } .field { margin-bottom: 14px; } label { display: block; font-size: 0.82rem; font-weight: 600; color: #334155; margin-bottom: 6px; } input { width: 100%; padding: 12px 14px; border: 2px solid #e2e8f0; border-radius: 10px; font-size: 1rem; font-weight: 600; outline: none; transition: 0.2s; } input:focus { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); } /* Result box — default neutral state */ .result { margin-top: 20px; padding: 20px; border-radius: 12px; text-align: center; background: #f1f5f9; transition: 0.3s; } .result .num { font-size: 2.4rem; font-weight: 900; line-height: 1; color: #64748b; font-family: "Courier New", monospace; } .result .cat { font-size: 0.9rem; font-weight: 700; margin-top: 6px; color: #64748b; } /* Category colors */ .result.under { background: #e0f2fe; } .result.under .num, .result.under .cat { color: #0284c7; } .result.normal { background: #dcfce7; } .result.normal .num, .result.normal .cat { color: #16a34a; } .result.over { background: #fef3c7; } .result.over .num, .result.over .cat { color: #d97706; } .result.obese { background: #fee2e2; } .result.obese .num, .result.obese .cat { color: #dc2626; }
Key CSS concept: We define four result states (.under, .normal, .over, .obese) and JavaScript will add the right class to #result. This is a common pattern — CSS handles the visual states, JS decides which state to show.
Write the JavaScript Logic
Create script.js in the same folder. This is where the calculator actually does its job.
// 1. Grab the elements from the page const heightEl = document.getElementById("height"); const weightEl = document.getElementById("weight"); const resultEl = document.getElementById("result"); const numEl = document.getElementById("num"); const catEl = document.getElementById("cat"); // 2. The main function — runs every time an input changes function calculate() { const h = parseFloat(heightEl.value); const w = parseFloat(weightEl.value); // 3. Guard clause — if input is invalid, show placeholder if (!h || !w || h <= 0 || w <= 0) { resultEl.className = "result"; numEl.textContent = "—"; catEl.textContent = "Enter values above"; return; } // 4. BMI formula — height must be in meters const meters = h / 100; const bmi = w / (meters * meters); const rounded = bmi.toFixed(1); // 5. Determine category and CSS class let cat, cls; if (bmi < 18.5) { cat = "Underweight"; cls = "under"; } else if (bmi < 25) { cat = "Normal weight"; cls = "normal"; } else if (bmi < 30) { cat = "Overweight"; cls = "over"; } else { cat = "Obese"; cls = "obese"; } // 6. Update the page resultEl.className = "result " + cls; numEl.textContent = rounded; catEl.textContent = cat; } // 7. Recalculate every time the user types heightEl.addEventListener("input", calculate); weightEl.addEventListener("input", calculate); // 8. Run once on load in case the fields are pre-filled calculate();
Save all three files and open index.html in your browser. Type a height and weight — the BMI should calculate instantly. You just built a working calculator.
Understand What You Just Built
Take a moment to appreciate what's happening here. You used the input → calculate → display pattern — the backbone of every calculator app, from a simple tip splitter to a complex mortgage simulator.
Here's the pattern abstracted:
- Capture input:
parseFloat()converts the text inside the input into a usable number. - Validate: The
if (!h || !w)guard prevents NaN errors and division by zero. - Process: The formula runs — in this case,
w / (h/100)². - Display: We update two DOM elements: the number and the category.
- State styling: By swapping the class on
#result, CSS handles all visual feedback. - Live updates: The
inputevent fires on every keystroke — no button needed.
Extend It — Practice Challenges
You have a working calculator. Now it's time to make it your own. Try these challenges in order — each one builds on what you've learned.
🟢 Beginner Challenges
- Add a healthy weight range below the result —
min = 18.5 × (h)²,max = 24.9 × (h)² - Show a tips line under each category (e.g., "Aim for 30 min of walking daily")
- Change the colour palette — try a dark mode version
🟡 Intermediate Challenges
- Add a metric ↔ imperial toggle — feet/inches and pounds
- Display BMI Prime (your BMI ÷ 25)
- Add a copy button that copies the result to clipboard
- Save the last-used height/weight to
localStorageso it persists on refresh
🔴 Advanced Challenges
- Add a colour-coded scale bar with a marker showing where you fall
- Add a projection chart — "If you weighed X, your BMI would be Y"
- Make it a PWA — installable on phones, works offline
Compare your solution: We built a full production version with all the advanced features. See how we handled the scale bar, imperial toggle, and BMI Prime — open the live BMI calculator.
Full Source Code
All three files together. Copy them into a folder and open index.html.
Common Mistakes & Fixes
❌ "Nothing happens when I type"
Check that you attached the event listeners correctly. Open the browser console (F12) — if you see an error like "Cannot read property 'value' of null," your element IDs don't match. Verify id="height" in HTML matches getElementById("height") in JS.
❌ "My result shows NaN"
NaN means "Not a Number." You're probably trying to calculate with empty inputs. The guard clause at the top of calculate() should catch this — check that it's not been accidentally removed.
❌ "Division by zero gives Infinity"
If height is 0, dividing weight by it gives Infinity. The guard clause (h <= 0) prevents this.
❌ "My category colours don't change"
Check the CSS class names — .result.under, .result.normal, etc. — match exactly what JavaScript sets. CSS class names are case-sensitive.
What to Build Next
You've learned the input → calculate → display pattern. Here are the natural next projects that use the same skill set with new twists:
Learn More — Related Lessons
This project used concepts from these lessons. If any part felt shaky, review them:
Snippets Used in This Project
Grab these ready-made snippets if you want to skip the copy-paste and start from a building block: