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.