Ad – 728Γ—90
πŸ› οΈ Projects

JavaScript Calculator – Build a Functional Calculator from Scratch

Build a fully functional calculator with number input, four arithmetic operators, decimal support, keyboard shortcuts, and a clear/backspace function. This project practices DOM manipulation, event handling, and state management in a single self-contained JavaScript file.

⏱️ 25 min read 🎯 Advanced πŸ“… Updated 2026

Project Overview

The calculator supports all standard operations: addition, subtraction, multiplication, division, decimal input, chained operations, and keyboard input. The state is a single string expression that gets evaluated when the user presses =.

FeatureImplementation
Number buttons (0–9)Append digit to expression string
Operators (+, βˆ’, Γ—, Γ·)Append operator to expression string
Equals (=)Evaluate expression with Function()
Clear (C)Reset expression to ""
Backspace (⌫)Remove last character
Decimal (.)Append "." (prevent double dots)
Keyboard supportMap keydown events to handleInput()

HTML Structure

The calculator layout uses a CSS grid for the button pad and a display area at the top. Each button carries a data-value attribute that maps directly to what gets added to the expression string.

JavaScript
/* HTML Structure (save as calculator.html):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Calculator</title>
  <link rel="stylesheet" href="calc-style.css">
</head>
<body>
  <div class="calculator">
    <div class="calc-display">
      <div class="expression" id="expression"></div>
      <div class="result" id="result">0</div>
    </div>
    <div class="calc-buttons" id="calc-buttons">
      <button data-value="C"  class="btn-wide btn-clear">C</button>
      <button data-value="⌫" class="btn-secondary">⌫</button>
      <button data-value="%"  class="btn-secondary">%</button>
      <button data-value="/"  class="btn-operator">Γ·</button>

      <button data-value="7">7</button>
      <button data-value="8">8</button>
      <button data-value="9">9</button>
      <button data-value="*"  class="btn-operator">Γ—</button>

      <button data-value="4">4</button>
      <button data-value="5">5</button>
      <button data-value="6">6</button>
      <button data-value="-"  class="btn-operator">βˆ’</button>

      <button data-value="1">1</button>
      <button data-value="2">2</button>
      <button data-value="3">3</button>
      <button data-value="+"  class="btn-operator">+</button>

      <button data-value="+/-" class="btn-secondary">Β±</button>
      <button data-value="0">0</button>
      <button data-value=".">.</button>
      <button data-value="="  class="btn-equals">=</button>
    </div>
  </div>
  <script src="calc-script.js"></script>
</body>
</html>
*/

CSS Styling

JavaScript
/* calc-style.css

* { box-sizing: border-box; margin: 0; padding: 0; }
body {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background: #1a1a2e;
  font-family: 'Segoe UI', sans-serif;
}
.calculator {
  background: #16213e;
  border-radius: 20px;
  padding: 24px;
  box-shadow: 0 20px 60px rgba(0,0,0,0.5);
  width: 320px;
}
.calc-display {
  background: #0f3460;
  border-radius: 12px;
  padding: 16px 20px;
  margin-bottom: 20px;
  min-height: 90px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: flex-end;
}
.expression { color: #a0aec0; font-size: 14px; min-height: 20px; }
.result     { color: #ffffff; font-size: 36px; font-weight: 300; }
.calc-buttons {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
}
button {
  background: #1a1a2e;
  color: #e2e8f0;
  border: none;
  border-radius: 12px;
  padding: 18px;
  font-size: 18px;
  cursor: pointer;
  transition: background 0.15s;
}
button:hover  { background: #2d3748; }
button:active { transform: scale(0.95); }
.btn-operator { background: #2b6cb0; color: white; }
.btn-operator:hover { background: #3182ce; }
.btn-equals   { background: #e53e3e; color: white; }
.btn-equals:hover   { background: #fc8181; }
.btn-clear    { background: #c05621; color: white; }
.btn-secondary { background: #2d3748; }
*/

JavaScript Logic

The core logic uses a single expression string as state. All input appends to or modifies this string. On equals, the expression is evaluated safely using a Function constructor wrapped in try/catch.

JavaScript
// calc-script.js β€” Full working calculator logic

let expression = "";
let justEvaluated = false;

const expressionEl = document.getElementById("expression");
const resultEl     = document.getElementById("result");

function updateDisplay() {
  expressionEl.textContent = expression;
  if (expression === "") resultEl.textContent = "0";
}

function safeEval(expr) {
  try {
    // Security: only allow numbers and operators
    if (!/^[0-9+\-*/.%() ]+$/.test(expr)) throw new Error("Invalid");
    const result = Function('"use strict"; return (' + expr + ')')();
    if (!isFinite(result)) throw new Error("Math Error");
    // Round to avoid floating point display issues
    return parseFloat(result.toFixed(10)).toString();
  } catch {
    return "Error";
  }
}

function handleInput(value) {
  if (value === "C") {
    expression = "";
    resultEl.textContent = "0";
    justEvaluated = false;
    updateDisplay();
    return;
  }

  if (value === "⌫") {
    expression = expression.slice(0, -1);
    updateDisplay();
    return;
  }

  if (value === "=") {
    if (!expression) return;
    expressionEl.textContent = expression + " =";
    const result = safeEval(expression);
    resultEl.textContent = result;
    expression = result === "Error" ? "" : result;
    justEvaluated = true;
    return;
  }

  if (value === ".") {
    // Prevent multiple dots in the same number segment
    const parts = expression.split(/[+\-*/]/);
    const currentNum = parts[parts.length - 1];
    if (currentNum.includes(".")) return;
  }

  if (value === "+/-") {
    if (expression === "" || expression === "0") return;
    expression = expression.startsWith("-") ? expression.slice(1) : "-" + expression;
    updateDisplay();
    return;
  }

  const isOperator = ["+", "-", "*", "/", "%"].includes(value);

  // If just evaluated and user presses a number, start fresh
  if (justEvaluated && !isOperator) {
    expression = value;
    justEvaluated = false;
    updateDisplay();
    return;
  }
  justEvaluated = false;

  // Prevent double operators
  const lastChar = expression.slice(-1);
  if (isOperator && ["+", "-", "*", "/", "%"].includes(lastChar)) {
    expression = expression.slice(0, -1) + value;
  } else {
    expression += value;
  }

  updateDisplay();
}

// Button clicks via event delegation
document.getElementById("calc-buttons").addEventListener("click", e => {
  if (e.target.matches("button")) {
    handleInput(e.target.dataset.value);
  }
});

// Keyboard support
document.addEventListener("keydown", e => {
  if (e.key >= "0" && e.key <= "9") handleInput(e.key);
  if (["+", "-", "*", "/", "."].includes(e.key)) handleInput(e.key);
  if (e.key === "Enter" || e.key === "=") handleInput("=");
  if (e.key === "Backspace") handleInput("⌫");
  if (e.key === "Escape")    handleInput("C");
  if (e.key === "%")         handleInput("%");
});
Security Note: The Function() constructor can execute arbitrary JavaScript. The regex check /^[0-9+\-*/.%() ]+$/ acts as a whitelist guard β€” it ensures only numbers and arithmetic operators are evaluated, blocking any injection like alert(1).

Enhancements and Extensions

Once the basic calculator works, these enhancements increase complexity and make it more portfolio-worthy:

JavaScript
// Enhancement 1: Calculation history (last 5 results)
const history = [];

function recordHistory(expr, result) {
  history.unshift({ expr, result, time: new Date().toLocaleTimeString() });
  if (history.length > 5) history.pop();
  renderHistory();
}

function renderHistory() {
  const el = document.getElementById("history");
  if (!el) return;
  el.innerHTML = history.map(h =>
    `<div class="history-item">
      <span class="history-expr">${h.expr}</span>
      <span class="history-result">= ${h.result}</span>
    </div>`
  ).join("");
}

// Enhancement 2: Format large numbers with commas
function formatNumber(numStr) {
  const num = parseFloat(numStr);
  if (isNaN(num)) return numStr;
  return new Intl.NumberFormat().format(num);
}

// Enhancement 3: Percentage button logic
function handlePercent() {
  if (!expression) return;
  const parts = expression.split(/([+\-*/])/);
  const lastNum = parseFloat(parts[parts.length - 1]);
  if (isNaN(lastNum)) return;
  parts[parts.length - 1] = lastNum / 100;
  expression = parts.join("");
  updateDisplay();
}
Concepts Practiced: This project exercises DOM querying and manipulation, event handling and delegation, keyboard event mapping, string manipulation (the expression string), try/catch error handling, and basic security thinking (input validation before eval).

Testing Your Calculator

Run through this test checklist to verify all features work:

Test Checklist

  • Basic: 5 + 3 = 8
  • Chained: 5 + 3 * 2 = 11 (tests operator precedence)
  • Decimal: 1.5 + 2.5 = 4
  • Double decimal prevention: pressing "1.." should only give "1."
  • Backspace: 123⌫ should give 12
  • Clear: any expression then C should show 0
  • Division by zero: 5 / 0 should show "Infinity" or "Math Error"
  • Keyboard: type 7 + 8 then press Enter β€” should show 15
  • Double operator: 5 + * should replace + with *

Challenge: Scientific Mode

Extend the calculator with scientific functions:

  • Square root: Math.sqrt(x)
  • Power: x ** y
  • Trigonometry: Math.sin/cos/tan
  • Toggle between basic and scientific mode with a button

FAQ

Why use a string expression instead of tracking operands separately?

The string approach is simpler and automatically handles operator precedence (5 + 3 * 2 = 11, not 16) because Function() evaluates it like real JavaScript. The alternative β€” tracking currentValue, operator, pendingValue β€” requires manual precedence handling.

Is eval() safe to use in a calculator?

The raw eval() function is dangerous in production. Using Function() with a strict whitelist regex (allowing only digits and operators) makes it safe for a calculator context where input is tightly controlled.

How do I prevent floating point errors like 0.1 + 0.2 = 0.30000000000000004?

Use parseFloat(result.toFixed(10)) to round to 10 decimal places, which eliminates most floating point artifacts while preserving legitimate decimal precision.

How do I add a history panel?

Maintain a history array, push { expr, result } on each evaluation, and render it below the calculator. Store it in localStorage to persist across page loads.

Can I deploy this to GitHub Pages?

Yes. Push the HTML, CSS, and JS files to a GitHub repository, enable GitHub Pages in the repo settings, and your calculator will be live at username.github.io/repo-name.

Summary

  • State: a single expression string β€” simple and handles operator precedence automatically
  • Evaluation: Function() constructor with whitelist regex for security
  • Events: delegated click handler on the button container + keyboard event listener
  • Edge cases: double decimal, double operator, division by zero, floating point rounding
  • Concepts practiced: DOM manipulation, event handling, string methods, error handling
  • A working, polished calculator on GitHub Pages makes an excellent portfolio piece
  • The justEvaluated flag pattern is a good example of state machine thinking
  • Event delegation on the button container is more efficient than 20 individual listeners
  • Always handle the Error / Math Error display case β€” interviewers note defensive coding