DQDeepQAILearning CenterSave Progress

Build a Simple Calculator

Build a working calculator web page that adds, subtracts, multiplies and divides two numbers, and explains mistakes in plain words.

First-time coders. No coding experience needed. · About 45 minutes

Before you start

  • You can create a folder and save a file on your computer.
  • No coding knowledge needed. Every line of code is given and explained.
  • A computer with a current web browser (Chrome, Edge, Firefox or Safari).
  • A plain-text editor: Notepad on Windows, TextEdit on Mac (switched to plain text), or the free Visual Studio Code.
  • On a phone? You can do the Practice section on this page. Building the files works best on a computer.

Your result

A calculator page in your own folder that works with a mouse, a touch screen or only a keyboard, handles decimals and negative numbers, and shows a clear message for empty boxes, invalid entries and division by zero.

A web page is usually three kinds of file working together. HTML builds the parts (boxes, menu, buttons). CSS decides how they look. JavaScript makes them do something.

Our calculator follows four steps every time you press Calculate: read the boxes, check that they hold numbers, do the math, then show the answer or explain what to fix.

We do the math with plain if statements, one for each operation. We never use eval() or Function(), which run any typed text as code. That would be unsafe and hard to predict.

1. Set up your project folder

Make a new folder named calculator.

Inside it, create three empty files named exactly index.html, style.css and script.js.

Notepad (Windows): choose File > Save As, set "Save as type" to All files, and type the full name, like index.html.

TextEdit (Mac): choose Format > Make Plain Text before saving.

Visual Studio Code: choose File > New File, then save with the full name.

Why this works

Browsers read plain-text files. Keeping structure (HTML), looks (CSS) and behavior (JavaScript) in separate files makes each one shorter and easier to fix.

Check: Your folder shows three files with exactly these names. If you see index.html.txt, turn on file name extensions and rename it.

2. Build the page with HTML

Copy this code into index.html and save.

Notice that each <label for="..."> matches the id of its box. That connection is what makes the label work.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Simple Calculator</title>
  <link rel="stylesheet" href="style.css">
  <!-- "defer" waits until the page is built before running script.js -->
  <script src="script.js" defer></script>
</head>
<body>
  <main class="calculator">
    <h1>Simple Calculator</h1>

    <!-- novalidate: our own script explains mistakes in plain words -->
    <form id="calc-form" novalidate>
      <div class="field">
        <label for="first">First number</label>
        <input id="first" name="first" type="text" autocomplete="off"
               spellcheck="false" aria-describedby="number-hint first-error">
        <p id="first-error" class="error" hidden></p>
      </div>

      <div class="field">
        <label for="operator">Operation</label>
        <select id="operator" name="operator">
          <option value="add">+ Add</option>
          <option value="subtract">− Subtract</option>
          <option value="multiply">× Multiply</option>
          <option value="divide">÷ Divide</option>
        </select>
      </div>

      <div class="field">
        <label for="second">Second number</label>
        <input id="second" name="second" type="text" autocomplete="off"
               spellcheck="false" aria-describedby="number-hint second-error">
        <p id="second-error" class="error" hidden></p>
      </div>

      <p id="number-hint" class="hint">Use digits, an optional minus sign and a period for decimals, like -2.5</p>

      <div class="actions">
        <button type="submit">Calculate</button>
        <button type="reset" class="secondary">Reset</button>
      </div>
    </form>

    <section class="result" aria-labelledby="result-heading">
      <h2 id="result-heading">Result</h2>
      <!-- <output> is announced by screen readers when its text changes -->
      <output id="result" for="first operator second" aria-live="polite">Enter two numbers and choose an operation.</output>
    </section>
  </main>
</body>
</html>

Why this works

Labels tell everyone, including people using screen readers, what each box is for, and clicking a label puts the cursor in its box. A <form> lets the Enter key press Calculate. The <output> element is announced by screen readers when the answer changes. We use type="text" instead of type="number" because some phone number pads have no minus sign, and number boxes quietly empty themselves when the entry is invalid, which hides the mistake.

Check: The page is not styled yet, and pressing Calculate only reloads the page. That is expected. You will fix it in Step 8.

3. Open it in your browser

Double-click index.html, or drag it into an open browser window.

Keep this tab open. Each time you save a file, refresh the tab (Ctrl+R on Windows, Cmd+R on Mac) to see the change.

Why this works

Seeing every change right away helps you catch a mistake while it is still small.

Check: The browser tab says Simple Calculator and you can see two labeled boxes, an Operation menu and two buttons.

4. Make it readable with CSS

Copy this code into style.css, save, and refresh the browser tab.

/* Simple Calculator — finished styles */

/* 1. Colors in one place, so they are easy to change */
:root {
  --ink: #1b1f24;
  --muted: #4a5360;
  --paper: #f6f7f9;
  --card: #ffffff;
  --accent: #1f5fbf;
  --error: #b3261e;
  --focus: #f2a900;
}

/* 2. Readable page: comfortable text size and line spacing */
* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-size: 1.125rem;
  line-height: 1.5;
  color: var(--ink);
  background: var(--paper);
}

/* 3. One centered card that never gets wider than a phone-friendly column */
.calculator {
  max-width: 28rem;
  margin: 2rem auto;
  padding: 1.5rem;
  background: var(--card);
  border-radius: 12px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
}

h1 { font-size: 1.6rem; margin: 0 0 1rem; }
h2 { font-size: 1.1rem; margin: 0 0 0.25rem; }

/* 4. Each label sits directly above its box */
.field { margin-bottom: 1rem; }
label { display: block; font-weight: 600; margin-bottom: 0.25rem; }

/* 5. Big, easy-to-tap controls. 16px+ text stops phones from zooming in. */
input, select, button {
  font: inherit;
  width: 100%;
  min-height: 48px;
  padding: 0.5rem 0.75rem;
  border: 2px solid #8a94a3;
  border-radius: 8px;
  background: #fff;
  color: var(--ink);
}

/* 6. A bright outline shows where the keyboard is */
input:focus-visible, select:focus-visible, button:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/* 7. Mistakes are shown with words AND color, never color alone */
input[aria-invalid="true"] { border-color: var(--error); }
.error { color: var(--error); margin: 0.25rem 0 0; font-size: 1rem; }
.hint { color: var(--muted); font-size: 1rem; margin: 0 0 1rem; }

/* 8. Buttons side by side, each taking half the width */
.actions { display: flex; gap: 0.75rem; }
button {
  cursor: pointer;
  font-weight: 600;
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
button.secondary { background: #fff; color: var(--accent); }

/* 9. The result stands out */
.result { margin-top: 1.5rem; }
output {
  display: block;
  min-height: 3rem;
  padding: 0.75rem;
  border-radius: 8px;
  background: #eef3fb;
  font-size: 1.25rem;
  font-weight: 600;
  overflow-wrap: anywhere;
}
output[data-state="error"] { background: #fdecea; color: var(--error); }

/* 10. On narrow phones, use the full width */
@media (max-width: 480px) {
  .calculator { margin: 0; border-radius: 0; min-height: 100vh; }
}

Why this works

Large text and 48-pixel-tall controls are easy to read and tap. Text of 16 pixels or more stops iPhones from zooming in when you tap a box. The bright outline shows keyboard users where they are. Mistakes are shown with words and color, so nobody has to rely on color alone.

Check: You see a white card in the middle of the page. Press Tab a few times: a yellow outline moves from box to menu to button.

5. Read and check each number

Copy this code into script.js and save.

// ===== Step 5: Read and check a number =====
// Allowed: optional + or -, digits, and at most one period (like 12, -3, 2.5, .5)
const NUMBER_PATTERN = /^[+-]?(\d+(\.\d*)?|\.\d+)$/;

function parseNumber(text) {
  // Trim spaces and accept the typographic minus sign "−" as well as "-"
  const cleaned = text.trim().replace(/−/g, '-');

  if (cleaned === '') {
    return { ok: false, message: 'Enter a number.' };
  }
  if (cleaned.includes(',')) {
    return { ok: false, message: 'Use a period for decimals, like 2.5 (no commas).' };
  }
  if (!NUMBER_PATTERN.test(cleaned)) {
    return { ok: false, message: 'Use only digits, one optional minus sign and one period.' };
  }

  const value = Number(cleaned);
  if (!Number.isFinite(value)) {
    return { ok: false, message: 'That number is too large for this calculator.' };
  }
  return { ok: true, value: value };
}

Why this works

Everything typed into a box arrives as text, and adding text joins it: "2" + "3" makes "23". Number() turns text into a number, but it turns an empty box into 0 and "abc" into NaN (Not a Number). So we first check the text against a pattern, and if it is not a plain number we return a message that explains what to fix instead of guessing.

Check: Open the browser console (press F12 in Chrome or Edge and choose Console; on a Mac press Cmd+Option+J in Chrome). Type parseNumber("2.5") and press Enter. You should see ok: true and value: 2.5. Try parseNumber("abc") to see the message.

6. Do the math, one operation at a time

Add this code below the previous code in script.js and save.

// ===== Step 6: Do the math =====
function calculate(a, operator, b) {
  if (operator === 'add') {
    return a + b;
  }
  if (operator === 'subtract') {
    return a - b;
  }
  if (operator === 'multiply') {
    return a * b;
  }
  if (operator === 'divide') {
    if (b === 0) {
      throw new Error("You can't divide by zero. Choose a different second number.");
    }
    return a / b;
  }
  throw new Error('Choose an operation.');
}

Why this works

Each operation is spelled out, so the code can only do these four things. If you divide by zero, JavaScript returns Infinity, which is not a helpful answer. Throwing an Error stops the calculation and carries a message the page can show.

Check: In the console, calculate(9, "subtract", 12) shows -3, and calculate(1, "divide", 0) shows an error message about zero.

7. Show the answer neatly

Add this code below the previous code and save.

// ===== Step 7: Show the answer neatly =====
// Rounding policy: show at most 10 decimal places and drop trailing zeros,
// so 0.1 + 0.2 shows 0.3 and 1 ÷ 3 shows 0.3333333333.
function formatNumber(value) {
  if (!Number.isFinite(value)) {
    throw new Error('The answer is too large for this calculator.');
  }
  let text;
  if (Math.abs(value) >= 1e21) {
    text = String(value);              // huge numbers use scientific notation, like 1e+21
  } else {
    text = value.toFixed(10);          // round to 10 decimal places
    text = text.replace(/\.?0+$/, ''); // remove trailing zeros (and a lonely period)
    if (text === '-0') {
      text = '0';
    }
  }
  return text.replace('-', '−');  // show a real minus sign: −3
}

const SYMBOLS = { add: '+', subtract: '−', multiply: '×', divide: '÷' };

Why this works

Computers store decimals in binary, so 0.1 + 0.2 is stored as 0.30000000000000004. Rounding only for display shows 0.3 without changing the math. Rounding policy: answers show at most 10 decimal places with trailing zeros removed; anything smaller than 0.00000000005 shows as 0; answers of 1e+21 or more appear in scientific notation. Negative answers use a true minus sign (−).

Check: In the console, formatNumber(0.1 + 0.2) shows "0.3" and formatNumber(-3) shows "−3".

8. Connect the code to the page

Add this last piece at the bottom of script.js, save, and refresh the browser tab.

// ===== Step 8: Connect the code to the page =====
const form = document.getElementById('calc-form');
const firstInput = document.getElementById('first');
const secondInput = document.getElementById('second');
const operatorSelect = document.getElementById('operator');
const output = document.getElementById('result');
const START_MESSAGE = 'Enter two numbers and choose an operation.';

function showFieldError(input, message) {
  const errorText = document.getElementById(input.id + '-error');
  errorText.textContent = message;
  errorText.hidden = false;
  input.setAttribute('aria-invalid', 'true');
}

function clearFieldError(input) {
  const errorText = document.getElementById(input.id + '-error');
  errorText.textContent = '';
  errorText.hidden = true;
  input.removeAttribute('aria-invalid');
}

function showResult(message, state) {
  output.textContent = message;
  output.dataset.state = state;
}

function submitCalculation(event) {
  event.preventDefault(); // stop the browser from reloading the page

  clearFieldError(firstInput);
  clearFieldError(secondInput);

  const first = parseNumber(firstInput.value);
  const second = parseNumber(secondInput.value);

  if (!first.ok) {
    showFieldError(firstInput, first.message);
  }
  if (!second.ok) {
    showFieldError(secondInput, second.message);
  }
  if (!first.ok || !second.ok) {
    showResult('Fix the highlighted box, then press Calculate.', 'error');
    (first.ok ? secondInput : firstInput).focus(); // take the keyboard to the problem
    return;
  }

  const operator = operatorSelect.value;
  try {
    const answer = calculate(first.value, operator, second.value);
    showResult(
      formatNumber(first.value) + ' ' + SYMBOLS[operator] + ' ' +
      formatNumber(second.value) + ' = ' + formatNumber(answer),
      'ok'
    );
  } catch (error) {
    showResult(error.message, 'error');
  }
}

form.addEventListener('submit', submitCalculation);
// A sandbox can block native form submission before a submit event is fired.
// Handle the button and Enter directly too, without granting form permissions.
form.querySelector('button[type="submit"]').addEventListener('click', submitCalculation);
form.addEventListener('keydown', function (event) {
  if (event.key === 'Enter' && event.target.matches('input')) {
    submitCalculation(event);
  }
});

form.addEventListener('reset', function () {
  // The browser empties the boxes; we clear our messages and start over
  clearFieldError(firstInput);
  clearFieldError(secondInput);
  output.textContent = START_MESSAGE;
  delete output.dataset.state;
  firstInput.focus();
});

Why this works

The same submitCalculation function handles the form, Calculate button and Enter key. Handling the button and Enter directly also makes the calculator work inside this lesson's restricted preview, where native form submission is blocked. event.preventDefault() stops navigation. Error messages appear under the box that needs fixing, the box is marked invalid for screen readers, and focus moves there. Reset clears the messages and returns focus to the first box.

Check: Type 2 and 3, choose Add, and press Calculate. The result shows 2 + 3 = 5.

9. Test every case

Try each row in the table below and compare what you see with the Expected result.

Then do the whole thing without a mouse: press Tab to reach a box, type, press Tab to reach Operation, use the arrow keys to choose, and press Enter to calculate.

Why this works

Testing the unusual cases, like empty boxes and dividing by zero, is how you find problems before someone else does.

Check: Every row matches. If one doesn’t, use Troubleshooting below.

Test the finished calculator

Open finished calculator on its own
12 checks for your calculator
FirstOperationSecondExpected result
2Add32 + 3 = 5
9Subtract129 − 12 = −3
6Multiply76 × 7 = 42
8Divide28 ÷ 2 = 4
0.1Add0.20.1 + 0.2 = 0.3
1Divide31 ÷ 3 = 0.3333333333
-2.5Multiply4−2.5 × 4 = −10
7Divide0You can't divide by zero. Choose a different second number.
(empty)Add3Under First number: Enter a number.
abcAdd3Under First number: Use only digits, one optional minus sign and one period.
2,5Add1Under First number: Use a period for decimals, like 2.5 (no commas).
Press ResetBoxes empty, Operation back to Add, "Enter two numbers and choose an operation.", cursor in First number

Practice: finish calculate()

This practice works on any device, including phones. The code below only knows how to add. Finish it so it can also subtract, multiply and divide, and so dividing by zero throws a helpful Error. Choose Check my code to run eight checks.

Loading local practice…

Starter and finished files

Start here

index.htmlDownload index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Simple Calculator</title>
  <link rel="stylesheet" href="style.css">
  <!-- "defer" waits until the page is built before running script.js -->
  <script src="script.js" defer></script>
</head>
<body>
  <main class="calculator">
    <h1>Simple Calculator</h1>

    <!-- Step 2: add the form (two labeled number boxes, the operation menu,
         and the Calculate and Reset buttons) here. -->

    <!-- Step 2: add the Result section with an <output> element here. -->
  </main>
</body>
</html>
style.cssDownload style.css
/* Simple Calculator — your styles */

/* Step 4: add the styles from the lesson here.
   Tip: save this file, then refresh the browser tab to see each change. */
script.jsDownload script.js
// Simple Calculator — your code
// Rule for this project: never use eval() or Function(). We do the math with if/else.

// Step 5: write parseNumber(text) here — it turns box text into a number or a message.

// Step 6: write calculate(a, operator, b) here — add, subtract, multiply, divide.

// Step 7: write formatNumber(value) here — round to 10 decimal places for display.

// Step 8: connect the form's submit and reset events here.

Compare your finished code

index.htmlDownload index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Simple Calculator</title>
  <link rel="stylesheet" href="style.css">
  <!-- "defer" waits until the page is built before running script.js -->
  <script src="script.js" defer></script>
</head>
<body>
  <main class="calculator">
    <h1>Simple Calculator</h1>

    <!-- novalidate: our own script explains mistakes in plain words -->
    <form id="calc-form" novalidate>
      <div class="field">
        <label for="first">First number</label>
        <input id="first" name="first" type="text" autocomplete="off"
               spellcheck="false" aria-describedby="number-hint first-error">
        <p id="first-error" class="error" hidden></p>
      </div>

      <div class="field">
        <label for="operator">Operation</label>
        <select id="operator" name="operator">
          <option value="add">+ Add</option>
          <option value="subtract">− Subtract</option>
          <option value="multiply">× Multiply</option>
          <option value="divide">÷ Divide</option>
        </select>
      </div>

      <div class="field">
        <label for="second">Second number</label>
        <input id="second" name="second" type="text" autocomplete="off"
               spellcheck="false" aria-describedby="number-hint second-error">
        <p id="second-error" class="error" hidden></p>
      </div>

      <p id="number-hint" class="hint">Use digits, an optional minus sign and a period for decimals, like -2.5</p>

      <div class="actions">
        <button type="submit">Calculate</button>
        <button type="reset" class="secondary">Reset</button>
      </div>
    </form>

    <section class="result" aria-labelledby="result-heading">
      <h2 id="result-heading">Result</h2>
      <!-- <output> is announced by screen readers when its text changes -->
      <output id="result" for="first operator second" aria-live="polite">Enter two numbers and choose an operation.</output>
    </section>
  </main>
</body>
</html>
style.cssDownload style.css
/* Simple Calculator — finished styles */

/* 1. Colors in one place, so they are easy to change */
:root {
  --ink: #1b1f24;
  --muted: #4a5360;
  --paper: #f6f7f9;
  --card: #ffffff;
  --accent: #1f5fbf;
  --error: #b3261e;
  --focus: #f2a900;
}

/* 2. Readable page: comfortable text size and line spacing */
* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-size: 1.125rem;
  line-height: 1.5;
  color: var(--ink);
  background: var(--paper);
}

/* 3. One centered card that never gets wider than a phone-friendly column */
.calculator {
  max-width: 28rem;
  margin: 2rem auto;
  padding: 1.5rem;
  background: var(--card);
  border-radius: 12px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
}

h1 { font-size: 1.6rem; margin: 0 0 1rem; }
h2 { font-size: 1.1rem; margin: 0 0 0.25rem; }

/* 4. Each label sits directly above its box */
.field { margin-bottom: 1rem; }
label { display: block; font-weight: 600; margin-bottom: 0.25rem; }

/* 5. Big, easy-to-tap controls. 16px+ text stops phones from zooming in. */
input, select, button {
  font: inherit;
  width: 100%;
  min-height: 48px;
  padding: 0.5rem 0.75rem;
  border: 2px solid #8a94a3;
  border-radius: 8px;
  background: #fff;
  color: var(--ink);
}

/* 6. A bright outline shows where the keyboard is */
input:focus-visible, select:focus-visible, button:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

/* 7. Mistakes are shown with words AND color, never color alone */
input[aria-invalid="true"] { border-color: var(--error); }
.error { color: var(--error); margin: 0.25rem 0 0; font-size: 1rem; }
.hint { color: var(--muted); font-size: 1rem; margin: 0 0 1rem; }

/* 8. Buttons side by side, each taking half the width */
.actions { display: flex; gap: 0.75rem; }
button {
  cursor: pointer;
  font-weight: 600;
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
button.secondary { background: #fff; color: var(--accent); }

/* 9. The result stands out */
.result { margin-top: 1.5rem; }
output {
  display: block;
  min-height: 3rem;
  padding: 0.75rem;
  border-radius: 8px;
  background: #eef3fb;
  font-size: 1.25rem;
  font-weight: 600;
  overflow-wrap: anywhere;
}
output[data-state="error"] { background: #fdecea; color: var(--error); }

/* 10. On narrow phones, use the full width */
@media (max-width: 480px) {
  .calculator { margin: 0; border-radius: 0; min-height: 100vh; }
}
script.jsDownload script.js
// Simple Calculator — finished version
// It reads two numbers and an operation, does the math with plain if/else
// statements, and shows the answer or a clear message about what to fix.
// It never uses eval() or Function(): those run any text as code, which is unsafe.

// ===== Step 5: Read and check a number =====
// Allowed: optional + or -, digits, and at most one period (like 12, -3, 2.5, .5)
const NUMBER_PATTERN = /^[+-]?(\d+(\.\d*)?|\.\d+)$/;

function parseNumber(text) {
  // Trim spaces and accept the typographic minus sign "−" as well as "-"
  const cleaned = text.trim().replace(/−/g, '-');

  if (cleaned === '') {
    return { ok: false, message: 'Enter a number.' };
  }
  if (cleaned.includes(',')) {
    return { ok: false, message: 'Use a period for decimals, like 2.5 (no commas).' };
  }
  if (!NUMBER_PATTERN.test(cleaned)) {
    return { ok: false, message: 'Use only digits, one optional minus sign and one period.' };
  }

  const value = Number(cleaned);
  if (!Number.isFinite(value)) {
    return { ok: false, message: 'That number is too large for this calculator.' };
  }
  return { ok: true, value: value };
}

// ===== Step 6: Do the math =====
function calculate(a, operator, b) {
  if (operator === 'add') {
    return a + b;
  }
  if (operator === 'subtract') {
    return a - b;
  }
  if (operator === 'multiply') {
    return a * b;
  }
  if (operator === 'divide') {
    if (b === 0) {
      throw new Error("You can't divide by zero. Choose a different second number.");
    }
    return a / b;
  }
  throw new Error('Choose an operation.');
}

// ===== Step 7: Show the answer neatly =====
// Rounding policy: show at most 10 decimal places and drop trailing zeros,
// so 0.1 + 0.2 shows 0.3 and 1 ÷ 3 shows 0.3333333333.
function formatNumber(value) {
  if (!Number.isFinite(value)) {
    throw new Error('The answer is too large for this calculator.');
  }
  let text;
  if (Math.abs(value) >= 1e21) {
    text = String(value);              // huge numbers use scientific notation, like 1e+21
  } else {
    text = value.toFixed(10);          // round to 10 decimal places
    text = text.replace(/\.?0+$/, ''); // remove trailing zeros (and a lonely period)
    if (text === '-0') {
      text = '0';
    }
  }
  return text.replace('-', '−');  // show a real minus sign: −3
}

const SYMBOLS = { add: '+', subtract: '−', multiply: '×', divide: '÷' };

// ===== Step 8: Connect the code to the page =====
const form = document.getElementById('calc-form');
const firstInput = document.getElementById('first');
const secondInput = document.getElementById('second');
const operatorSelect = document.getElementById('operator');
const output = document.getElementById('result');
const START_MESSAGE = 'Enter two numbers and choose an operation.';

function showFieldError(input, message) {
  const errorText = document.getElementById(input.id + '-error');
  errorText.textContent = message;
  errorText.hidden = false;
  input.setAttribute('aria-invalid', 'true');
}

function clearFieldError(input) {
  const errorText = document.getElementById(input.id + '-error');
  errorText.textContent = '';
  errorText.hidden = true;
  input.removeAttribute('aria-invalid');
}

function showResult(message, state) {
  output.textContent = message;
  output.dataset.state = state;
}

function submitCalculation(event) {
  event.preventDefault(); // stop the browser from reloading the page

  clearFieldError(firstInput);
  clearFieldError(secondInput);

  const first = parseNumber(firstInput.value);
  const second = parseNumber(secondInput.value);

  if (!first.ok) {
    showFieldError(firstInput, first.message);
  }
  if (!second.ok) {
    showFieldError(secondInput, second.message);
  }
  if (!first.ok || !second.ok) {
    showResult('Fix the highlighted box, then press Calculate.', 'error');
    (first.ok ? secondInput : firstInput).focus(); // take the keyboard to the problem
    return;
  }

  const operator = operatorSelect.value;
  try {
    const answer = calculate(first.value, operator, second.value);
    showResult(
      formatNumber(first.value) + ' ' + SYMBOLS[operator] + ' ' +
      formatNumber(second.value) + ' = ' + formatNumber(answer),
      'ok'
    );
  } catch (error) {
    showResult(error.message, 'error');
  }
}

form.addEventListener('submit', submitCalculation);
// A sandbox can block native form submission before a submit event is fired.
// Handle the button and Enter directly too, without granting form permissions.
form.querySelector('button[type="submit"]').addEventListener('click', submitCalculation);
form.addEventListener('keydown', function (event) {
  if (event.key === 'Enter' && event.target.matches('input')) {
    submitCalculation(event);
  }
});

form.addEventListener('reset', function () {
  // The browser empties the boxes; we clear our messages and start over
  clearFieldError(firstInput);
  clearFieldError(secondInput);
  output.textContent = START_MESSAGE;
  delete output.dataset.state;
  firstInput.focus();
});

If you get stuck

Pressing Calculate reloads the page or nothing happens.

Check that script.js is saved in the same folder and spelled exactly like the <script src="script.js" defer> line. Open the console (F12) and read any red error. Make sure event.preventDefault() is the first line inside the submit handler.

The console says "Cannot read properties of null (reading 'addEventListener')".

An id in getElementById doesn’t match an id in index.html (they must match exactly, including capitals), or the defer word is missing from the script tag.

2 + 3 shows 23.

The code is adding text, not numbers. Use parseNumber() (Step 5) and do the math on first.value and second.value.

The answer shows NaN.

Something that isn’t a number reached the math. Make sure you return early when parseNumber() reports a problem.

0.1 + 0.2 shows 0.30000000000000004.

Show the answer with formatNumber() from Step 7.

Dividing by zero shows Infinity.

Check b === 0 before dividing, and throw an Error with a message (Step 6).

The browser shows my code as text instead of a page.

The file was saved as index.html.txt or as rich text. Turn on file name extensions and rename it, or in TextEdit choose Format > Make Plain Text.

My styles don’t change anything.

Check that style.css is in the same folder and the <link rel="stylesheet" href="style.css"> line is spelled the same. Refresh with Ctrl+Shift+R (Windows) or Cmd+Shift+R (Mac).

"Uncaught SyntaxError" appears.

A bracket, brace or quote is missing. Go to the line number in the message and check that every ( { [ " has a partner.

Practice says it stopped after 2 seconds.

Your code has a loop that never ends. Check every while and for loop for a condition that eventually becomes false.

Challenges

Add a remainder operation

Add <option value="remainder">% Remainder</option> to the menu, a branch in calculate() that returns a % b (with the same zero check as divide), and remainder: '%' to SYMBOLS.

Hint and expected result

Copy the divide branch and change two things.

17 % 5 = 2, and 4 % 0 shows the zero message.

Show the last three answers

Add an empty <ol id="history"></ol> under the result. After each successful calculation, add the result as a new <li> at the top of the list and remove any items after the third.

Hint and expected result

Use list.prepend(item) and list.children.length.

Newest answer first, never more than three items. Reset can clear the list too.

Money mode

Make a copy of formatNumber() that always shows exactly two decimal places using toFixed(2).

Hint and expected result

Rounding changes only what you see, not the stored number.

10 ÷ 3 shows 3.33 and 2 + 3 shows 5.00.

Swap the numbers

Add a <button type="button">Swap</button> that swaps the text in the two boxes.

Hint and expected result

Save the first box’s value in a variable before you overwrite it.

9 and 12 become 12 and 9.

Check your understanding

Why does every box have a <label>?

It names the box for everyone, including screen-reader users, and clicking the label moves the cursor into the box.

Why don’t we use eval() to do the math?

eval() runs any text as code, which is unsafe and unpredictable. Separate if statements allow only our four operations.

Why does the answer need rounding before it is shown?

Computers store most decimals as close binary approximations, so 0.1 + 0.2 is stored as 0.30000000000000004. We round only the display.

Ready to mark this complete?Device progress uses browser storage. Account sync requires sign-in and a successful save. Clearing browser data removes device-only progress.
Next Lesson →