Loading...
Loading...
Your first real project. DOM events, state management, and clean architecture in one app.
Module 11 · JavaScript Projects
Theory becomes skill through building. These guided projects: calculator, todo app, weather app, and more: combine everything you've learned into real applications you can extend, share, and put in a portfolio.
Learn Calculator in JavaScript: covering Project Setup, HTML Structure, State & Logic, Operations with interactive visual examples and step-by-step explanations.
Reading about JavaScript is not the same as building with it. These guided projects combine variables, functions, DOM manipulation, async fetch, and state management into applications you can run, extend, and showcase.
Each project walks through architecture decisions, common pitfalls, and incremental steps. By the end you will have portfolio-ready apps and the confidence to start your own projects from scratch.
Understanding calculator is essential for every JavaScript developer. It shows up in frontend UI code, backend APIs, and framework internals, and getting it wrong leads to bugs that are hard to trace. This chapter builds intuition with visuals so the behavior sticks.
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
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; }
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.
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); }
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)
Common questions about calculator.