1. The Problem
Build a browser-based calculator with buttons for digits 0–9, the four operations (+, −, ×, ÷), equals, and clear. Clicking buttons updates a display; clicking equals computes the result. This puts DOM & Events directly into practice.
Why this project? A calculator forces you to think about state (what's been entered so far), user interaction (buttons), and display (updating the DOM). It's the first project where JavaScript makes something visual happen in the browser.
2. How to Think About This
A calculator needs to track state between button clicks. At any moment, the state is:
- displayValue — what's shown on screen right now
- firstOperand — the number before the operator
- operator — which operation was chosen (+, -, *, /)
- waitingForSecond — true after an operator is pressed (next digits start a new number)
Instead of one big function, split the logic: one function handles digits, one handles operators, one handles equals. This is the separation of concerns principle.
3. The Build
Two files: index.html (the structure) and calc.js (the logic).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Calculator</title>
<style>
body { font-family: sans-serif; display: flex; justify-content: center; padding: 2rem; }
.calc { background: #1a1a1a; border-radius: 12px; padding: 1rem; width: 240px; }
.display { background: #000; color: #fff; font-size: 2rem; text-align: right;
padding: 0.5rem 1rem; border-radius: 8px; margin-bottom: 0.75rem;
min-height: 3rem; word-break: break-all; }
.buttons { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
button { padding: 1rem; font-size: 1.1rem; border: none; border-radius: 8px;
cursor: pointer; background: #333; color: #fff; }
button:hover { background: #444; }
.btn-op { background: #ff9500; }
.btn-eq { background: #ff9500; }
.btn-clr { background: #a6a6a6; color: #000; }
</style>
</head>
<body>
<div class="calc">
<div class="display" id="display">0</div>
<div class="buttons">
<button class="btn-clr" onclick="clearCalc()">AC</button>
<button onclick="inputDigit('±')">±</button>
<button onclick="inputDigit('%')">%</button>
<button class="btn-op" onclick="inputOp('/')">÷</button>
<button onclick="inputDigit('7')">7</button>
<button onclick="inputDigit('8')">8</button>
<button onclick="inputDigit('9')">9</button>
<button class="btn-op" onclick="inputOp('*')">×</button>
<button onclick="inputDigit('4')">4</button>
<button onclick="inputDigit('5')">5</button>
<button onclick="inputDigit('6')">6</button>
<button class="btn-op" onclick="inputOp('-')">−</button>
<button onclick="inputDigit('1')">1</button>
<button onclick="inputDigit('2')">2</button>
<button onclick="inputDigit('3')">3</button>
<button class="btn-op" onclick="inputOp('+')">+</button>
<button style="grid-column:span 2" onclick="inputDigit('0')">0</button>
<button onclick="inputDigit('.')">.</button>
<button class="btn-eq" onclick="calculate()">=</button>
</div>
</div>
<script src="calc.js"></script>
</body>
</html>
// Calculator state — everything the calculator "remembers"
let displayValue = "0"; // what's shown right now
let firstOperand = null; // the number before the operator
let operator = null; // which operator was pressed
let waitingForSecond = false; // true after an operator is pressed
// Get a reference to the display element
const display = document.getElementById("display");
// Update the display to show the current displayValue
function updateDisplay() {
display.textContent = displayValue;
}
// Called when a digit or decimal point button is clicked
function inputDigit(value) {
if (value === "±") {
// Toggle positive/negative
displayValue = String(-parseFloat(displayValue));
return updateDisplay();
}
if (value === "%") {
// Convert to percentage
displayValue = String(parseFloat(displayValue) / 100);
return updateDisplay();
}
if (value === "." && displayValue.includes(".")) return; // only one decimal
if (waitingForSecond) {
// Start fresh for the second number
displayValue = value === "." ? "0." : value;
waitingForSecond = false;
} else {
// Append digit to current display (replace "0" at start)
displayValue = displayValue === "0" ? value : displayValue + value;
}
updateDisplay();
}
// Called when an operator button (+, -, *, /) is clicked
function inputOp(op) {
// Store the current display value as the first operand
firstOperand = parseFloat(displayValue);
operator = op;
waitingForSecond = true; // next digits start the second number
}
// Called when = is clicked
function calculate() {
if (operator === null || waitingForSecond) return; // nothing to calculate
const secondOperand = parseFloat(displayValue);
let result;
switch (operator) {
case "+": result = firstOperand + secondOperand; break;
case "-": result = firstOperand - secondOperand; break;
case "*": result = firstOperand * secondOperand; break;
case "/":
if (secondOperand === 0) { displayValue = "Error"; return updateDisplay(); }
result = firstOperand / secondOperand;
break;
}
// Round to 10 decimal places to avoid floating-point display issues (0.1+0.2)
displayValue = String(parseFloat(result.toFixed(10)));
operator = null;
waitingForSecond = false;
updateDisplay();
}
// Called when AC (all clear) is clicked
function clearCalc() {
displayValue = "0";
firstOperand = null;
operator = null;
waitingForSecond = false;
updateDisplay();
}
4. Test & Prove
The pure calculation logic can be tested without a browser. Extract the maths into a module:
// Pure calculate function (extract this from calc.js for testing)
function applyOp(a, op, b) {
switch (op) {
case "+": return a + b;
case "-": return a - b;
case "*": return a * b;
case "/": if (b === 0) return null; return a / b;
}
}
let pass = 0, fail = 0;
function test(desc, actual, expected) {
const ok = actual === expected || (actual !== null && Math.abs(actual - expected) < 1e-9);
ok ? pass++ : fail++;
console.log(ok ? "✓" : "✗", desc, ok ? "" : `(got ${actual}, expected ${expected})`);
}
test("10 + 5 = 15", applyOp(10, "+", 5), 15);
test("10 - 3 = 7", applyOp(10, "-", 3), 7);
test("6 × 7 = 42", applyOp(6, "*", 7), 42);
test("10 ÷ 4 = 2.5", applyOp(10, "/", 4), 2.5);
test("divide by zero → null", applyOp(5, "/", 0), null);
test("0.1 + 0.2 ≈ 0.3", parseFloat(applyOp(0.1, "+", 0.2).toFixed(10)), 0.3);
test("negative result: 3-8", applyOp(3, "-", 8), -5);
console.log(\`
\${pass} passed, \${fail} failed\`);
5. The Interface
Input
Mouse clicks (or touch) on the calculator buttons. No keyboard input in this version.
Output
The display updates after every button press. Equals shows the computed result. Dividing by zero shows "Error". AC resets everything to 0.
State machine
Idle → digit pressed → first number building → operator pressed → waiting for second → digit pressed → second number building → equals pressed → result shown → repeat.
6. Run It
# Open index.html in any browser — no server needed
open index.html # macOS
start index.html # Windows
# Run the logic tests (Node.js):
node calc.test.mjs🎯 Try this next
- Keyboard support — add a
keydownevent listener to handle number and operator keys - History — keep an array of past calculations and display the last 5
- Memory buttons — add M+, M-, MR (memory store, subtract, recall)
- Scientific mode — add sqrt, square, and 1/x buttons using
Math
What you practised
State management with plain variables, event handling via HTML onclick attributes vs addEventListener,
DOM text updates via textContent, separating pure logic from UI code for testability,
and floating-point display fixes with toFixed.
Reference pages: DOM & Events · Functions · Numbers & Math