Everyone's first "real" program is a calculator, and for good reason: you already know exactly what it should do, so all your attention goes to how to make it do that. There's no domain to learn, no API to read — just buttons, a screen, and the logic in between. That makes it the honest place to meet the two ideas that every program after this one is built from: splitting work into small functions, and keeping track of state.
This is a true first build. It assumes no prior projects and no prerequisites — if you've never shipped anything, start here; it's the front door to the rest of the project library. One honest note on scope: a calculator looks trivial and mostly is, but the part in the middle — remembering what you typed, which operator you picked, and what happens when you press another one — is genuinely fiddly. That fiddliness is the lesson, not a distraction from it.
Estimated time: ~4 hours. The layout and the digit-typing come quickly. The time goes into the middle milestones, where you model what the calculator has to remember and reason through the button-to-button transitions. That part rewards going slow — rushing it gets you a calculator that works until it doesn't.
What you're building
A calculator you can actually use: a grid of buttons (digits 0–9, a decimal point, the four operators, equals, and clear) under a display that shows what you're typing and the answer when you hit equals. It behaves the way the one on your phone does — type 12, press +, type 8, press =, and see 20.
Stack: plain HTML, CSS, and JavaScript — no framework, no build step, no npm install. Just three files you open in a browser. That's a deliberate choice for a first project: a framework would hide the exact mechanics you're here to learn behind its own machinery. With vanilla JavaScript, every button press runs code you wrote and can see, top to bottom. You'll deploy it at the end so it has a real URL you can share.
You'll see this same shape — some HTML for structure, a little CSS for looks, and JavaScript that reacts to clicks — in almost every web project you ever build. Learning it here, with nothing else in the way, is why the calculator is the classic starting point. It's the smallest complete version of "a page that does something."
Milestone 0 — The layout: a button grid and a display
Create three files in a folder:
calculator/
index.html
style.css
app.js
Lay out the display and the buttons in index.html. A CSS grid turns a plain list of buttons into the familiar calculator shape:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Calculator</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="calculator">
<div id="display">0</div>
<div class="keys">
<button data-clear>C</button>
<button data-op="/">÷</button>
<button data-op="*">×</button>
<button data-op="-">−</button>
<button data-digit>7</button>
<button data-digit>8</button>
<button data-digit>9</button>
<button data-op="+" class="tall">+</button>
<!-- 4 5 6, then 1 2 3, then 0 and . -->
<button data-digit>0</button>
<button data-digit>.</button>
<button data-equals>=</button>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
Give the grid its shape in style.css:
.keys {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
#display {
text-align: right;
font-size: 2rem;
padding: 16px;
}
What to understand here: notice the data- attributes — data-digit, data-op="+", data-clear, data-equals. Those aren't decoration. They're how your JavaScript will tell one button from another without caring what character is printed on it. The visible label (×) and the value your code uses (*) are deliberately different: humans read ×, JavaScript multiplies with *. Keeping the meaning of a button in a data attribute, separate from its label, is a small habit that pays off in every UI you ever build. There's no JavaScript yet and nothing works — that's expected. You're building the stage before the actors show up.
Milestone 1 — Show what's typed: capturing digit input onto the display
Make the digits work first, before any math. When you press a number, it should appear on the display; press more, and they should build up left to right. In app.js:
const display = document.getElementById("display");
let typed = "";
function pressDigit(char) {
typed = typed + char;
display.textContent = typed;
}
document.querySelectorAll("[data-digit]").forEach((button) => {
button.addEventListener("click", () => pressDigit(button.textContent));
});
Open the page and press 1, then 2, then 3. The display should read 123.
What to understand before moving on:
querySelectorAll("[data-digit]")grabs every digit button at once, and the loop wires the same click handler to all of them. You write the behavior once; it applies to eleven buttons. Imagine instead copy-pasting a handler for each — this is your first taste of why not repeating yourself matters.typedlives outside the function on purpose. Each click needs to remember what came before it, so the string that's building up can't be a local variable that vanishes whenpressDigitreturns. This is the very first piece of state — a value your program keeps around between events. The next milestone makes that idea the whole point.
Milestone 2 — Model the state: current value, previous value, and the pending operator
Here's the beginner leap this whole project is built around. A calculator is easy to use because it quietly remembers three things for you. To build one, you have to make that memory explicit. Press 12, then +, then 8, then =. Walk through what the calculator must hold at each step:
- After
12: the number you're currently typing — call it the current value. - After
+: it has to stash that12somewhere (the previous value) and remember you chose+(the pending operator), then get ready for a fresh number. - After
8: a new current value, while12and+wait in the wings. - After
=: combine previous (12) and current (8) using the pending operator (+).
That's the entire model. Write it as three variables:
let current = "0"; // the number being typed right now
let previous = null; // the number waiting for an operation
let operator = null; // the pending operation: "+", "-", "*", "/"
function updateDisplay() {
display.textContent = current;
}
Why this is the milestone that matters: almost every bug you'll hit in this project is really a state bug — the calculator remembering the wrong thing, or forgetting something it needed. Once you can name the three things it has to track and say what each button press does to them, the code writes itself. Miss the model and you'll be patching symptoms forever. If you can explain, out loud, what current, previous, and operator each hold after every press in 12 + 8 =, you understand this calculator better than most people who've "finished" one.
The trickiest transition is pressing an operator, then typing the next digit. When you hit +, the display still shows 12. The moment you press 8, that 8 must replace the display, not append to it — you don't want 128. So an operator press has to set a flag like let resetOnNextDigit = true, and pressDigit must check it: if the flag is set, start current fresh from this digit and clear the flag; otherwise append. Forgetting this one transition is the single most common reason a hand-built calculator "works" but produces nonsense. Understanding why it happens — the display's job silently changes from "show what I'm typing" to "show the last answer" and back — is the real lesson.
Milestone 3 — The operations as functions (add, subtract, multiply, divide)
Now the math — and the second big skill: small, named functions that each do exactly one thing. Don't write one giant tangle of if statements. Write four tiny functions and a fifth that picks between them:
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }
function multiply(a, b) { return a * b; }
function divide(a, b) { return a / b; }
function operate(op, a, b) {
if (op === "+") return add(a, b);
if (op === "-") return subtract(a, b);
if (op === "*") return multiply(a, b);
if (op === "/") return divide(a, b);
}
What to understand here: you could absolutely have written the arithmetic inline, right where a button is clicked. Pulling it into named functions instead is the habit worth forming now, for reasons that outlast this project:
- Each function has one job and a name that says what it is. You can read
operate("+", 12, 8)and know exactly what it does without tracing any logic. Code that reads like a sentence is code you can debug. operateis the single place that maps an operator symbol to an action. Want to add a percent or a power button later? You change one function, not scatteredifbranches all over your click handlers. Keeping "which operation" in one spot is the same instinct as keeping state in named variables — put each responsibility in exactly one place.
This is the core skill the whole project is really teaching. "Break the problem into small named functions" sounds obvious written down; doing it by reflex is what separates code you can grow from code you have to rewrite.
Milestone 4 — Equals, wiring buttons to the logic, and Clear
Now connect the state from Milestone 2 to the functions from Milestone 3. An operator press banks the current number and records the operation; equals runs it:
document.querySelectorAll("[data-op]").forEach((button) => {
button.addEventListener("click", () => {
previous = current;
operator = button.dataset.op;
resetOnNextDigit = true; // next digit starts a fresh number
});
});
document.querySelector("[data-equals]").addEventListener("click", () => {
if (operator === null || previous === null) return;
const result = operate(operator, Number(previous), Number(current));
current = String(result);
operator = null;
previous = null;
updateDisplay();
});
document.querySelector("[data-clear]").addEventListener("click", () => {
current = "0";
previous = null;
operator = null;
resetOnNextDigit = false;
updateDisplay();
});
What to understand before moving on:
currentandpreviousare strings;operateneeds numbers. That's whatNumber(previous)is doing — converting the typed text"12"into the number12so the math is arithmetic, not string-gluing. Forget the conversion and"12" + "8"gives you"128", because+on two strings joins them. This is one of JavaScript's classic traps, and meeting it here on purpose beats being ambushed by it later.- Clear has to reset everything. Not just the display —
current,previous,operator, and the reset flag. A "clear" button that only blanks the screen but leaves a staleoperatorsitting in memory is the classic half-working calculator: it looks reset but behaves haunted. Truly resetting every piece of state is the point.
Milestone 5 — Edge cases (divide by zero, multiple decimals, leading zeros) and ship it
A tutorial calculator handles 2 + 2. A real one survives what people actually do to it. Handle these on purpose:
- Divide by zero. In JavaScript,
5 / 0doesn't crash — it quietly returnsInfinity, which looks broken on a display. Catch it and show a friendly message instead:
function divide(a, b) {
if (b === 0) return "Error";
return a / b;
}
- Multiple decimal points. Nothing stops a user pressing
.twice, giving you3.1.4, which isn't a number. Before adding a decimal, check whethercurrentalready contains one:if (char === "." && current.includes(".")) return;. - Leading zeros. Typing
0then5shouldn't read05. When the current value is just"0"and a digit is pressed, replace it rather than append.
Try 0.1 + 0.2 on your calculator. You'll get 0.30000000000000004, and it is not a bug in your code — it's how every language stores decimals in binary, JavaScript included. The fix isn't more logic; it's rounding for display: show Number(result.toFixed(10)), which trims the floating-point noise while keeping real precision. Knowing that this is a fundamental property of computers, not a mistake you made, is one of those facts that quietly makes you a more confident programmer.
Once the edge cases hold, put it on the internet. Because it's just static files, deploying is nearly free:
- Push the folder to a GitHub repo.
- Turn on GitHub Pages (repo Settings → Pages → deploy from your main branch), or drag the folder onto Netlify. Either gives you a public URL in minutes.
- Open the live link on your phone and do some math on the thing you built.
That's the loop that makes it real: not "code on my laptop," but a working tool anyone can open.
Stuck? Study after you've built your own version
When something won't click, reading other people's code helps — but after you've wrestled with the problem yourself, so you see what they did differently and why, instead of just copying an answer:
- freeCodeCamp/freeCodeCamp — its JavaScript calculator project and surrounding curriculum are a solid second angle on the same fundamentals once yours works. Build first, then compare.
- getify/You-Dont-Know-JS — the deep dive on the JavaScript behind this build: types, coercion, scope. Read the relevant part the moment a "wait, why does JavaScript do that?" question hits you — like the string-versus-number trap in Milestone 4.
The order matters: build first, read second. A finished implementation seen before you've struggled teaches you what the code is, never why it's shaped that way.
Build it with Cartara
Here's the honest part. You could get a calculator working by pasting each block above and never really grasping the operator-then-digit transition, why current is a string, or how operate keeps the math in one place. It would run. You'd also be unable to change it, debug it, or answer a single follow-up about it — and increasingly, an AI assistant will have written chunks of it for you, which makes that gap invisible until it bites.
That gap is exactly what Cartara exists to close. As you build:
- Point Cartara at your repo. It watches the code that lands — including anything an AI assistant generated — and turns each meaningful change into a checkpoint for understanding, not just a commit.
- Explain your changes back. When you add the divide-by-zero guard or the reset-on-next-digit flag, Cartara asks you the why and the trade-offs — the same questions a reviewer or interviewer would.
- See your comprehension, not just your output. You get an honest signal on which parts of your own first project you actually understand versus which you shipped on faith — so you can shore up the weak spots before you move on to something harder.
That's the loop this whole library is built around: build something real, then prove to yourself you understand it. The calculator is the project. The understanding is the point.
Make your first build one you can actually explain
Point Cartara at your calculator repo and it captures what you genuinely learned — the honest way to turn a first project into a skill you own.