Skip to content
Happy Programming Guide
Start learning
Programming Projects

JavaScript Project: Build a Calculator in the Browser

Build a working calculator with HTML, CSS Grid and JavaScript. Covers event delegation, managing state, keyboard support and the edge cases that break naive versions.

A browser calculator is deceptively good practice. The layout teaches CSS Grid, the buttons teach event delegation, and the logic teaches you to think carefully about state — because a naive version breaks the moment someone presses . twice.

The HTML#

HTML
<main class="calc">
  <output id="display">0</output>

  <div class="keys" id="keys">
    <button data-action="clear" class="span-2">AC</button>
    <button data-action="delete">DEL</button>
    <button data-op="/">÷</button>

    <button data-num="7">7</button>
    <button data-num="8">8</button>
    <button data-num="9">9</button>
    <button data-op="*">×</button>

    <button data-num="4">4</button>
    <button data-num="5">5</button>
    <button data-num="6">6</button>
    <button data-op="-">−</button>

    <button data-num="1">1</button>
    <button data-num="2">2</button>
    <button data-num="3">3</button>
    <button data-op="+">+</button>

    <button data-num="0" class="span-2">0</button>
    <button data-num=".">.</button>
    <button data-action="equals">=</button>
  </div>
</main>

<script src="calc.js" defer></script>

Every button carries a data- attribute saying what it is. That single decision is what lets one listener handle all nineteen buttons.

The CSS#

CSS
.calc {
  max-width: 320px;
  margin: 40px auto;
  padding: 16px;
  border-radius: 16px;
  background: #14161d;
  font-family: system-ui, sans-serif;
}

#display {
  display: block;
  padding: 18px 14px;
  margin-bottom: 12px;
  border-radius: 10px;
  background: #0c0e14;
  color: #f2f4f9;
  font-size: 2rem;
  text-align: right;
  overflow-x: auto;
  white-space: nowrap;
}

.keys {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
}

.keys button {
  padding: 16px 0;
  border: 0;
  border-radius: 10px;
  background: #262c39;
  color: #e5e8f0;
  font-size: 1.1rem;
  cursor: pointer;
}

.keys button:hover { background: #333a49; }
.keys button[data-op] { background: #4f46e5; color: #fff; }
.keys button[data-action="equals"] { background: #059669; color: #fff; }
.span-2 { grid-column: span 2; }

grid-template-columns: repeat(4, 1fr) plus span-2 for the wide keys gives the whole layout in four lines. See CSS basics explained.

Step 1: the state#

A calculator has exactly three things to remember.

JavaScript
const state = {
  current: "0",     // what is on screen
  previous: null,   // the number waiting for an operator
  operator: null,   // the pending operation
};

const display = document.querySelector("#display");

function render() {
  display.textContent = state.current;
}

Same pattern as the to-do app: keep the truth in an object, and draw the screen from it.

Step 2: typing numbers#

JavaScript
function inputNumber(char) {
  if (char === "." && state.current.includes(".")) return;   // only one dot

  if (state.current === "0" && char !== ".") {
    state.current = char;          // replace the leading zero
  } else {
    state.current += char;
  }
}

Those two guards are the difference between a calculator and a bug report. Without the first you get 3.4.5; without the second you get 07.

Step 3: the operators#

JavaScript
function calculate(a, b, operator) {
  a = Number(a);
  b = Number(b);

  switch (operator) {
    case "+": return a + b;
    case "-": return a - b;
    case "*": return a * b;
    case "/": return b === 0 ? null : a / b;   // null means "cannot"
    default: return b;
  }
}

function chooseOperator(op) {
  if (state.operator && state.previous !== null) {
    const result = calculate(state.previous, state.current, state.operator);
    if (result === null) { showError(); return; }
    state.current = String(result);
  }
  state.previous = state.current;
  state.operator = op;
  state.current = "0";
}

Pressing an operator when one is already pending finishes the previous sum first. That is what makes 2 + 3 + 4 show 5 before you press the second plus, exactly like a real calculator.

Step 4: one listener for every button#

JavaScript
document.querySelector("#keys").addEventListener("click", (event) => {
  const button = event.target.closest("button");
  if (!button) return;

  if (button.dataset.num) {
    inputNumber(button.dataset.num);
  } else if (button.dataset.op) {
    chooseOperator(button.dataset.op);
  } else if (button.dataset.action === "clear") {
    state.current = "0";
    state.previous = null;
    state.operator = null;
  } else if (button.dataset.action === "delete") {
    state.current = state.current.slice(0, -1) || "0";
  } else if (button.dataset.action === "equals") {
    equals();
  }

  render();
});

function equals() {
  if (state.operator === null || state.previous === null) return;

  const result = calculate(state.previous, state.current, state.operator);
  if (result === null) { showError(); return; }

  state.current = String(Number(result.toFixed(10)));   // trim float noise
  state.previous = null;
  state.operator = null;
}

function showError() {
  state.current = "Cannot divide by 0";
  state.previous = null;
  state.operator = null;
}

render();

One listener on the container instead of nineteen on the buttons — see JavaScript events explained.

Step 5: keyboard support#

Ten lines that make it feel finished:

JavaScript
document.addEventListener("keydown", (event) => {
  const key = event.key;

  if (/^[0-9.]$/.test(key)) inputNumber(key);
  else if (["+", "-", "*", "/"].includes(key)) chooseOperator(key);
  else if (key === "Enter" || key === "=") equals();
  else if (key === "Backspace") state.current = state.current.slice(0, -1) || "0";
  else if (key === "Escape") { state.current = "0"; state.previous = null; state.operator = null; }
  else return;

  render();
});

How the code works#

The whole app is one loop: a key is pressed → the state object changes → render draws the screen. Nothing ever reads the display to find out what the calculator is doing, which is what keeps it predictable. If you can hold that pattern in your head, most front-end frameworks will feel familiar when you meet them.

Questions people ask#

Why not use eval()?

eval("2+3") works and is a bad habit. It runs whatever text it is given, which is a serious problem the moment any of that text comes from somewhere you do not control. Writing the arithmetic yourself is also the part that teaches you something.

Why keep numbers as strings?

Because you are building up what the user typed, character by character, and "1" + "2" needs to be "12", not 3. Convert to a number only when you actually calculate.

How would I handle operator precedence?

This calculator works left to right, like a pocket calculator — 2 + 3 * 4 gives 20. Proper precedence means parsing the whole expression rather than evaluating pair by pair, which is a considerably bigger project.

Where to go next#

Next projectBuild a JavaScript quiz app

Keep reading

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *