The Codex / Projects / Calculator / JavaScript
Tier 0 — Absolute Beginner

Calculator

Build a browser calculator — DOM manipulation, state management, and event handling.

🐍 Python 🟨 JavaScript

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).

index.html
<!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>
calc.js
// 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:

calc.test.mjs
// 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

  1. Keyboard support — add a keydown event listener to handle number and operator keys
  2. History — keep an array of past calculations and display the last 5
  3. Memory buttons — add M+, M-, MR (memory store, subtract, recall)
  4. 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

Try It Yourself