Calculator
A fully functional calculator with keyboard support, history, and edge-case handling.
Project Overview
Build a calculator that handles the full operation cycle: number entry, operator selection, and result calculation. This project practises state management, event delegation, keyboard events, and edge-case logic.
What you will build
- Numeric display showing current input and expression
- Buttons: 0–9, ., +, -, ×, ÷, =, C, ±, %
- Keyboard support (number keys, operators, Enter, Backspace, Escape)
- Calculation history (last 5 operations)
- Edge case handling (divide by zero, leading zeros, multiple decimals)
Concepts used
- State object to track operands and operator
- Event delegation on the button grid
- Keyboard event handling on
document - Number parsing and floating-point rounding
Step-by-Step Build
Step 1 — State model
// The calculator's entire logic state
const state = {
current: '0', // what's displayed
previous: '', // first operand
operator: null, // +, -, *, /
shouldReset: false // true after = — next digit starts fresh
};
Step 2 — Digit input
function inputDigit(digit) {
if (state.shouldReset) {
state.current = digit;
state.shouldReset = false;
return;
}
if (state.current === '0' && digit !== '.') {
state.current = digit; // replace leading zero
} else if (digit === '.' && state.current.includes('.')) {
return; // prevent multiple decimals
} else {
state.current += digit;
}
}
Step 3 — Operator selection
function chooseOperator(op) {
if (state.operator && !state.shouldReset) {
calculate(); // chain: 5 + 3 × → compute 5+3 first
}
state.previous = state.current;
state.operator = op;
state.shouldReset = true;
}
Step 4 — Calculate
function calculate() {
if (!state.operator || state.shouldReset) return;
const a = parseFloat(state.previous);
const b = parseFloat(state.current);
let result;
switch (state.operator) {
case '+': result = a + b; break;
case '-': result = a - b; break;
case '*': result = a * b; break;
case '/':
if (b === 0) { state.current = 'Error'; state.operator = null; return; }
result = a / b;
break;
}
// Round floating-point noise (e.g. 0.1 + 0.2 = 0.30000000000000004)
state.current = String(parseFloat(result.toFixed(10)));
state.operator = null;
state.shouldReset = true;
}
Complete Working Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calculator</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { min-height:100vh; display:flex; align-items:center; justify-content:center;
background:#1a1a2e; font-family:sans-serif; }
.calc { background:#16213e; border-radius:1rem; padding:1.5rem; width:320px;
box-shadow:0 10px 40px #0008; }
.display { background:#0f3460; border-radius:.5rem; padding:1rem; margin-bottom:1rem;
text-align:right; min-height:80px; }
.expr { font-size:.8rem; color:#888; min-height:1.2em; }
.current { font-size:2.5rem; color:#fff; word-break:break-all; }
.buttons { display:grid; grid-template-columns:repeat(4,1fr); gap:.6rem; }
button {
padding:1rem; border:none; border-radius:.5rem; font-size:1.1rem;
cursor:pointer; transition:filter .1s;
}
button:hover { filter:brightness(1.2); }
button:active { filter:brightness(.9); }
.btn-num { background:#334155; color:#fff; }
.btn-op { background:#e26d5c; color:#fff; }
.btn-fn { background:#1e3a5f; color:#7dd3fc; }
.btn-eq { background:#3b82f6; color:#fff; grid-column:span 2; }
.btn-clear { background:#475569; color:#fff; }
.history { margin-top:1rem; color:#888; font-size:.78rem; }
.history li { padding:.2rem 0; border-bottom:1px solid #ffffff11; }
</style>
</head>
<body>
<div class="calc">
<div class="display">
<div class="expr" id="expr"></div>
<div class="current" id="display">0</div>
</div>
<div class="buttons">
<button class="btn-fn" data-action="clear">C</button>
<button class="btn-fn" data-action="sign">±</button>
<button class="btn-fn" data-action="percent">%</button>
<button class="btn-op" data-op="/">÷</button>
<button class="btn-num" data-digit="7">7</button>
<button class="btn-num" data-digit="8">8</button>
<button class="btn-num" data-digit="9">9</button>
<button class="btn-op" data-op="*">×</button>
<button class="btn-num" data-digit="4">4</button>
<button class="btn-num" data-digit="5">5</button>
<button class="btn-num" data-digit="6">6</button>
<button class="btn-op" data-op="-">−</button>
<button class="btn-num" data-digit="1">1</button>
<button class="btn-num" data-digit="2">2</button>
<button class="btn-num" data-digit="3">3</button>
<button class="btn-op" data-op="+">+</button>
<button class="btn-num" data-digit="0" style="grid-column:span 2">0</button>
<button class="btn-num" data-digit=".">.</button>
<button class="btn-eq" data-action="equals">=</button>
</div>
<ul class="history" id="history"></ul>
</div>
<script>
const displayEl = document.querySelector('#display');
const exprEl = document.querySelector('#expr');
const histEl = document.querySelector('#history');
const buttons = document.querySelector('.buttons');
const state = { current: '0', previous: '', operator: null, shouldReset: false };
const history = [];
function updateDisplay() {
displayEl.textContent = state.current;
exprEl.textContent = state.previous && state.operator
? `${state.previous} ${state.operator === '*' ? '×' : state.operator === '/' ? '÷' : state.operator}`
: '';
}
function inputDigit(d) {
if (state.shouldReset) { state.current = d; state.shouldReset = false; return; }
if (state.current === '0' && d !== '.') { state.current = d; return; }
if (d === '.' && state.current.includes('.')) return;
state.current += d;
}
function chooseOp(op) {
if (state.operator && !state.shouldReset) calculate(false);
state.previous = state.current;
state.operator = op;
state.shouldReset = true;
}
function calculate(addHistory = true) {
if (!state.operator || state.shouldReset) return;
const a = parseFloat(state.previous);
const b = parseFloat(state.current);
let result;
const expr = `${a} ${state.operator} ${b}`;
switch (state.operator) {
case '+': result = a + b; break;
case '-': result = a - b; break;
case '*': result = a * b; break;
case '/':
if (b === 0) { state.current = 'Error'; state.operator = null; updateDisplay(); return; }
result = a / b; break;
}
state.current = String(parseFloat(result.toFixed(10)));
if (addHistory) {
history.unshift(`${expr} = ${state.current}`);
if (history.length > 5) history.pop();
histEl.innerHTML = history.map(h => `<li>${h}</li>`).join('');
}
state.operator = null;
state.shouldReset = true;
}
buttons.addEventListener('click', e => {
const btn = e.target.closest('button');
if (!btn) return;
const { digit, op, action } = btn.dataset;
if (digit !== undefined) { inputDigit(digit); }
else if (op) { chooseOp(op); }
else if (action === 'equals') { calculate(); }
else if (action === 'clear') { Object.assign(state, { current:'0', previous:'', operator:null, shouldReset:false }); }
else if (action === 'sign') { state.current = String(parseFloat(state.current) * -1); }
else if (action === 'percent') { state.current = String(parseFloat(state.current) / 100); }
updateDisplay();
});
document.addEventListener('keydown', e => {
if ('0123456789.'.includes(e.key)) { inputDigit(e.key); }
else if (['+','-','*','/'].includes(e.key)) { chooseOp(e.key); }
else if (e.key === 'Enter' || e.key === '=') { calculate(); }
else if (e.key === 'Escape') { Object.assign(state, { current:'0', previous:'', operator:null, shouldReset:false }); }
else if (e.key === 'Backspace') {
if (!state.shouldReset && state.current.length > 1) {
state.current = state.current.slice(0, -1);
} else {
state.current = '0';
}
}
else return;
updateDisplay();
});
updateDisplay();
</script>
</body>
</html>
Code Explained
State machine approach
The calculator uses a state object with four properties. shouldReset is the key insight — after pressing = or an operator, the next digit should start a fresh number rather than appending to the current one.
Operator chaining
If the user types 5 + 3 ×, the second operator (×) triggers calculate(false) first (computing 5+3=8), then stores 8 as previous with the new operator. This matches standard calculator behaviour.
toFixed(10) + parseFloat
Floating-point arithmetic in JavaScript: 0.1 + 0.2 === 0.30000000000000004. Rounding to 10 decimal places with toFixed, then parsing back to a number with parseFloat removes the trailing noise while preserving precision for normal use.
Challenges
- Add a backspace button that removes the last digit.
- Add memory functions: M+, M-, MR, MC.
- Support keyboard input for the Backspace key.
- Add a scientific mode with square root, power, and logarithm buttons.
- Style the active operator button differently (highlight which operator is selected).