Build a JavaScript Calculator: Complete Guide

Build a working calculator with plain JavaScript, covering number input, operator handling, event delegation, and safe evaluation of the final result.

8 min read

A JavaScript calculator app takes clicks on number and operator buttons, builds an expression, and shows the result on a display. It is a step up from a counter app because it needs to track more than one piece of state: the first number, the chosen operator, and the second number, before it can calculate anything.

You will build a grid of buttons and a display, wire up every button with a single click listener, and calculate the result without using eval(). Along the way you will use event delegation, a pattern that lets one listener handle clicks from many buttons at once.

What You Will Build

The calculator needs to support:

  • Typing digits that appear on the display.
  • Choosing an operator like add, subtract, multiply, or divide.
  • Calculating and showing the result when equals is pressed.
  • Clearing the display to start over.
Calculator input flow

Every click is one of three kinds, and the app responds differently depending on which button fired the event. The next steps build this decision one piece at a time.

Step 1: Build the HTML

A calculator button grid is a group of buttons inside one shared container.

htmlhtml
<div class="calculator">
  <input type="text" id="display" readonly />
  <div id="buttons">
    <button data-value="7">7</button><button data-value="8">8</button>
    <button data-value="9">9</button><button data-value="/">/</button>
    <button data-value="4">4</button><button data-value="5">5</button>
    <button data-value="6">6</button><button data-value="*">*</button>
    <button data-value="1">1</button><button data-value="2">2</button>
    <button data-value="3">3</button><button data-value="-">-</button>
    <button data-value="0">0</button><button data-value="+">+</button>
    <button data-value="=">=</button><button data-action="clear">C</button>
  </div>
</div>

Every button carries its value directly in a data-value attribute, and the clear button uses a separate data-action attribute. Reading the button's own attribute, instead of matching its visible text, keeps the JavaScript from breaking if the label ever changes.

Step 2: Handle Every Button with One Listener

Rather than attaching a click listener to each button, attach one listener to the container and let it figure out which button was clicked.

javascriptjavascript
const display = document.getElementById("display");
const buttons = document.getElementById("buttons");
 
buttons.addEventListener("click", (event) => {
  const button = event.target;
  if (button.tagName !== "BUTTON") return;
 
  handleButtonClick(button);
});

This technique is called event delegation, covered in more depth in the JavaScript event delegation guide. Clicking any button inside the container fires this one listener, and the guard clause ignores clicks that land on the container itself rather than a button.

Step 3: Track the Calculator State

Before writing the click logic, decide what the app needs to remember between clicks.

javascriptjavascript
let firstNumber = "";
let operator = "";
let secondNumber = "";

These three variables hold the state of an in-progress calculation. The display only shows text, so the app cannot rely on reading numbers back out of it reliably once an operator has been chosen.

Step 4: Respond to Number, Operator, and Equals Clicks

With the state variables ready, write the function that decides what each click does.

javascriptjavascript
function handleButtonClick(button) {
  const value = button.dataset.value;
 
  if (button.dataset.action === "clear") return resetCalculator();
  if (value === "=") return calculateResult();
 
  updateNumbers(value);
}

This dispatcher checks the action first, so clear always wins, then checks for equals, then hands everything else to a number and operator handler. Each of the three cases below is its own small function.

javascriptjavascript
function resetCalculator() {
  firstNumber = "";
  operator = "";
  secondNumber = "";
  display.value = "";
}

Clear wipes every stored value and the display text, so the calculator behaves exactly as if it had just loaded, ready for a brand new calculation with nothing left over from before.

javascriptjavascript
function updateNumbers(value) {
  if (!isNaN(value)) {
    if (operator) {
      secondNumber += value;
    } else {
      firstNumber += value;
    }
  } else if (firstNumber && !operator) {
    operator = value;
  }
  display.value = firstNumber + operator + secondNumber;
}

A digit appends to the first number until an operator has been chosen, then it appends to the second number instead. An operator is accepted only after the first number exists and only when no operator is already stored. These checks stop an operator from starting an incomplete expression or overwriting the first operator before equals is pressed.

Step 5: Calculate the Result Safely

The temptation with a calculator is to pass the whole display string to eval(). Avoid that, since eval() runs any text as real JavaScript code, which is unsafe the moment the input is not fully trusted.

javascriptjavascript
function getOperationResult(op, first, second) {
  if (op === "+") return first + second;
  if (op === "-") return first - second;
  if (op === "*") return first * second;
  return first / second;
}

This helper takes two real numbers and the chosen operator, then runs the matching arithmetic directly instead of evaluating arbitrary text.

javascriptjavascript
function calculateResult() {
  if (!firstNumber || !operator || !secondNumber) return;
 
  const result = getOperationResult(operator, parseFloat(firstNumber), parseFloat(secondNumber));
  display.value = result;
  firstNumber = String(result);
  operator = "";
  secondNumber = "";
}

The guard clause ignores equals until both numbers and an operator are present, preventing an incomplete expression from producing NaN. The function then converts the stored strings to real numbers before passing them to the helper above. After showing the result, it resets the operator and second number, but keeps the result as the new first number so the calculator is ready for another operation right away.

Common Mistakes

MistakeWhy it breaksFix
Using eval() on the display textRuns any text as code, which is unsafe and unpredictableConvert stored numbers with parseFloat and calculate based on the operator
Attaching a separate listener to every buttonMore code to maintain and misses buttons added laterUse one listener on the container with event delegation
Allowing a second operator click to overwrite the firstProduces an incomplete expression on the displayOnly accept an operator click when none is already stored
Calculating before both numbers are enteredConverts an empty string to NaNReturn early unless both numbers and an operator are present

Next Step

Event delegation and explicit state variables show up again in bigger UIs, including the JavaScript shopping cart project. If you have not built the JavaScript todo list app yet, it covers rendering a list from an array, a pattern that pairs well with the state tracking used here.

Rune AI

Rune AI

Key Insights

  • Use one click listener on the button container instead of one listener per button, known as event delegation.
  • Read which button was clicked from the event target, not from a separate listener per button.
  • Store the first number, the operator, and the second number in variables instead of parsing the display text.
  • Never use eval() to calculate the result; perform the arithmetic directly based on the stored operator.
  • Reset the stored state after showing a result so the next calculation starts clean.
RunePowered by Rune AI

Frequently Asked Questions

Why use one click listener on the whole button grid instead of one per button?

A single listener on the container, known as event delegation, handles clicks from every button including ones added later, and avoids attaching and tracking a separate listener for each one.

Is it safe to use eval() to calculate the result?

No. eval() runs any text as JavaScript code, which is a security risk if the input ever comes from outside your own buttons. Build the calculation from stored numbers and an operator instead.

Why does the calculator break if I click two operators in a row?

Without a check, clicking + then - overwrites the stored operator with the second click, then the display briefly shows an incomplete expression. Add a guard that ignores an operator click if one is already waiting for a second number.

Conclusion

A calculator app looks simple, but it teaches a pattern used in far bigger UIs: track state in variables, read every button through one shared handler, and only touch the display at the end. That combination of event delegation and explicit state is what keeps the logic manageable as the app grows.