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 =.
| Feature | Implementation |
|---|---|
| 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 support | Map 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.
/* 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
/* 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.
// 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("%");
});
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:
// 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();
}
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
justEvaluatedflag 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 Errordisplay case β interviewers note defensive coding