What You'll Build

A percentage calculator with four modes: "X% of Y", "X is what % of Y", "percentage change", and "add/subtract a percent". Tab switching changes the input fields and the formula, but they all share one result area.

  • Four calculation modes switched by tabs
  • Live calculation as you type
  • Formula display showing the math behind the result
  • Input validation with clear error messages
  • Empty inputs handled gracefully
▶ Live Preview — this is what you'll build

What You'll Learn

Key insight: The result display is shared across all modes. Only the inputs change. This is how real-world tools keep their UI small while offering many features.

1

Build the HTML Structure

Create index.html. The four panels are all present, but only one is visible at a time.

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Percentage Calculator</title>
</head>
<body>
    <div class="card">
        <h1>Percentage Calculator</h1>
        <p class="sub">Four modes - click a tab</p>

        <div class="tabs">
            <button class="tab active" data-mode="1">What is X% of Y?</button>
            <button class="tab" data-mode="2">X is what % of Y?</button>
            <button class="tab" data-mode="3">% change from X to Y</button>
            <button class="tab" data-mode="4">Add / subtract a %</button>
        </div>

        <div class="panel active" id="p1">
            <div class="row">
                <div class="field">
                    <label>Percent (%)</label>
                    <input type="number" id="p1p" value="15">
                </div>
                <div class="field">
                    <label>Of value</label>
                    <input type="number" id="p1v" value="200">
                </div>
            </div>
        </div>

        <div class="panel" id="p2">
            <div class="row">
                <div class="field">
                    <label>Value X</label>
                    <input type="number" id="p2x" value="30">
                </div>
                <div class="field">
                    <label>Total Y</label>
                    <input type="number" id="p2y" value="150">
                </div>
            </div>
        </div>

        <div class="panel" id="p3">
            <div class="row">
                <div class="field">
                    <label>From</label>
                    <input type="number" id="p3a" value="80">
                </div>
                <div class="field">
                    <label>To</label>
                    <input type="number" id="p3b" value="100">
                </div>
            </div>
        </div>

        <div class="panel" id="p4">
            <div class="row">
                <div class="field">
                    <label>Base value</label>
                    <input type="number" id="p4v" value="200">
                </div>
                <div class="field">
                    <label>Percent (%)</label>
                    <input type="number" id="p4p" value="10">
                </div>
            </div>
        </div>

        <div class="result">
            <div class="num" id="resultNum">30</div>
            <div class="formula" id="resultFormula">Enter values</div>
        </div>
    </div>

    <script src="script.js"></script>
</body>
</html>

Structure: Notice the data-mode attribute on each tab. This is how JavaScript knows which panel to show. When you click a tab, JS reads its data-mode and shows #p{mode}.

2

Style It with CSS

CSS
* { box-sizing: border-box; margin: 0; padding: 0; }

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background: #f1f5f9;
    padding: 20px;
    color: #0f172a;
    min-height: 100vh;
}

.card {
    max-width: 460px;
    margin: 0 auto;
    background: #fff;
    border-radius: 16px;
    padding: 24px;
    box-shadow: 0 10px 40px rgba(15,23,42,0.08);
}

h1 { font-size: 1.3rem; margin-bottom: 4px; }
.sub { color: #64748b; font-size: 0.85rem; margin-bottom: 16px; }

/* Tabs - 2x2 grid */
.tabs {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 6px;
    margin-bottom: 16px;
}
.tab {
    background: #f1f5f9;
    border: 2px solid #e2e8f0;
    color: #64748b;
    padding: 10px 8px;
    border-radius: 10px;
    font-weight: 700;
    font-size: 0.78rem;
    cursor: pointer;
    transition: 0.2s;
    font-family: inherit;
    text-align: left;
    line-height: 1.3;
}
.tab:hover { border-color: #cbd5e1; color: #334155; }
.tab.active {
    background: #eff6ff;
    border-color: #3b82f6;
    color: #1d4ed8;
}

/* Panels - only active is visible */
.panel { display: none; margin-bottom: 8px; }
.panel.active { display: block; }

/* Two-column input row */
.row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
}

.field { margin-bottom: 10px; }
label {
    display: block;
    font-size: 0.75rem;
    font-weight: 600;
    color: #334155;
    margin-bottom: 4px;
}

input[type="number"] {
    width: 100%;
    padding: 11px 13px;
    border: 2px solid #e2e8f0;
    border-radius: 10px;
    font-size: 1rem;
    font-weight: 600;
    font-family: inherit;
    outline: none;
    transition: 0.2s;
}
input:focus {
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
}

/* Result box */
.result {
    background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
    border-radius: 12px;
    padding: 22px;
    text-align: center;
    color: #fff;
    margin-top: 10px;
}
.result .num {
    font-size: 2.4rem;
    font-weight: 900;
    font-family: "Courier New", monospace;
    line-height: 1;
    word-break: break-all;
}
.result .formula {
    font-size: 0.78rem;
    opacity: 0.9;
    margin-top: 10px;
    font-family: "Courier New", monospace;
}
3

Write the JavaScript — Tab Switching

The tab-switching logic uses a single state variable (mode) and re-renders when it changes. This is the pattern behind every settings page, wizard, or dashboard filter you'll ever build.

JavaScript — Part 1: Tabs
// ============================================================
// STATE — the currently active mode (1-4)
// ============================================================

var mode = 1;

var tabs = document.querySelectorAll(".tab");

// ============================================================
// TAB SWITCHING
// ============================================================
// When a tab is clicked:
//   1. Move the "active" class to the clicked tab
//   2. Move the "active" class to the matching panel
//   3. Update the mode variable
//   4. Recalculate with the new mode

tabs.forEach(function(tab) {
    tab.addEventListener("click", function() {
        // 1. Update active tab
        tabs.forEach(function(t) { t.classList.remove("active"); });
        tab.classList.add("active");

        // 2. Update mode
        mode = parseInt(tab.dataset.mode);

        // 3. Show matching panel, hide others
        document.querySelectorAll(".panel").forEach(function(p) {
            p.classList.remove("active");
        });
        document.getElementById("p" + mode).classList.add("active");

        // 4. Recalculate
        calculate();
    });
});

Why data-mode instead of id? Data attributes let us attach custom values to any element. The tab's mode number (1, 2, etc.) lives in data-mode, and we read it with tab.dataset.mode. It's clean and doesn't clutter the ID namespace.

4

Write the JavaScript — The Calculate Function

JavaScript — Part 2: Calculate
// ============================================================
// CALCULATE — one function, four code paths
// ============================================================
// Each mode reads from its own inputs, but they all write
// to the SAME result box. That's the key insight.

function calculate() {
    var num = document.getElementById("resultNum");
    var fml = document.getElementById("resultFormula");
    var result;

    // ---------- MODE 1: What is X% of Y? ----------
    if (mode === 1) {
        var p = parseFloat(document.getElementById("p1p").value);
        var v = parseFloat(document.getElementById("p1v").value);

        if (isNaN(p) || isNaN(v)) {
            num.textContent = "—";
            fml.textContent = "Enter values above";
            return;
        }

        result = (p / 100) * v;
        num.textContent = fmt(result);
        fml.textContent = "(" + p + " ÷ 100) × " + v + " = " + fmt(result);
        return;
    }

    // ---------- MODE 2: X is what % of Y? ----------
    if (mode === 2) {
        var x = parseFloat(document.getElementById("p2x").value);
        var y = parseFloat(document.getElementById("p2y").value);

        if (isNaN(x) || isNaN(y) || y === 0) {
            num.textContent = "—";
            fml.textContent = "Total cannot be zero";
            return;
        }

        result = (x / y) * 100;
        num.textContent = fmt(result) + "%";
        fml.textContent = "(" + x + " ÷ " + y + ") × 100 = " + fmt(result) + "%";
        return;
    }

    // ---------- MODE 3: Percentage change ----------
    if (mode === 3) {
        var a = parseFloat(document.getElementById("p3a").value);
        var b = parseFloat(document.getElementById("p3b").value);

        if (isNaN(a) || isNaN(b) || a === 0) {
            num.textContent = "—";
            fml.textContent = "Original cannot be zero";
            return;
        }

        result = ((b - a) / a) * 100;
        var sign = result >= 0 ? "+" : "";
        num.textContent = sign + fmt(result) + "%";
        fml.textContent = "((" + b + " − " + a + ") ÷ " + a + ") × 100 = " + sign + fmt(result) + "%";
        return;
    }

    // ---------- MODE 4: Add / subtract a percent ----------
    var base = parseFloat(document.getElementById("p4v").value);
    var pct = parseFloat(document.getElementById("p4p").value);

    if (isNaN(base) || isNaN(pct)) {
        num.textContent = "—";
        fml.textContent = "Enter values above";
        return;
    }

    result = base * (1 + pct / 100);
    num.textContent = fmt(result);
    fml.textContent = base + " × (1 + " + pct + " ÷ 100) = " + fmt(result);
}

// ============================================================
// NUMBER FORMATTING — handle rounding nicely
// ============================================================

function fmt(n) {
    if (!isFinite(n)) return "—";
    return Math.round(n * 10000) / 10000;
}

Save all files and open index.html. Click through the four tabs — the inputs change and the result updates. Type in any field — the calculation runs live.

5

Write the JavaScript — Events & Boot

JavaScript — Part 3: Events
// ============================================================
// EVENTS — recalculate on every input
// ============================================================
// There are 8 inputs across the four panels. We attach the
// same listener to all of them — no need to track which
// panel each one belongs to.

document.querySelectorAll("input").forEach(function(el) {
    el.addEventListener("input", calculate);
    el.addEventListener("change", calculate);
});

// ============================================================
// BOOT — run once on page load
// ============================================================

calculate();

Efficiency note: We attach the same calculate() function to every input. When any of them changes, we recalculate based on the current mode. No need for per-panel handlers.

6

Understand the Big Ideas

1. Multi-mode UI with shared output

All four modes write to the same #resultNum and #resultFormula elements. Only the inputs differ. This is a powerful pattern — it keeps the UI small while supporting many features.

2. State-driven rendering

The mode variable is the single source of truth. Everything on the page derives from it: which tab is highlighted, which panel is visible, which formula runs. Change the mode — everything updates. This is how modern frameworks like React think.

3. Early returns for validation

Notice how each mode's code checks its inputs and returns early if invalid. This pattern (called a "guard clause") keeps code flat and readable:

JavaScript
if (isNaN(p) || isNaN(v)) {
    num.textContent = "—";
    fml.textContent = "Enter values above";
    return;  // stop here — don't run the rest
}

Without this, you'd need if (valid) { ... } around all the calculation code. Guard clauses avoid that nesting.

4. The math behind each mode

Mode 1: X% of Y = (X ÷ 100) × Y
Mode 2: X is what % of Y = (X ÷ Y) × 100
Mode 3: % change = ((new − old) ÷ old) × 100
Mode 4: add X% = Y × (1 + X ÷ 100)

7

Practice Challenges

🟢 Beginner

🟡 Intermediate

🔴 Advanced

Compare your solution: We built the production version with all four modes — see the live Percentage Calculator.

Full Source Code

Common Mistakes & Fixes

❌ "Tab clicks don't switch panels"

The panel IDs must match the tab's data-mode. If the tab has data-mode="2", there must be a panel with id="p2". The code does document.getElementById("p" + mode).

❌ "Result shows the previous mode's number"

You're not calling calculate() after switching modes. The tab click handler must end with calculate().

❌ "NaN shows in the result"

One of the inputs is empty. Every mode has a guard clause for that — make sure the isNaN() check is happening before calculation.

❌ "Very small numbers look weird"

fmt() rounds to 4 decimals. For most use cases that's fine, but if you need more precision, remove the multiplication and use n.toFixed(6) instead.

What to Build Next