Calculator

Your first real project. DOM events, state management, and clean architecture in one app.

Project Setup

Three files, zero dependencies. Pure JavaScript.

// Project structure:
// calculator/
// ├── index.html
// ├── style.css
// └── script.js

// Design decisions:
// - No frameworks — vanilla JS only
// - State object holds all calculator data
// - Event delegation on the button grid
// - Separate display logic from calculation logic

// The calculator handles:
// ✓ Basic operations (+, -, ×, ÷)
// ✓ Chained operations (2 + 3 × 4)
// ✓ Decimal numbers
// ✓ Clear / All Clear
// ✓ Keyboard support
// ✓ Display overflow handling

HTML Structure

Semantic markup with a grid layout for buttons.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Calculator</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div class="calculator">
    <div class="display">
      <div class="previous-operand"></div>
      <div class="current-operand">0</div>
    </div>
    <div class="buttons">
      <button data-action="clear" class="span-two">AC</button>
      <button data-action="delete">DEL</button>
      <button data-action="operator" data-value="÷">÷</button>
      <button data-action="number" data-value="7">7</button>
      <button data-action="number" data-value="8">8</button>
      <button data-action="number" data-value="9">9</button>
      <button data-action="operator" data-value="×">×</button>
      <button data-action="number" data-value="4">4</button>
      <button data-action="number" data-value="5">5</button>
      <button data-action="number" data-value="6">6</button>
      <button data-action="operator" data-value="-">-</button>
      <button data-action="number" data-value="1">1</button>
      <button data-action="number" data-value="2">2</button>
      <button data-action="number" data-value="3">3</button>
      <button data-action="operator" data-value="+">+</button>
      <button data-action="number" data-value="0" class="span-two">0</button>
      <button data-action="decimal">.</button>
      <button data-action="equals" class="equals">=</button>
    </div>
  </div>
  <script src="script.js"></script>
</body>
</html>
/* style.css — key parts */
.calculator {
  max-width: 320px;
  margin: 2rem auto;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}

.display {
  background: #1a1a2e;
  padding: 1.5rem;
  text-align: right;
  min-height: 100px;
}

.current-operand {
  font-size: 2.5rem;
  color: #fff;
}

.previous-operand {
  font-size: 1rem;
  color: #888;
  min-height: 1.5rem;
}

.buttons {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  background: #2d2d44;
}

button {
  padding: 1.25rem;
  font-size: 1.25rem;
  border: none;
  background: #16213e;
  color: #fff;
  cursor: pointer;
  transition: background 0.1s;
}

button:hover { background: #0f3460; }
button:active { background: #533483; }
.span-two { grid-column: span 2; }
.equals { background: #533483; }

State & Logic

A single state object drives the entire calculator.

// script.js — State management:
const state = {
  currentOperand: "0",
  previousOperand: "",
  operator: null,
  shouldResetDisplay: false,
};

// DOM references:
const currentDisplay = document.querySelector(".current-operand");
const previousDisplay = document.querySelector(".previous-operand");
const buttonsContainer = document.querySelector(".buttons");

// Single event listener using delegation:
buttonsContainer.addEventListener("click", (e) => {
  const button = e.target.closest("button");
  if (!button) return;

  const action = button.dataset.action;
  const value = button.dataset.value;

  switch (action) {
    case "number": inputNumber(value); break;
    case "operator": inputOperator(value); break;
    case "decimal": inputDecimal(); break;
    case "equals": calculate(); break;
    case "clear": clear(); break;
    case "delete": deleteDigit(); break;
  }

  updateDisplay();
});

One state object, one event listener. The switch routes button presses to handler functions.

1 / 2

Operations

The calculate function — where math happens.

function calculate() {
  if (!state.operator || state.shouldResetDisplay) return;

  const prev = parseFloat(state.previousOperand);
  const curr = parseFloat(state.currentOperand);
  let result;

  switch (state.operator) {
    case "+": result = prev + curr; break;
    case "-": result = prev - curr; break;
    case "×": result = prev * curr; break;
    case "÷":
      if (curr === 0) {
        state.currentOperand = "Error";
        state.operator = null;
        state.previousOperand = "";
        return;
      }
      result = prev / curr;
      break;
    default: return;
  }

  // Handle floating point display:
  state.currentOperand = formatResult(result);
  state.operator = null;
  state.previousOperand = "";
  state.shouldResetDisplay = true;
}

function formatResult(num) {
  // Avoid displaying 0.30000000000000004
  if (Number.isInteger(num)) return num.toString();
  // Limit decimal places, remove trailing zeros:
  return parseFloat(num.toFixed(10)).toString();
}

function inputDecimal() {
  if (state.shouldResetDisplay) {
    state.currentOperand = "0.";
    state.shouldResetDisplay = false;
    return;
  }
  // Prevent multiple decimals:
  if (state.currentOperand.includes(".")) return;
  state.currentOperand += ".";
}

function deleteDigit() {
  if (state.currentOperand === "Error") { clear(); return; }
  state.currentOperand = state.currentOperand.length === 1
    ? "0"
    : state.currentOperand.slice(0, -1);
}

Edge Cases

Handle the tricky stuff — keyboard, overflow, errors.

// Keyboard support:
document.addEventListener("keydown", (e) => {
  if (e.key >= "0" && e.key <= "9") inputNumber(e.key);
  else if (e.key === ".") inputDecimal();
  else if (e.key === "+" || e.key === "-") inputOperator(e.key);
  else if (e.key === "*") inputOperator("×");
  else if (e.key === "/") { e.preventDefault(); inputOperator("÷"); }
  else if (e.key === "Enter" || e.key === "=") calculate();
  else if (e.key === "Backspace") deleteDigit();
  else if (e.key === "Escape") clear();
  else return; // don't update display for unhandled keys

  updateDisplay();
});

// Display overflow — shrink font for long numbers:
function updateDisplay() {
  const text = state.currentOperand;
  currentDisplay.textContent = text;

  // Auto-shrink if too long:
  if (text.length > 12) {
    currentDisplay.style.fontSize = "1.5rem";
  } else if (text.length > 9) {
    currentDisplay.style.fontSize = "2rem";
  } else {
    currentDisplay.style.fontSize = "2.5rem";
  }

  previousDisplay.textContent = state.operator
    ? `${state.previousOperand} ${state.operator}`
    : "";
}

// Prevent number overflow:
function inputNumber(num) {
  if (state.currentOperand.replace(".", "").length >= 15) return;
  // ... rest of inputNumber logic
}

// Key patterns used in this project:
// ✓ State machine (single source of truth)
// ✓ Event delegation (one listener for all buttons)
// ✓ Data attributes (data-action, data-value)
// ✓ Separation of concerns (logic vs display)
// ✓ Guard clauses (early returns for edge cases)

FAQ

Common questions about this project.